"use strict"; (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_family_social.js"); Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ reset(seedPopulation = null, fieldType = this.fieldType || "garden") { const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden; 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.tarinai = []; this.items = []; this.ants = []; this.effects = []; this.logs = []; this.events = window.TarinaiEvents || null; this.eventCounters = {}; this.countsTimer = 0; this.compactTimer = 0; this.drawSortTimer = 0; this.drawListDirty = true; this.antUpdatePhase = 0; this.selected = null; this.deadCount = 0; this.liveIdNext = 1; this.liveIdFree = []; this.liveIdSerial = 0; this.liveTarinai = new Map(); this.lastBirthAt = -999; this.maxGeneration = 1; this.family = {}; this.familyVersion = 0; this.relationNotices = {}; this.resolvedFightIds = {}; this.foodSpoilagePenalty = 0; this.foodSpoilageEvents = 0; this.pointer = { x: this.w / 2, y: this.h / 2, inside: false, motion: 0, movedAt: 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.nextWeatherChange = rand(CONFIG.weatherChangeMin, CONFIG.weatherChangeMax); seedPopulation = seedPopulation ?? field.population ?? CONFIG.initialPopulation; const grassCount = field.grass ?? 18; const types = spawnableSprites().map(s => s.id); for (let i = 0; i < seedPopulation; i++) { this.addTarinai({ type: types[i % types.length], x: rand(80, this.w - 80), y: rand(80, this.h - 80), generation: 1, }); } for (let i = 0; i < grassCount; i++) this.items.push(new Item("grass", rand(70, this.w - 70), rand(70, this.h - 70))); this.items.push(new Item("stone", this.w * 0.68, this.h * 0.32)); this.items.push(new Item("bed", this.w * 0.82, this.h * 0.22)); this.items.push(new Item("bed", this.w * 0.18, this.h * 0.78)); this.updateItemCounts(); this.updateEffectCounts(); this.markTerrainDirty?.("reset"); this.rebuildSpatial(true); showToast("\u65b0\u3057\u3044\u89b3\u5bdf\u3092\u958b\u59cb\u3057\u307e\u3057\u305f\u3002"); syncTopButtons(); }, tarinaiFamilyKey(t) { return t?.familyKey || t?.archiveKey || 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. this.liveIdFree = []; 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; 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, 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; 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.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; this.familyTreeDirtyReason = reason; this.familyTreeDirtyAt = this.time || 0; }, 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), 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]; const 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; } }, validateFamily() { this.normalizeFamily(); const family = this.family || {}; const issues = []; const seenRenderIds = new Set(); for (const n of Object.values(family)) { if (!n?.id) continue; const childSeen = new Set(); for (const p of n.parents || []) { if (!family[p]) issues.push({ type: "missing-parent", id: n.id, parent: p }); else if (!family[p].children?.includes(n.id)) issues.push({ type: "missing-reciprocal-child", id: n.id, parent: p }); } for (const c of n.children || []) { if (childSeen.has(c)) issues.push({ type: "duplicate-child", id: n.id, child: c }); childSeen.add(c); if (!family[c]) issues.push({ type: "missing-child", id: n.id, child: c }); else if (!family[c].parents?.includes(n.id)) issues.push({ type: "missing-reciprocal-parent", id: n.id, child: c }); } if (seenRenderIds.has(n.id)) issues.push({ type: "duplicate-render-node", id: n.id }); seenRenderIds.add(n.id); } return issues; }, 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?.("参照していた相手がいなくなった"); 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.notifyDeathToRelations?.(t, finalReason); 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 = {}) { const t = new Tarinai(this, opts); 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); this.emit?.("tarinai:born", { tarinai: t, source: opts.parents?.length ? "family" : "spawn" }); return t; }, startBirthRitual(a, b) { if (!a || !b || a.dead || b.dead) 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; a.setActionState("birth_ritual", { target: b, reason: "近くで体をゆすっている" }); b.setActionState("birth_ritual", { target: a, reason: "近くで体をゆすっている" }); a.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10); b.reproductionTimer = CONFIG.reproductionCooldown + rand(2, 10); a.surpriseTimer = Math.max(a.surpriseTimer, 0.35); b.surpriseTimer = Math.max(b.surpriseTimer, 0.35); return true; }, 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("誕生の前ぶれが終わった"); b.goIdle("誕生の前ぶれが終わった"); 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) 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 && !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 inherited = Math.random() < 0.56 ? a.type : b.type; const mutation = zunchiSlaveChild ? "zunchi_slave" : (Math.random() < 0.18 ? baseSpriteId() : inherited); const childSeed = crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}-${index}`; const childGenetics = inheritedGenetics(a, b, childSeed); const childAdultScale = adultScaleFromGenetics(childSeed, childGenetics); const childBirthPersonality = zunchiSlaveChild ? zunchiSlavePersonalityValue() : personalityFromParents(a, b); const child = this.addTarinai({ familyKey: childSeed, type: mutation, birthPersonality: childBirthPersonality, currentPersonality: childBirthPersonality, genetics: childGenetics, adultScale: childAdultScale, name: zunchiSlaveChild ? "\u305a\u3093\u3061\u3069\u308c\u3044" : makeName(), 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), scale: scaleForGrowth(childAdultScale, 0), hunger: rand(20, 40), loneliness: rand(30, 56), energy: rand(55, 95), stress: rand(0, 14), need: Math.random() < 0.6 ? pick([a.need, b.need]) : pick(NEEDS), generation: Math.max(a.generation, b.generation) + 1, lifeSpan: rand(1500, 2200) * (childGenetics.lifeSpanMul || 1), parents: [this.tarinaiFamilyKey(a), this.tarinaiFamilyKey(b)], parentNames: [a.name, b.name], birthTime: this.time, powerItemMode: "", sizeItemMode: "", lifeItemMode: "", }); 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; audio.birth(); 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; }, startForcedFight(a, b) { if (!a || !b || a === b || a.dead || b.dead) return false; if (!this.canFightPair(a, b)) return false; const alreadyFighting = a.fightTimer > 0.04 && b.fightTimer > 0.04 && ((a.fightTargetIds || []).includes(b.id) || a.fightTargetId === b.id); if (alreadyFighting) return true; 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.fightCooldown = 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); return true; }, startFightMochiIntimidation(a, b) { if (!a || !b || a.dead || b.dead) 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 (!this.canFightPair(actor, target)) return false; actor.intimidateTimer = Math.max(actor.intimidateTimer || 0, rand(0.85, 1.35)); actor.intimidateTargetId = target.id; actor.surpriseTimer = Math.max(actor.surpriseTimer || 0, 0.28); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(0.75, 1.25)); target.fearTimer = Math.max(target.fearTimer || 0, 0.72 * target.personalityProfile().fear); actor.adjustRelation(target, -0.05, 0.06, "intimidate"); target.adjustRelation(actor, -0.12, 0.30 * target.personalityProfile().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; if ((a.fightMochiTimer || 0) > 0.04 || (b.fightMochiTimer || 0) > 0.04) { this.startForcedFight(a, b); return; } if (this.areParentChild(a, b) || this.areCoParents?.(a, b)) return; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return; if (a.fightTimer > 0.04 || b.fightTimer > 0.04 || a.intimidateTimer > 0.04 || b.intimidateTimer > 0.04) return; 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 + ((b.relationTo(a.id).fear || 0) * 0.65) + (a.isZunchiSlave ? -10 : 0) + ((a.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5); const scoreB = b.energy * 0.38 + b.mood * 0.18 + b.stress * 0.12 + (b.type === "angry" ? 12 : 0) + bAggressiveIntent * 8 + ((a.relationTo(b.id).fear || 0) * 0.65) + (b.isZunchiSlave ? -10 : 0) + ((b.fightMochiTimer || 0) > 0.04 ? 9 : 0) + rand(-5, 5); const actor = scoreA >= scoreB ? a : b; const target = actor === a ? b : a; if (!this.canFightPair(actor, target)) return; const actorScore = actor === a ? scoreA : scoreB; const targetScore = actor === a ? scoreB : scoreA; const canIntimidate = !actor.lowHealthSprite || !actor.lowHealthSprite(); const battleDrug = (actor.fightMochiTimer || 0) > 0.04 || (target.fightMochiTimer || 0) > 0.04; const intimidationChance = clamp(0.40 + (battleDrug ? 0.24 : 0) + (target.isZunchiSlave ? 0.18 : 0), 0.24, 0.90); if (canIntimidate && Math.random() < intimidationChance) { this.startIntimidation(actor, target, actorScore, targetScore); return; } this.startFight(a, b); }, startIntimidation(actor, target, actorScore = 0, targetScore = 0) { if (!actor || !target || actor.dead || target.dead) return false; if (actor.lowHealthSprite && actor.lowHealthSprite()) return false; const fearLoad = (target.relationTo(actor.id).fear || 0) * 0.72 + target.personalityProfile().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) + rand(-5, 5); 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 + rand(1, 4); target.fightCooldown = CONFIG.fightCooldown + rand(1, 4); actor.intimidateTimer = Math.max(actor.intimidateTimer, rand(2.45, 3.35)); actor.intimidateTargetId = target.id; actor.setActionState("intimidate", { target, reason: "相手を威嚇している" }); 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, rand(1.65, 2.45)); target.fearTimer = Math.max(target.fearTimer, 0.65 * target.personalityProfile().fear); if (Math.random() >= successChance) { actor.adjustPersonality?.("aggression", -0.018, "after failed intimidation"); target.adjustRelation(actor, -0.08, 0.22 * target.personalityProfile().fear, "intimidate"); actor.intimidateTimer = 0; actor.intimidateTargetId = null; target.intimidatedTimer = 0; if (actor.state === "intimidate") actor.goIdle("威嚇に失敗した"); if (target.state === "panic") target.goIdle("威嚇から戻った"); actor.spriteLockUntil = 0; target.spriteLockUntil = 0; this.startFight(actor, target); return true; } actor.adjustPersonality?.("aggression", 0.018, "after successful intimidation"); target.defeatedById = actor.id; target.fightWinnerId = actor.id; target.defeatedTimer = Math.max(target.defeatedTimer, rand(2.8, 4.2)); target.intimidatedTimer = Math.max(target.intimidatedTimer || 0, rand(2.4, 3.4)); if (target.enterPanic) target.enterPanic({ threat: actor, target: actor, reason: "威嚇されて逃げている", fear: 1.35, wake: true, cause: "intimidated" }); else { target.fearTimer = Math.max(target.fearTimer, 1.35 * target.personalityProfile().fear); target.setActionState?.("panic", { target: actor, reason: "威嚇されて逃げている", 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) * rand(26, 54); target.vy += rand(-18, 18); actor.adjustRelation(target, -0.08, 0.06, "intimidate"); target.adjustRelation(actor, -0.22, 0.85 * target.personalityProfile().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; } return true; }, startFight(a, b) { if (!a || !b || a.dead || b.dead) return; if (!this.canFightPair(a, b) && !this.canFightPair(b, a)) return; if ((a.postBirthPeaceTimer || 0) > 0 || (b.postBirthPeaceTimer || 0) > 0) return; a.setActionState("fight", { target: b, reason: `${b.name || "相手"}と喧嘩している` }); b.setActionState("fight", { target: a, reason: `${a.name || "相手"}と喧嘩している` }); a.fightTimer = Math.max(a.fightTimer, rand(4.5, 7.0)); b.fightTimer = Math.max(b.fightTimer, rand(4.5, 7.0)); a.nextHeadbutt = this.time + rand(0.08, 0.18); b.nextHeadbutt = a.nextHeadbutt; a.fightCooldown = CONFIG.fightCooldown + rand(1, 5); b.fightCooldown = CONFIG.fightCooldown + rand(1, 5); 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(rand(8, 18), { threshold: 8 }); if (b.addStress) b.addStress(rand(8, 18), { threshold: 8 }); const damageToA = b.outgoingDamage ? b.outgoingDamage(rand(2, 7)) : rand(2, 7); const damageToB = a.outgoingDamage ? a.outgoingDamage(rand(2, 7)) : rand(2, 7); a.damage(damageToA, "\u55a7\u5629"); b.damage(damageToB, "\u55a7\u5629"); this.maybeDefeatFromFightDamage?.(a, b, damageToA); this.maybeDefeatFromFightDamage?.(b, a, damageToB); const dx = b.x - a.x, dy = b.y - a.y; const d = Math.hypot(dx, dy) || 1; a.vx -= dx / d * rand(32, 62); a.vy -= dy / d * rand(32, 62); b.vx += dx / d * rand(32, 62); b.vy += dy / d * rand(32, 62); 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: rand(12, 20), life: rand(0.32, 0.52), 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] }); a.lastLog = b.lastLog = this.time; } } })); })(typeof window !== "undefined" ? window : globalThis);