"use strict"; (function (global) { function socialPersonalityProfile(tarinai) { if (tarinai && typeof tarinai.personalityProfile === "function") return tarinai.personalityProfile(); const base = { label: "\u4E2D\u7ACB", fear: 1, fight: 1, social: 1, relation: 1, sleep: 1, zunda: 1, play: 1 }; const cur = tarinai?.currentPersonality || {}; const neu = Number(cur.neuroticism || 0) || 0; const aggr = Number(cur.aggression || 0) || 0; const soc = Number(cur.sociability || 0) || 0; const open = Number(cur.openness || 0) || 0; return { ...base, fear: clamp(1 + neu * 0.38, 0.55, 1.85), fight: clamp(1 + aggr * 0.55, 0.35, 2.35), social: clamp(1 + soc * 0.24, 0.62, 1.52), relation: clamp(1 + soc * 0.24, 0.62, 1.60), play: clamp(1 + open * 0.45, 0.45, 2.10), zunda: 1, sleep: 1, }; } function isSocialTarinaiEntity(value) { return Boolean(value && !value.dead && typeof value.relationTo === "function" && Number.isFinite(value.x) && Number.isFinite(value.y)); } const World = global.World; if (!World) throw new Error("World is not available for mixin: world_family_social.js"); const resetPresets = global.TarinaiWorldResetPresets || global.TarinaiResetPresets || {}; const normalizeResetPresetId = resetPresets.normalize || ((presetId = "default") => String(presetId || "default")); const generatePresetWorld = resetPresets.generatePresetWorld || (() => {}); const groundForResetPreset = resetPresets.groundForResetPreset || (() => "soil"); const resetPresetLabel = resetPresets.label || (() => "\u30C7\u30D5\u30A9\u30EB\u30C8"); function pointerLargeTarinaiTooltipCandidatesLocal(worldRef, now = worldRef?.time || 0) { const cache = worldRef._pointerTooltipLargeTarinaiCache; if (cache && worldRef._pointerTooltipLargeTarinaiSerial === worldRef.liveIdSerial && now < (worldRef._pointerTooltipLargeTarinaiUntil || 0)) return cache; const out = []; for (const t of worldRef.tarinai || []) { if (!t || t.dead) continue; if ((Number(t.radius) || 20) > 64) out.push(t); } worldRef._pointerTooltipLargeTarinaiCache = out; worldRef._pointerTooltipLargeTarinaiSerial = worldRef.liveIdSerial; worldRef._pointerTooltipLargeTarinaiUntil = now + 0.35; return out; } function pointerTarinaiTooltipCandidatesLocal(worldRef, p) { const all = Array.isArray(worldRef.tarinai) ? worldRef.tarinai : []; if (!p?.inside || !all.length) return all; if (all.length < 72 || typeof worldRef.nearbyTarinai !== "function") return all; const local = worldRef.nearbyTarinai(p.x, p.y, 128, false) || []; const large = pointerLargeTarinaiTooltipCandidatesLocal(worldRef, worldRef.time || 0) || []; if (!large.length) return local; const stamp = (worldRef._pointerTooltipCandidateStamp = (worldRef._pointerTooltipCandidateStamp || 0) + 1); for (const t of local) if (t) t._pointerTooltipCandidateStamp = stamp; for (const t of large) { if (!t || t.dead || t._pointerTooltipCandidateStamp === stamp) continue; t._pointerTooltipCandidateStamp = stamp; local.push(t); } return local; } function pointerTarinaiTooltipInfoLocal(worldRef) { const p = worldRef.pointer; if (!p?.inside || !Array.isArray(worldRef.tarinai)) return null; let best = null, bestD = Infinity; for (const t of pointerTarinaiTooltipCandidatesLocal(worldRef, p) || worldRef.tarinai) { if (!t || t.dead) continue; if (worldRef.isTarinaiHiddenInNestBox?.(t)) continue; const d = distXY(p.x, p.y, t.x, t.y); const limit = Math.max(46, (t.radius || 20) * 1.35); if (d <= limit && d < bestD) { best = t; bestD = d; } } if (!best) return null; const nameLine = String(best.name || "たりない"); const behavior = (typeof behaviorText === "function" ? behaviorText(best) : "") || window.TEXT_CATALOG?.stateLabel?.(best.state, best) || best.thought || "様子を見ている"; const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(best) : (Number(best.maxEnergy) || 100); const statusLine = `体力: ${Math.round(best.energy || 0)}/${Math.round(maxEnergy)} 空腹: ${Math.round(best.hunger || 0)} ストレス: ${Math.round(best.stress || 0)}`; const tempLine = worldRef.temperatureTooltipLineFor(best, "体感気温") || "体感気温: --℃"; return { target: best, lines: [nameLine, String(behavior), statusLine, tempLine], kind: "tarinai" }; } function pointerOwnedBedInfoLocal(worldRef) { const p = worldRef.pointer; if (!p?.inside) return null; let best = null, bestD = Infinity; for (const it of worldRef.nearbyItems(p.x, p.y, 140)) { if (!it || it.dead || it.type !== "grass_bed") continue; const d = distXY(p.x, p.y, it.x, it.y); if (d < bestD && d <= Math.max(60, (it.r || 24) * 2.0)) { best = it; bestD = d; } } if (!best) return null; const owner = worldRef.liveTarinaiById?.(best.ownerId) || null; return { bed: best, owner }; } function ownedBedTooltipLinesLocal(worldRef, bed) { if (!bed || bed.dead || bed.type !== "grass_bed") return []; const owner = worldRef.liveTarinaiById?.(bed.ownerId) || null; return ["かんたんベッド", `持ち主: ${owner?.name || "不明"}`, worldRef.temperatureTooltipLineFor?.(bed, "周辺気温")].filter(Boolean); } Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ reset(seedPopulation = null, fieldType = this.fieldType || "garden", presetId = "default") { const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden; const preset = normalizeResetPresetId(presetId); this.fieldType = field.id; this.fieldZoom = 1; this.setViewportSize(this.viewportW, this.viewportH); this.time = 0; this.day = 1; this.paused = false; this.speed = 1; this.toolSize = this.toolSize || "medium"; this.toolSizes = this.toolSizes || {}; this.resetRuntimeCollections?.(); this.itemCounts = {}; this.effectCounts = {}; this.itemTypeBuckets = new Map(); this.itemIdMap = new Map(); this.itemBucketsDirty = false; this.grassGrowthTimer = 0; this.nextGrassLimitCheckAt = 0; this.worldSeed = global.TarinaiSeedFactory.createWorldSeed() || `w${Date.now().toString(36)}`; this.birthSerial = 0; this.pointer = { x: this.w / 2, y: this.h / 2, inside: false, motion: 0 }; this.cameraX = Math.max(0, (this.w - (this.viewportW || this.w)) / 2); this.cameraY = Math.max(0, (this.h - (this.viewportH || this.h)) / 2); this.shakeTimer = 0; this.shakeDuration = 0; this.shakeStrength = 0; this.clampCamera(); this.lastPhase = this.phaseName(); this.weather = "sunny"; this.groundType = window.TarinaiGround.exists(this.groundType || "soil") ? (this.groundType || "soil") : "soil"; const presetGround = groundForResetPreset(preset); if (presetGround && window.TarinaiGround.exists(presetGround)) this.groundType = presetGround; this.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); generatePresetWorld(this, field, preset, seedPopulation); this.updateItemCounts(); this.enforceGrassLimit?.("reset-grass-limit"); this.updateItemCounts(); this.updateEffectCounts(); this.markTerrainDirty?.("reset"); this.rebuildSpatial(true); const label = resetPresetLabel(preset); showToast(`${label}\u3067\u65B0\u3057\u3044\u89B3\u5BDF\u3092\u958B\u59CB\u3057\u307E\u3057\u305F\u3002`); syncTopButtons(); }, tarinaiFamilyKey(t) { return t?.familyKey || t?.id || ""; }, assignTarinaiLiveId(t) { if (!t) return ""; if (!this.liveTarinai) this.liveTarinai = new Map(); // Live IDs are never recycled. Relationships and transient targets are keyed by // live ID, so reuse can make dead friends/enemies leak onto new individuals. if (!Number.isFinite(this.liveIdNext)) this.liveIdNext = 1; if (!Number.isFinite(this.liveIdSerial)) this.liveIdSerial = 0; const id = `t${this.liveIdNext++}`; t.id = id; t.liveToken = ++this.liveIdSerial; this.liveTarinai.set(id, { token: t.liveToken, target: t }); return id; }, releaseTarinaiLiveId(t) { if (!t?.id || !this.liveTarinai) return false; const current = this.liveTarinai.get(t.id); if (!current || current.target !== t || current.token !== t.liveToken) return false; const id = t.id; this.liveTarinai.delete(id); // Do not recycle live IDs. Relationships are keyed by live ID; recycling can // make a dead friend's/enemy's relationship appear on a newly spawned individual. t.releasedLiveId = id; t.id = ""; t.liveToken = 0; return true; }, liveTarinaiById(id, token = null) { if (!id || !this.liveTarinai) return null; const entry = this.liveTarinai.get(id); if (!entry || (token !== null && entry.token !== token)) return null; const t = entry.target; return t && !t.dead ? t : null; }, recordFamily(t) { if (!this.family) this.family = {}; const familyKey = this.tarinaiFamilyKey(t); if (!familyKey) return; const uniqueIds = (list) => Array.from(new Set((list || []).filter(Boolean))); const sameList = (a, b) => { const aa = uniqueIds(a); const bb = uniqueIds(b); return aa.length === bb.length && aa.every((id, i) => id === bb[i]); }; let changed = false; const nextParents = uniqueIds(t.parents || []); const nextChildren = uniqueIds(t.children || []); const nextPersonalityTags = getPersonalityTraitTags(t); const nextScale = Number.isFinite(t.scale) ? t.scale : (Number.isFinite(t.adultScale) ? t.adultScale : 0.28); const nextAdultScale = Number.isFinite(t.adultScale) ? t.adultScale : nextScale; const nextGrowth = Number.isFinite(t.growth) ? t.growth : 1; const nextGenetics = typeof normalizeGenetics === "function" ? normalizeGenetics(t.genetics, t.familyKey || t.id || "") : { ...(t.genetics || {}) }; const nextFightWins = Number(t.totalFightWins || 0) || 0; const nextFightLosses = Number(t.totalFightLosses || 0) || 0; const nextChampion = !!t.isTarinaiChampion; if (!this.family[familyKey]) { this.family[familyKey] = { id: familyKey, name: t.name, type: t.type, personalityTags: nextPersonalityTags, generation: t.generation, hasPaired: !!t.hasPaired, parents: nextParents, parentNames: [...(t.parentNames || [])], children: nextChildren, birthTime: t.birthTime, scale: nextScale, adultScale: nextAdultScale, growth: nextGrowth, genetics: nextGenetics, lifeSpan: Number(t.lifeSpan) || 0, totalFightWins: nextFightWins, totalFightLosses: nextFightLosses, isTarinaiChampion: nextChampion, alive: !t.dead, deathReason: t.deathReason || "", }; changed = true; } else { const entry = this.family[familyKey]; const nextAlive = !t.dead; if (entry.name !== t.name) changed = true; if (entry.type !== t.type) changed = true; if (!sameList(entry.personalityTags, nextPersonalityTags)) changed = true; if (entry.hasPaired !== !!t.hasPaired) changed = true; if (entry.generation !== t.generation) changed = true; if (Math.abs((Number(entry.scale) || 0) - nextScale) > 0.002) changed = true; if (Math.abs((Number(entry.adultScale) || 0) - nextAdultScale) > 0.002) changed = true; if (Math.abs((Number(entry.growth) || 0) - nextGrowth) > 0.02) changed = true; for (const key of ["lifeSpanMul", "attackMul", "speedMul", "sizeMul", "temperatureOffset"]) { const fallback = key === "temperatureOffset" ? 0 : 1; const prevGene = Number.isFinite(Number(entry.genetics?.[key])) ? Number(entry.genetics?.[key]) : fallback; const nextGene = Number.isFinite(Number(nextGenetics?.[key])) ? Number(nextGenetics?.[key]) : fallback; if (Math.abs(prevGene - nextGene) > 0.002) changed = true; } if (Math.abs((Number(entry.lifeSpan) || 0) - (Number(t.lifeSpan) || 0)) > 1) changed = true; if ((Number(entry.totalFightWins) || 0) !== nextFightWins) changed = true; if ((Number(entry.totalFightLosses) || 0) !== nextFightLosses) changed = true; if (!!entry.isTarinaiChampion !== nextChampion) changed = true; if ((entry.deathReason || "") !== (t.deathReason || entry.deathReason || "")) changed = true; if (entry.alive !== nextAlive) changed = true; if (!sameList(entry.parents, nextParents)) changed = true; if (!sameList(entry.children, nextChildren)) changed = true; entry.name = t.name; entry.type = t.type; delete entry.personality; entry.personalityTags = nextPersonalityTags; entry.alive = nextAlive; entry.generation = t.generation; entry.scale = nextScale; entry.adultScale = nextAdultScale; entry.growth = nextGrowth; entry.genetics = nextGenetics; entry.lifeSpan = Number(t.lifeSpan) || entry.lifeSpan || 0; entry.totalFightWins = nextFightWins; entry.totalFightLosses = nextFightLosses; entry.isTarinaiChampion = nextChampion; entry.hasPaired = !!t.hasPaired; entry.deathReason = t.deathReason || entry.deathReason || ""; entry.parents = nextParents; entry.parentNames = [...(t.parentNames || entry.parentNames || [])]; entry.children = nextChildren; } if (changed) { this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("family-record"); } }, markFamilyTreeDirty(reason = "family") { this.familyTreeDirty = true; }, normalizeFamily() { if (!this.family) this.family = {}; if (this.familyCleanVersion === (this.familyVersion || 0)) return false; const family = this.family; const liveById = new Map((this.tarinai || []).map(t => [this.tarinaiFamilyKey(t), t]).filter(([id]) => id)); let changed = false; const uniqueIds = (list, allow) => Array.from(new Set((list || []).filter(id => id && (!allow || allow.has(id))))); const sameList = (a, b) => a.length === b.length && a.every((id, i) => id === b[i]); for (const t of this.tarinai || []) { const familyKey = this.tarinaiFamilyKey(t); if (!familyKey || (!this.family[familyKey] && !(t.hasPaired || (t.parents || []).length || (t.children || []).length))) continue; const before = this.familyVersion || 0; this.recordFamily(t); changed = changed || before !== (this.familyVersion || 0); } const known = new Set(Object.keys(family)); for (const id of Object.keys(family)) { const n = family[id] || {}; const live = liveById.get(id); const parents = uniqueIds(n.parents, known); const children = uniqueIds(n.children, known); if (live) { const liveParents = uniqueIds(live.parents, known); const liveChildren = uniqueIds(live.children, known); for (const p of liveParents) if (!parents.includes(p)) parents.push(p); for (const c of liveChildren) if (!children.includes(c)) children.push(c); } const next = { id, name: live?.name || n.name || id, type: live?.type || n.type || "smile", personalityTags: live ? getPersonalityTraitTags(live) : [...(n.personalityTags || [])], generation: Math.max(1, live?.generation || n.generation || 1), hasPaired: !!(live?.hasPaired || n.hasPaired), parents, parentNames: live?.parentNames ? [...live.parentNames] : [...(n.parentNames || [])], children, birthTime: live?.birthTime ?? n.birthTime ?? 0, scale: Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : (Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : 0.28))), adultScale: Number.isFinite(live?.adultScale) ? live.adultScale : (Number.isFinite(n.adultScale) ? n.adultScale : (Number.isFinite(live?.scale) ? live.scale : (Number.isFinite(n.scale) ? n.scale : 0.28))), growth: Number.isFinite(live?.growth) ? live.growth : (Number.isFinite(n.growth) ? n.growth : 1), genetics: live && typeof normalizeGenetics === "function" ? normalizeGenetics(live.genetics, live.familyKey || live.id || "") : (typeof normalizeGenetics === "function" ? normalizeGenetics(n.genetics, id) : { ...(n.genetics || {}) }), lifeSpan: Number(live?.lifeSpan || n.lifeSpan || 0) || 0, totalFightWins: Number(live?.totalFightWins ?? n.totalFightWins ?? 0) || 0, totalFightLosses: Number(live?.totalFightLosses ?? n.totalFightLosses ?? 0) || 0, isTarinaiChampion: !!(live?.isTarinaiChampion || n.isTarinaiChampion), alive: live ? !live.dead : n.alive !== false, deathReason: live?.deathReason || n.deathReason || "", }; for (const key of Object.keys(next)) { const a = family[id]?.[key]; const b = next[key]; let equal = false; if (key === "genetics") { equal = ["lifeSpanMul", "attackMul", "speedMul", "sizeMul", "temperatureOffset"].every(gk => { const fallback = gk === "temperatureOffset" ? 0 : 1; const av = Number.isFinite(Number(a?.[gk])) ? Number(a?.[gk]) : fallback; const bv = Number.isFinite(Number(b?.[gk])) ? Number(b?.[gk]) : fallback; return Math.abs(av - bv) <= 0.002; }); } else { equal = Array.isArray(a) && Array.isArray(b) ? sameList(a, b) : a === b; } if (!equal) changed = true; } family[id] = next; } for (const child of Object.values(family)) { for (const parentId of child.parents || []) { const parent = family[parentId]; if (!parent) continue; if (!parent.children.includes(child.id)) { parent.children.push(child.id); changed = true; } } } for (const parent of Object.values(family)) { const nextChildren = uniqueIds(parent.children, known).filter(childId => family[childId]?.parents?.includes(parent.id)); if (!sameList(parent.children || [], nextChildren)) { parent.children = nextChildren; changed = true; } } for (const id of Object.keys(family)) { const n = family[id]; if (!n?.hasPaired && !(n?.parents || []).length && !(n?.children || []).length) { delete family[id]; changed = true; } } for (const [id, live] of liveById.entries()) { const n = family[id]; if (!n) continue; if (!sameList(live.parents || [], n.parents || [])) { live.parents = [...(n.parents || [])]; changed = true; } if (!sameList(live.children || [], n.children || [])) { live.children = [...(n.children || [])]; changed = true; } if (live.hasPaired !== !!n.hasPaired) { live.hasPaired = !!n.hasPaired; changed = true; } if (live.generation !== n.generation) { live.generation = n.generation; changed = true; } } if (changed) { this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("family-normalize"); } this.familyCleanVersion = this.familyVersion || 0; return changed; }, resetFamilyTree() { this.family = {}; this.familyCleanVersion = 0; this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("family-reset"); this.maxGeneration = 1; for (const t of this.tarinai || []) { t.parents = []; t.parentNames = []; t.children = []; t.generation = 1; t.hasPaired = false; } }, clearLiveReferencesTo(dead) { const deadId = dead?.id || dead?.releasedLiveId || ""; if (!deadId) return false; let changed = false; for (const other of this.tarinai || []) { if (!other || other === dead || other.dead) continue; if (other.target === dead || other.target?.id === deadId) { other.target = null; if (["seek_friend", "panic", "fight", "intimidate"].includes(other.state)) other.goIdle?.("\u53c2\u7167\u3057\u3066\u3044\u305f\u76f8\u624b\u304c\u3044\u306a\u304f\u306a\u3063\u305f"); changed = true; } if (other.panicTarget === dead || other.panicTarget?.id === deadId) { other.panicTarget = null; changed = true; } if (other.fightTargetId === deadId) { other.fightTargetId = null; changed = true; } if (Array.isArray(other.fightTargetIds) && other.fightTargetIds.includes(deadId)) { other.fightTargetIds = other.fightTargetIds.filter(id => id !== deadId); changed = true; } for (const key of ["defeatedById", "fightWinnerId", "intimidateTargetId", "birthPartnerId"]) { if (other[key] === deadId) { other[key] = null; changed = true; } } } return changed; }, markDead(t, reason) { this.clearLiveReferencesTo(t); if (!this.family) this.family = {}; const finalReason = t?.normalizeDeathReason ? t.normalizeDeathReason(reason) : reason; const familyKey = this.tarinaiFamilyKey(t); if (familyKey && this.family[familyKey]) { if (this.family[familyKey].alive !== false || this.family[familyKey].deathReason !== finalReason) { this.family[familyKey].alive = false; this.family[familyKey].deathReason = finalReason; delete this.family[familyKey].personality; this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("death"); } } this.familyPrunePending = true; }, pruneExtinctFamilies(opts = {}) { if (opts.normalize !== false) this.normalizeFamily(); const family = this.family || {}; const nodes = Object.values(family).filter(Boolean).filter(n => n.hasPaired || (n.parents || []).length || (n.children || []).length); if (!nodes.length) return; const components = globalThis.TarinaiFamilyGraph.connectedComponents ? globalThis.TarinaiFamilyGraph.connectedComponents(nodes) : []; const remove = components.length ? components.filter(comp => !comp.some(n => family[n.id]?.alive)).flat().map(n => n.id) : []; if (!remove.length) return; for (const id of remove) delete family[id]; this.familyVersion = (this.familyVersion || 0) + 1; this.markFamilyTreeDirty("family-prune"); this.familyCleanVersion = this.familyVersion || 0; }, addTarinai(opts = {}) { if (!(this.canAddTarinai?.(1) ?? true)) return null; opts = global.TarinaiSeedFactory.prepareAddOptions(this, opts) || opts; const seedParents = Array.isArray(opts.seedParents) ? opts.seedParents.filter(p => p && typeof p === "object") : []; if (!opts.genetics && seedParents.length >= 2 && typeof inheritedGenetics === "function") { opts.genetics = inheritedGenetics(seedParents[0], seedParents[1], opts.birthSeed || opts.familyKey || ""); } if (opts.genetics) { if (!Number.isFinite(opts.adultScale) && typeof adultScaleFromGenetics === "function") opts.adultScale = adultScaleFromGenetics(opts.birthSeed || opts.familyKey || "", opts.genetics); if (!Number.isFinite(opts.lifeSpan)) { const profile = global.TarinaiSeedFactory.birthProfile(opts.birthSeed || opts.familyKey || "", opts) || {}; if (Number.isFinite(profile.lifeSpan)) opts.lifeSpan = profile.lifeSpan; } } const t = new Tarinai(this, opts); const isSeededSpawn = !Number.isFinite(Number(opts.age)) && opts.birthTime == null && !opts.dead && !(Array.isArray(opts.parents) && opts.parents.length) && !(Array.isArray(opts.seedParents) && opts.seedParents.length) && (Number(opts.generation ?? 1) <= 1); if (isSeededSpawn) { const span = Math.max(1, Number(t.lifeSpan || 1) || 1); const ratio = typeof deterministicRange === "function" ? deterministicRange(this, "initial-life-spent-ratio", 0, 0.30, t.familyKey || t.birthSeed || t.id || this.birthSerial || Math.random()) : Math.random() * 0.30; t.age = Math.max(Number(t.age || 0) || 0, span * ratio); t.birthTime = (this.time || 0) - t.age; } this.assignTarinaiLiveId(t); this.tarinai.push(t); this.drawListDirty = true; if (t.hasPaired || (t.parents || []).length || (t.children || []).length) this.recordFamily(t); this.maxGeneration = Math.max(this.maxGeneration, t.generation); return t; }, nestBoxOccupants(box, limit = 5) { return globalThis.TarinaiNestSleepSystem.nestBoxOccupants(this, box, limit) || []; }, pointerNestBoxInfo() { const p = this.pointer; if (!p?.inside) return null; let best = null, bestD = Infinity; for (const it of this.nearbyItems(p.x, p.y, 140)) { if (!it || it.dead || !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(it)) continue; const d = distXY(p.x, p.y, it.x, it.y); if (d < bestD && d <= Math.max(80, it.r * 2.1)) { best = it; bestD = d; } } if (!best) return null; const occupants = this.nestBoxOccupants(best, Infinity); const capacity = this.nestBoxCapacity(best); return { box: best, occupants, capacity }; }, nestBoxTooltipLines(box) { if (!box || box.dead || !globalThis.TarinaiToolRuntime?.isNestContainerItem?.(box)) return []; const isPipe = box.type === "pipe"; let occupants = []; if (isPipe) { const seen = new Set(); const pipes = (this.items || []).filter(it => it && !it.dead && it.type === "pipe"); for (const pipe of pipes) { const direct = this.nestBoxOccupants?.(pipe, Infinity) || []; for (const t of direct) { if (!t || t.dead) continue; const key = t.id || t.name || `${t.x},${t.y}`; if (seen.has(key)) continue; seen.add(key); occupants.push(t); } } for (const t of (this.tarinai || [])) { if (!t || t.dead || !t.insideNestBoxId) continue; const holder = this.nestBoxById?.(t.insideNestBoxId) || pipes.find(it => it.id === t.insideNestBoxId) || null; if (!holder || holder.dead || holder.type !== "pipe") continue; const key = t.id || t.name || `${t.x},${t.y}`; if (seen.has(key)) continue; seen.add(key); occupants.push(t); } } else { occupants = this.nestBoxOccupants(box, Infinity); } const names = occupants.map(t => t?.name).filter(Boolean).slice(0, isPipe ? 8 : 5); const more = occupants.length > names.length ? `、ほか${occupants.length - names.length}匹` : ""; const label = isPipe ? "土管" : "巣箱"; const insideLabel = isPipe ? "土管に入ってる子" : "入ってる子"; return [label, names.length ? `${insideLabel}: ${names.join("、")}${more}` : `${insideLabel}: なし`, this.temperatureTooltipLineFor?.(box, "内部気温")].filter(Boolean); }, temperatureTooltipLineFor(target, label = "体感気温") { if (!target || !this.temperatureAt) return ""; const isActor = Array.isArray(this.tarinai) && this.tarinai.includes(target); const temp = isActor ? (this.feltTemperatureFor?.(target) ?? this.temperatureAt(target.x, target.y, target)) : (target.type === "pipe" && this.pipeInteriorTemperatureFor ? this.pipeInteriorTemperatureFor(target) : this.temperatureAt(target.x, target.y)); if (!Number.isFinite(Number(temp))) return ""; const status = this.temperatureStatusFor?.(temp, isActor ? target : null) || null; const suffix = status?.label ? `(${status.label})` : ""; return `${label}: ${Number(temp).toFixed(1)}℃${suffix}`; }, pointerItemTooltipInfo() { const tarinaiInfo = pointerTarinaiTooltipInfoLocal(this); if (tarinaiInfo?.target) return tarinaiInfo; const nest = this.pointerNestBoxInfo?.(); if (nest?.box) return { target: nest.box, lines: this.nestBoxTooltipLines(nest.box), kind: nest.box?.type === "pipe" ? "pipe" : "nest_box" }; const bedInfo = pointerOwnedBedInfoLocal(this); if (bedInfo?.bed) return { target: bedInfo.bed, lines: ownedBedTooltipLinesLocal(this, bedInfo.bed), kind: "grass_bed" }; return null; }, startBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) return false; if (!(this.canAddTarinai?.(1) ?? true)) return false; if (!this.tarinai?.includes?.(a) || !this.tarinai?.includes?.(b)) return false; if (!!a.isZunchiSlave !== !!b.isZunchiSlave) return false; if (this.areParentChild(a, b)) return false; if (a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease) return false; if (a.birthRitualTimer > 0.04 || b.birthRitualTimer > 0.04) return false; const aJoined = a.familyJoined ? a.familyJoined() : Boolean((a.parents || []).length || (a.children || []).length); const bJoined = b.familyJoined ? b.familyJoined() : Boolean((b.parents || []).length || (b.children || []).length); if (aJoined && !bJoined) b.generation = a.generation; else if (bJoined && !aJoined) a.generation = b.generation; else if (!aJoined && !bJoined) { a.generation = 1; b.generation = 1; } // Do not mark either parent as part of a family during the ritual. // The archive should only gain nodes after a child has actually been created; // otherwise childless ritual participants appear as isolated family trees. const duration = 10.0; a.birthRitualTimer = b.birthRitualTimer = duration; a.birthRitualMax = b.birthRitualMax = duration; a.birthPartnerId = b.id; b.birthPartnerId = a.id; a.birthRitualRole = -1; b.birthRitualRole = 1; a.birthRitualLeader = true; b.birthRitualLeader = false; const ritualReason = "\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b"; const applyRitualAction = (actor, partner) => { if (!actor) return false; if (typeof actor.setActionState === "function") { actor.setActionState("birth_ritual", { target: partner || null, reason: ritualReason, need: "social", subNeed: "mate", actionId: "birth_ritual", phase: "acting" }); return true; } actor.state = "birth_ritual"; actor.target = partner || null; actor.thought = ritualReason; actor.sleeping = false; setBehaviorText(actor, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: ritualReason, reasonText: ritualReason, target: partner || null, phase: "acting", source: "state" }); return true; }; applyRitualAction(a, b); applyRitualAction(b, a); const aBehavior = currentTarinaiBehavior(a); const bBehavior = currentTarinaiBehavior(b); const aLoveCause = (aBehavior?.source === "love_mochi" || (a.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${b.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`; const bLoveCause = (bBehavior?.source === "love_mochi" || (b.loveMochiTimer || 0) > 0.04) ? "\u3078\u3053\u9905\u306e\u52b9\u679c" : `${a.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u306a\u308b`; const aText = buildReasonText("social", aBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, a, this, { causeText: aLoveCause }); const bText = buildReasonText("social", bBehavior?.tiedNeeds || ["social"], { id: "birth_ritual", need: "social", subNeed: "mate", label: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b` }, b, this, { causeText: bLoveCause }); setBehaviorText(a, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${b.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: aText, causeText: aLoveCause, target: b }); setBehaviorText(b, { need: "social", subNeed: "mate", actionId: "birth_ritual", actionLabel: `${a.name || "\u76f8\u624b"}\u3068\u3078\u3053\u3078\u3053\u3057\u3066\u3044\u308b`, reasonText: bText, causeText: bLoveCause, target: a }); const championPair = Boolean(a.isTarinaiChampion && b.isTarinaiChampion); const reproductionCooldown = championPair ? Math.max(8, CONFIG.reproductionCooldown * 0.45) + rand(1, 4) : CONFIG.reproductionCooldown + rand(2, 10); a.reproductionTimer = reproductionCooldown; b.reproductionTimer = reproductionCooldown; a.surpriseTimer = Math.max(a.surpriseTimer, 0.35); b.surpriseTimer = Math.max(b.surpriseTimer, 0.35); return true; }, cancelBirthRitual(actor, options = {}) { const source = actor || null; const partnerId = source?.birthPartnerId || null; const partner = partnerId ? this.liveTarinaiById?.(partnerId) : null; const participants = []; if (source) participants.push(source); if (partner && partner !== source) participants.push(partner); if (!participants.length) return false; const reason = options.reason || "\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f"; let canceled = false; for (const t of participants) { if (!t || t.dead) continue; if ((t.birthRitualTimer || 0) <= 0.04 && t.state !== "birth_ritual") continue; t.birthRitualTimer = 0; t.birthRitualMax = 0; t.birthPartnerId = null; t.birthRitualLeader = false; t.birthRitualRole = 0; t.postBirthPeaceTimer = Math.max(t.postBirthPeaceTimer || 0, 1.4); t.behaviorLockTimer = Math.max(t.behaviorLockTimer || 0, 0.25); clearTarinaiBehavior(t, { reason }); clearForcedBehaviorQueue(t, e => e && (e.id === "approach_mate" || e.id === "birth_ritual")); if (options.panic && t === source) { if (t.enterPanic) t.enterPanic({ target: options.target || null, reason, fear: options.fear ?? 0.95, wake: true, cause: options.cause || "birth_ritual_interrupted" }); else t.setActionState?.("panic", { target: options.target || null, reason, wake: true }); t.fearTimer = Math.max(t.fearTimer || 0, options.fear ?? 0.95); } else { t.goIdle?.(reason); t.thought = reason; } canceled = true; } if (canceled && !options.silentLog && this.time - (this.lastBirthCancelLogAt || -999) > 1.2) { this.log?.(reason, "birth", { participants }); this.lastBirthCancelLogAt = this.time || 0; } return canceled; }, cancelBirthRitualOnForce(tarinai, force = 0, options = {}) { if (!tarinai || tarinai.dead) return false; if ((tarinai.birthRitualTimer || 0) <= 0.04 && tarinai.state !== "birth_ritual") return false; const amount = Number(force) || 0; const threshold = Number(options.threshold || 180) || 180; if (amount < threshold) return false; return this.cancelBirthRitual(tarinai, { reason: options.reason || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f", target: options.target || null, fear: options.fear ?? 1.0, cause: options.cause || "external_force", panic: options.panic !== false, silentLog: !!options.silentLog, }); }, finishBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) return null; const reproductionBlockedByDisease = a.sleepDisease || b.sleepDisease || a.fightDisease || b.fightDisease; a.birthRitualTimer = b.birthRitualTimer = 0; a.birthPartnerId = b.birthPartnerId = null; a.birthRitualLeader = b.birthRitualLeader = false; a.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f"); b.goIdle("\u8a95\u751f\u306e\u524d\u3076\u308c\u304c\u7d42\u308f\u3063\u305f"); if (reproductionBlockedByDisease || (!!a.isZunchiSlave !== !!b.isZunchiSlave)) { a.birthRitualRole = b.birthRitualRole = 0; return null; } a.fightTimer = b.fightTimer = 0; a.fightTargetIds = []; b.fightTargetIds = []; a.fightTargetId = b.fightTargetId = null; a.defeatedById = b.defeatedById = null; a.postBirthPeaceTimer = b.postBirthPeaceTimer = 18; a.fightCooldown = b.fightCooldown = Math.max(CONFIG.fightCooldown + 8, a.fightCooldown || 0, b.fightCooldown || 0); const children = []; const countRoll = Math.random(); const childCount = countRoll < 0.08 ? 3 : (countRoll < 0.36 ? 2 : 1); for (let i = 0; i < childCount; i++) { const child = this.spawnChild(a, b, i, childCount); if (child) { child.postBirthPeaceTimer = 18; children.push(child); } } if (children.length > 0) this.lastBirthAt = this.time; if (children.length > 1) this.log(`${a.name}\u3068${b.name}\u306e\u5b50\u304c${children.length}\u5339\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [a, b, ...children] }); return children[0] || null; }, areParentChild(a, b) { if (!a || !b) return false; const aId = this.tarinaiFamilyKey(a) || a; const bId = this.tarinaiFamilyKey(b) || b; if (!aId || !bId) return false; return Boolean((a.parents || []).includes(bId) || (a.children || []).includes(bId) || (b.parents || []).includes(aId) || (b.children || []).includes(aId)); }, areCoParents(a, b) { if (!a || !b) return false; const aLiveId = a.id || a; const bLiveId = b.id || b; if (!aLiveId || !bLiveId || aLiveId === bLiveId) return false; if (a.birthPartnerId && a.birthPartnerId === bLiveId) return true; if (b.birthPartnerId && b.birthPartnerId === aLiveId) return true; const aId = this.tarinaiFamilyKey(a) || aLiveId; const bId = this.tarinaiFamilyKey(b) || bLiveId; if (!aId || !bId || aId === bId) return false; const aChildren = new Set((a.children || []).filter(Boolean)); if (!aChildren.size) return false; for (const id of (b.children || [])) { if (aChildren.has(id)) return true; } return false; }, canStartFight(t) { if (!t || t.dead || typeof t.relationTo !== "function") return false; if (t.sleepDisease) return false; // Disease fight rule: explosion, zunchi, and fight disease can lash out; // sleep disease cannot fight or be challenged, and other diseases cannot start fights. if (t.explosionDisease || t.zunchiDisease || t.fightDisease) return true; const flag = (value) => typeof value === "function" ? Boolean(value.call(t)) : Boolean(value); const hasOtherDisease = flag(t.disease) || flag(t.sick) || flag(t.infection) || flag(t.hasDisease); if (hasOtherDisease) return false; return true; }, canBeFightTarget(t) { return Boolean(t && !t.dead && typeof t.relationTo === "function" && !t.sleepDisease); }, canFightPair(actor, target) { return this.canStartFight(actor) && this.canBeFightTarget(target); }, spawnChild(a, b, index = 0, total = 1) { const zunchiSlaveChild = !!(a.isZunchiSlave && b.isZunchiSlave); const child = this.addTarinai({ seedParents: [a, b], isZunchiSlave: zunchiSlaveChild, age: 0, x: (a.x + b.x) / 2 + rand(-14, 14) + (index - (total - 1) / 2) * 18, y: (a.y + b.y) / 2 + rand(-14, 14) + (total > 1 ? rand(-10, 10) : 0), hunger: rand(20, 40), loneliness: rand(30, 56), energy: rand(55, 95), stress: rand(0, 14), need: "", generation: Math.max(a.generation, b.generation) + 1, parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)], parentNames: [a.name, b.name], birthTime: this.time, powerItemMode: "", sizeItemMode: "", lifeItemMode: "", }); if (!child) return null; if (zunchiSlaveChild) child.becomeZunchiSlave?.({ birth: true }); this.maxGeneration = Math.max(this.maxGeneration, child.generation); a.hasPaired = true; b.hasPaired = true; a.affection = 0; b.affection = 0; a.loneliness *= 0.65; b.loneliness *= 0.65; a.adjustRelation?.(b, 0.65, -0.20, "co_parent"); b.adjustRelation?.(a, 0.65, -0.20, "co_parent"); a.postBirthPeaceTimer = Math.max(a.postBirthPeaceTimer || 0, 28); b.postBirthPeaceTimer = Math.max(b.postBirthPeaceTimer || 0, 28); const childKey = this.tarinaiFamilyKey(child); const aKey = this.tarinaiFamilyKey(a); const bKey = this.tarinaiFamilyKey(b); a.children.push(childKey); b.children.push(childKey); this.recordFamily(a); this.recordFamily(b); this.recordFamily(child); if (this.family[aKey] && !this.family[aKey].children.includes(childKey)) this.family[aKey].children.push(childKey); if (this.family[bKey] && !this.family[bKey].children.includes(childKey)) this.family[bKey].children.push(childKey); this.familyCleanVersion = null; this.lastBirthAt = this.time; if (total <= 1) this.log(`${child.name}\u304c${a.name}\u3068${b.name}\u306e\u5b50\u3068\u3057\u3066\u751f\u307e\u308c\u305f\u3002`, "birth", { participants: [child, a, b] }); return child; }, fightPairKey(a, b) { const aid = a?.id || a?.familyKey || ""; const bid = b?.id || b?.familyKey || ""; if (!aid || !bid) return ""; return String(aid) < String(bid) ? `${aid}:${bid}` : `${bid}:${aid}`; }, fightPairCooldownRemaining(a, b) { const key = this.fightPairKey?.(a, b); if (!key) return 0; const until = Number(this.fightPairCooldowns?.[key] || 0) || 0; return Math.max(0, until - (this.time || 0)); }, markFightPairCooldown(a, b, seconds = 3.2) { const key = this.fightPairKey?.(a, b); if (!key) return; this.fightPairCooldowns = this.fightPairCooldowns || {}; this.fightPairCooldowns[key] = Math.max(Number(this.fightPairCooldowns[key] || 0) || 0, (this.time || 0) + Math.max(0.2, Number(seconds) || 3.2)); }, isAlreadyFightingPair(a, b) { if (!a || !b) return false; return a.fightTimer > 0.04 && b.fightTimer > 0.04 && ((a.fightTargetIds || []).includes(b.id) || a.fightTargetId === b.id) && ((b.fightTargetIds || []).includes(a.id) || b.fightTargetId === a.id); }, startForcedFight(a, b) { if (!a || !b || a === b || a.dead || b.dead) return false; if (!this.canFightPair(a, b)) return false; const alreadyFighting = this.isAlreadyFightingPair?.(a, b); if (alreadyFighting) return true; if ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04) return false; if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false; const maxForcedFightDistance = Math.max(142, (a.radius || 20) + (b.radius || 20) + 96); if (dist(a, b) > maxForcedFightDistance) return false; if ((a.fightCooldown || 0) > 0.04 || (b.fightCooldown || 0) > 0.04) return false; for (const t of [a, b]) { t.birthRitualTimer = 0; t.birthRitualMax = 0; t.birthPartnerId = null; t.birthRitualLeader = false; t.birthRitualRole = 0; t.postBirthPeaceTimer = 0; t.intimidateTimer = 0; t.intimidateTargetId = null; t.intimidatedTimer = 0; t.defeatedTimer = 0; t.defeatedById = null; t.fightWinnerId = null; t.goIdle(""); } this.startFightMochiIntimidation(a, b); this.startFight(a, b, { forced: true, initiator: a }); return true; }, startFightMochiIntimidation(a, b) { if (!isSocialTarinaiEntity(a) || !isSocialTarinaiEntity(b)) return false; const aDrug = (a.fightMochiTimer || 0) > 0.04; const bDrug = (b.fightMochiTimer || 0) > 0.04; const actor = aDrug && !bDrug ? a : (!aDrug && bDrug ? b : (a.energy + a.stress * 0.36 >= b.energy + b.stress * 0.36 ? a : b)); const target = actor === a ? b : a; if (target.isTarinaiChampion) return false; if ((actor.intimidationCooldown || 0) > 0.04 || (target.intimidationCooldown || 0) > 0.04) return false; if (!this.canFightPair(actor, target)) return false; actor.intimidationCooldown = Math.max(actor.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-actor", 5.5, 8.5, actor, target)); target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "fight-mochi-intimidate-repeat-cooldown-target", 4.5, 7.5, actor, target)); actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, deterministicRange(this, "fight-mochi-intimidate-actor-timer", 0.32, 0.58, actor, target)); actor.intimidateTargetId = target.id; actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "fight-mochi-intimidate-target-timer", 0.30, 0.56, actor, target)); const targetProfile = socialPersonalityProfile(target); target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * targetProfile.fear); actor.adjustRelation(target, -0.05, 0.06, "intimidate"); target.adjustRelation(actor, -0.12, 0.30 * targetProfile.fear, "intimidate"); this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, { life: 0.34, size: actor.radius * 0.62, color: "rgba(154, 88, 42, 0.74)", })); this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "!", "rgba(120,62,38,0.82)"); return true; }, startConflict(a, b) { if (!a || !b || a.dead || b.dead) return false; if (typeof a.relationTo !== "function" || typeof b.relationTo !== "function") return false; if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) return this.startForcedFight(a, b); if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return false; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false; if ((a.postConflictPeaceTimer || 0) > 0.04 || (b.postConflictPeaceTimer || 0) > 0.04) return false; if (this.fightPairCooldownRemaining?.(a, b) > 0.04) return false; if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return false; const aAggressiveIntent = a.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0; const bAggressiveIntent = b.shouldApplyPersonalityBehavior?.("aggression", 1) ? 1 : 0; const scoreA = a.energy * 0.38 + a.mood * 0.18 + a.stress * 0.12 + (a.type === "angry" ? 12 : 0) + aAggressiveIntent * 8 + ((typeof b.relationTo === "function" ? (b.relationTo(a.id).fear || 0) : 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + deterministicRange(this, "conflict-score-a", -5, 5, a, b); const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((typeof a.relationTo === "function" ? (a.relationTo(b.id).fear || 0) : 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + deterministicRange(this, "conflict-score-b", -5, 5, a, b); const actor = scoreA >= scoreB ? a : b; const target = actor === a ? b : a; if (!this.canFightPair(actor, target)) return false; const actorScore = actor === a ? scoreA : scoreB; const targetScore = actor === a ? scoreB : scoreA; const canIntimidate = !target.isTarinaiChampion && (actor.intimidationCooldown || 0) <= 0.04 && (target.intimidationCooldown || 0) <= 0.04 && (!actor.lowHealthSprite || !actor.lowHealthSprite()); const intimidationChance = 0.30; if (canIntimidate && deterministicChance(this, "conflict-intimidation-choice", intimidationChance, actor, target)) { return this.startIntimidation(actor, target, actorScore, targetScore); } return this.startFight(actor, target, { initiator: actor }); }, startIntimidation(actor, target, actorScore = 0, targetScore = 0) { if (!isSocialTarinaiEntity(actor) || !isSocialTarinaiEntity(target) || actor === target) return false; if (target.isTarinaiChampion) return false; if ((actor.fightTimer || 0) > 0.04 || (target.fightTimer || 0) > 0.04) return false; if ((actor.intimidateTimer || 0) > 0.04 || (target.intimidateTimer || 0) > 0.04) return false; if ((actor.intimidationCooldown || 0) > 0.04 || (target.intimidationCooldown || 0) > 0.04) return false; if (this.fightPairCooldownRemaining?.(actor, target) > 0.04) return false; if (actor.lowHealthSprite && actor.lowHealthSprite()) return false; const targetProfile = socialPersonalityProfile(target); const fearLoad = ((typeof target.relationTo === "function" ? (target.relationTo(actor.id).fear || 0) : 0) * 0.72) + targetProfile.fear * 8; const actorAggression = Math.max(0, actor.currentPersonality?.aggression || 0); const targetAggression = Math.max(0, target.currentPersonality?.aggression || 0); const actorSize = actor.effectiveScale ? actor.effectiveScale() : (actor.scale || 0.28); const targetSize = target.effectiveScale ? target.effectiveScale() : (target.scale || 0.28); const sizeEdge = clamp((actorSize - targetSize) / 0.16, -1, 1); const aggressionEdge = clamp(actorAggression - targetAggression * 0.55, -1, 1); const resistance = target.energy * 0.30 + target.mood * 0.16 + targetAggression * 8 + (target.shouldApplyPersonalityBehavior?.("aggression", 1) ? 5 : 0) + deterministicRange(this, "intimidation-resistance", -5, 5, actor, target); const successChance = clamp(0.56 + (actorScore - targetScore + fearLoad - resistance) / 82 + aggressionEdge * 0.13 + sizeEdge * 0.16, 0.30, 0.96); actor.surpriseTimer = Math.max(actor.surpriseTimer, 0.20); target.surpriseTimer = Math.max(target.surpriseTimer, 0.32); actor.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-actor-cooldown", 1, 4, actor, target); target.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "intimidation-target-cooldown", 1, 4, actor, target); actor.intimidationCooldown = Math.max(actor.intimidationCooldown || 0, deterministicRange(this, "intimidation-repeat-cooldown-actor", 7.5, 11.5, actor, target)); target.intimidationCooldown = Math.max(target.intimidationCooldown || 0, deterministicRange(this, "intimidation-repeat-cooldown-target", 5.5, 9.0, actor, target)); actor.intimidateTimer = Math.max(actor.intimidateTimer, deterministicRange(this, "intimidation-actor-timer", 0.34, 0.62, actor, target)); actor.intimidateTargetId = target.id; actor.setActionState("intimidate", { target, reason: "\u76f8\u624b\u3092\u5a01\u5687\u3057\u3066\u3044\u308b" }); setBehaviorText(actor, { need: "social", subNeed: "conflict", actionId: "intimidate_enemy", actionLabel: "\u5a01\u5687\u3057\u3066\u3044\u308b", reasonText: `${target.name || "\u76f8\u624b"}\u3092\u5a01\u5687\u3057\u3066\u3044\u308b`, target, phase: "perform", source: "behavior" }); this.effects.push(new Effect("ring", actor.x, actor.y - actor.radius * 0.65, { life: 0.42, size: actor.radius * 0.55, color: "rgba(145, 106, 55, 0.70)", })); this.spawnBubble(actor.x, actor.y - actor.radius * 1.30, "\u306f\u3046\u30fc\uff01", "rgba(92,62,34,0.82)"); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-target-prep-timer", 0.28, 0.52, actor, target)); target.fearTimer = Math.max(target.fearTimer, 0.65 * targetProfile.fear); if (!deterministicChance(this, "intimidation-success", successChance, actor, target)) { actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation"); target.adjustRelation(actor, -0.08, 0.22 * targetProfile.fear, "intimidate"); actor.intimidateTimer = 0; actor.intimidateTargetId = null; target.intimidatedTimer = 0; if (actor.state === "intimidate") actor.goIdle("\u5a01\u5687\u306b\u5931\u6557\u3057\u305f"); if (target.state === "panic") target.goIdle("\u5a01\u5687\u304b\u3089\u623b\u3063\u305f"); actor.spriteLockUntil = 0; target.spriteLockUntil = 0; this.startFight(actor, target, { initiator: actor }); return true; } actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation"); target.defeatedById = actor.id; target.fightWinnerId = actor.id; target.defeatedTimer = Math.max(target.defeatedTimer, deterministicRange(this, "intimidation-defeated-timer", 0.8, 1.35, actor, target)); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, deterministicRange(this, "intimidation-success-timer", 0.34, 0.70, actor, target)); if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", fear: 1.35, wake: true, cause: "intimidated" }); else { target.fearTimer = Math.max(target.fearTimer, 1.35 * targetProfile.fear); target.setActionState?.("panic", { target: actor, reason: "\u5a01\u5687\u3055\u308c\u3066\u9003\u3052\u3066\u3044\u308b", wake: true }); } this.spawnBubble(target.x, target.y - target.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); target.vx += (target.x < actor.x ? -1 : 1) * deterministicRange(this, "intimidation-knockback-x", 26, 54, actor, target); target.vy += deterministicRange(this, "intimidation-knockback-y", -18, 18, actor, target); actor.adjustRelation(target, -0.08, 0.06, "intimidate"); target.adjustRelation(actor, -0.22, 0.85 * targetProfile.fear, "intimidate"); if (this.time - Math.max(actor.lastLog, target.lastLog) > 5.5) { this.log(`${actor.name}\u306f${target.name}\u3092\u5a01\u5687\u3057\u3066\u8ffd\u3044\u6255\u3063\u305f\u3002`, "fight", { participants: [actor, target] }); actor.lastLog = target.lastLog = this.time; } this.markFightPairCooldown?.(actor, target, Math.max(6.5, CONFIG.fightCooldown + 3)); actor.intimidateTimer = Math.min(actor.intimidateTimer || 0, 0.62); target.intimidatedTimer = Math.min(target.intimidatedTimer || 0, 0.70); actor.postConflictPeaceTimer = Math.max(actor.postConflictPeaceTimer || 0, deterministicRange(this, "intimidation-success-peace-actor", 2.4, 3.8, actor, target)); target.postConflictPeaceTimer = Math.max(target.postConflictPeaceTimer || 0, deterministicRange(this, "intimidation-success-peace-target", 2.4, 3.8, actor, target)); return true; }, startFight(a, b, opts = {}) { if (!a || !b || a.dead || b.dead) return false; if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return false; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return false; if ((a.postConflictPeaceTimer || 0) > 0.04 || (b.postConflictPeaceTimer || 0) > 0.04) return false; if (this.isAlreadyFightingPair?.(a, b)) return true; const allowForcedStart = Boolean(opts?.forced); if (!allowForcedStart && ((a.fightTimer || 0) > 0.04 || (b.fightTimer || 0) > 0.04 || (a.intimidateTimer || 0) > 0.04 || (b.intimidateTimer || 0) > 0.04)) return false; if (!allowForcedStart && this.fightPairCooldownRemaining?.(a, b) > 0.04) return false; const maxStartFightDistance = Math.max(150, (a.radius || 20) + (b.radius || 20) + 104); if (dist(a, b) > maxStartFightDistance) return false; this.markFightPairCooldown?.(a, b, CONFIG.fightCooldown + 6); const initiator = opts?.initiator === b ? b : a; const receiver = initiator === a ? b : a; const causeFor = (self, other) => { if (((currentTarinaiBehavior(self))?.source === "fight_mochi") || (self.fightMochiTimer || 0) > 0.04) return "\u3051\u3093\u304b\u9905\u306e\u52b9\u679c"; if (self === receiver && other === initiator) return `${other.name || "\u76f8\u624b"}\u306b\u3051\u3093\u304b\u3092\u58f2\u3089\u308c\u305f`; return `${other.name || "\u76f8\u624b"}\u304c\u6c17\u306b\u5165\u3089\u306a\u3044`; }; const textFor = (self, other) => buildReasonText("social", getTarinaiBehaviorTiedNeeds(self, "social") || ["social"], { id: "fight_rival", need: "social", subNeed: "conflict", label: self === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : `${other.name || "\u76f8\u624b"}\u3068\u55a7\u5629\u3057\u3066\u3044\u308b` }, self, this, { causeText: causeFor(self, other) }); const reasonA = textFor(a, b); const reasonB = textFor(b, a); a.counterAttackFromId = a === receiver ? initiator.id : null; b.counterAttackFromId = b === receiver ? initiator.id : null; a.setActionState("fight", { target: b, reason: reasonA }); b.setActionState("fight", { target: a, reason: reasonB }); setBehaviorText(a, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: a === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonA, causeText: causeFor(a, b), target: b, phase: "perform", source: "behavior" }); setBehaviorText(b, { need: "social", subNeed: "conflict", actionId: "fight_rival", actionLabel: b === receiver ? "\u53cd\u6483\u3057\u3066\u3044\u308b" : "\u55a7\u5629\u3057\u3066\u3044\u308b", reasonText: reasonB, causeText: causeFor(b, a), target: a, phase: "perform", source: "behavior" }); a.fightTimer = Math.max(a.fightTimer, deterministicRange(this, "fight-start-timer-a", 3.2, 4.8, a, b)); b.fightTimer = Math.max(b.fightTimer, deterministicRange(this, "fight-start-timer-b", 3.2, 4.8, a, b)); a.nextHeadbutt = this.time + deterministicRange(this, "fight-start-headbutt", 0.08, 0.18, a, b); b.nextHeadbutt = a.nextHeadbutt; a.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-a", 1, 5, a, b); b.fightCooldown = CONFIG.fightCooldown + deterministicRange(this, "fight-start-cooldown-b", 1, 5, a, b); if (!a.fightTargetIds) a.fightTargetIds = []; if (!b.fightTargetIds) b.fightTargetIds = []; if (!a.fightTargetIds.includes(b.id)) a.fightTargetIds.push(b.id); if (!b.fightTargetIds.includes(a.id)) b.fightTargetIds.push(a.id); a.fightTargetIds = a.fightTargetIds.slice(-4); b.fightTargetIds = b.fightTargetIds.slice(-4); a.fightTargetId = a.fightTargetIds[0]; b.fightTargetId = b.fightTargetIds[0]; if (a.addStress) a.addStress(deterministicRange(this, "fight-start-stress-a", 8, 18, a, b), { threshold: 8 }); if (b.addStress) b.addStress(deterministicRange(this, "fight-start-stress-b", 8, 18, a, b), { threshold: 8 }); // Damage is applied by repeated headbutt pulses while the fight is active. // Do not front-load or end-load a lump sum when the fight state starts/settles. const dx = b.x - a.x, dy = b.y - a.y; const d = Math.hypot(dx, dy) || 1; a.vx -= dx / d * deterministicRange(this, "fight-pulse-knockback-ax", 32, 62, a, b); a.vy -= dy / d * deterministicRange(this, "fight-pulse-knockback-ay", 32, 62, a, b); b.vx += dx / d * deterministicRange(this, "fight-pulse-knockback-bx", 32, 62, a, b); b.vy += dy / d * deterministicRange(this, "fight-pulse-knockback-by", 32, 62, a, b); a.surpriseTimer = 0.24; b.surpriseTimer = 0.24; this.effects.push(new Effect("fight", (a.x + b.x) / 2, (a.y + b.y) / 2 - 8, { size: deterministicRange(this, "fight-pulse-effect-size", 12, 20, a, b), life: deterministicRange(this, "fight-pulse-effect-life", 0.32, 0.52, a, b), color: "rgba(116, 73, 38, 0.82)", })); audio.fight(); if (this.time - Math.max(a.lastLog, b.lastLog) > 6) { this.log(`${a.name}\u3068${b.name}\u304c\u5c0f\u3055\u306a\u55a7\u5629\u3092\u3057\u305f\u3002`, "fight", { participants: [a, b], sound: false }); a.lastLog = b.lastLog = this.time; } return true; } })); })(typeof window !== "undefined" ? window : globalThis);