"use strict"; (function (global) { const BUILD_TEXT = "親しいたりないの墓を作っている。"; const MOURN_TEXT = "親しいたりないの墓が壊されて泣いている。"; const BELL_TEXT = "呼び鈴の鳴った場所に集まっている。"; const GRAVE_VISIT_APPROACH_TEXT = "親しいたりないの墓参りに向かっている。"; const GRAVE_VISIT_TEXT = "親しいたりないの墓参りをしている。"; const BELL_RANGES = Object.freeze({ small: 120, medium: 210, large: 350 }); const BELL_DURATIONS = Object.freeze({ small: 12, medium: 18, large: 26 }); function familyKeyOf(t) { return String(t?.familyKey || t?.id || ""); } function ensureRequests(worldRef) { if (!worldRef) return []; if (!Array.isArray(worldRef.memorialRequests)) worldRef.memorialRequests = []; return worldRef.memorialRequests; } function requestById(worldRef, id) { return ensureRequests(worldRef).find(req => req && req.id === id) || null; } function liveByFamilyKey(worldRef, key) { if (!worldRef || !key) return null; return (worldRef.tarinai || []).find(t => t && !t.dead && familyKeyOf(t) === key) || null; } function activeGraveFor(worldRef, deceasedFamilyKey) { if (!worldRef || !deceasedFamilyKey) return null; return (worldRef.items || []).find(item => item && !item.dead && item.type === "grave" && item.memorialFamilyKey === deceasedFamilyKey) || null; } function relatedGravesFor(worldRef, tarinai) { if (!worldRef || !tarinai) return []; const key = familyKeyOf(tarinai); if (!key) return []; return (worldRef.items || []).filter(item => item && !item.dead && item.type === "grave" && Array.isArray(item.memorialEligibleFamilyKeys) && item.memorialEligibleFamilyKeys.includes(key)); } function graveById(worldRef, id) { if (!worldRef || !id) return null; const item = worldRef.itemById?.(id) || (worldRef.items || []).find(entry => entry?.id === id) || null; return item && !item.dead && item.type === "grave" ? item : null; } function calmEnoughForGraveVisit(t) { if (!t || t.dead || isCritical(t)) return false; if (!["idle", "wander"].includes(String(t.state || "idle"))) return false; if ((t.hunger || 0) >= 62 || (t.energy || 100) <= 38 || (t.stress || 0) >= 72) return false; const needValues = ["food", "sleep", "health", "safety", "social", "fulfill"] .map(key => Number(t.needRaw?.[key] ?? t.needs?.[key] ?? 0) || 0); return Math.max(0, ...needValues) < 58; } function clearGraveVisit(t, reason = "") { if (!t) return; const wasVisiting = t.state === "visit_grave"; t.graveVisitTargetId = ""; t.graveVisitUntil = 0; t.graveVisitPhase = ""; t.graveVisitReliefApplied = false; t.nextGraveVisitCheckAt = Math.max(Number(t.nextGraveVisitCheckAt || 0), Number(t.world?.time || 0) + 28); if (wasVisiting) { t.target = null; t.goIdle?.(reason || "墓参りを終えた。"); } } function graveVisitHasUrgentNeed(t) { if (!t) return true; if ((t.hunger || 0) >= 78 || (t.energy || 100) <= 24 || (t.stress || 0) >= 84) return true; const needValues = ["food", "sleep", "health", "safety"] .map(key => Number(t.needRaw?.[key] ?? t.needs?.[key] ?? 0) || 0); return Math.max(0, ...needValues) >= 76; } function maintainGraveVisit(t) { if (!t || !t.graveVisitTargetId) return false; if (t.state !== "visit_grave") { clearGraveVisit(t); return false; } const worldRef = t.world; const grave = graveById(worldRef, t.graveVisitTargetId); if (!grave || isCritical(t) || graveVisitHasUrgentNeed(t)) { clearGraveVisit(t, grave ? "別の用事を優先した。" : "墓がなくなった。"); return false; } const now = Number(worldRef?.time || 0) || 0; if (t.graveVisitPhase === "visiting") { if (now >= Number(t.graveVisitUntil || 0)) { clearGraveVisit(t, "墓参りを終えた。"); return false; } const mournText = `${grave.memorialName || "親しいたりない"}を悼んでいる。`; setSpecialBehavior(t, "visit_grave", mournText, grave); return true; } const approachText = `${grave.memorialName || "親しいたりない"}の墓参りに向かっている。`; setSpecialBehavior(t, "visit_grave", approachText, grave); return true; } function maybeStartGraveVisit(t) { const worldRef = t?.world; if (!worldRef || !calmEnoughForGraveVisit(t)) return false; const now = Number(worldRef.time || 0) || 0; if (now < Number(t.nextGraveVisitCheckAt || 0)) return false; t.nextGraveVisitCheckAt = now + 10; const chance = 0.045; // checked at most once per 10 seconds while otherwise idle const hit = typeof global.deterministicChance === "function" ? global.deterministicChance(worldRef, "idle-grave-visit", chance, t, Math.floor(now / 10)) : Math.random() < chance; if (!hit) return false; const graves = relatedGravesFor(worldRef, t); if (!graves.length) return false; graves.sort((a, b) => Math.hypot(a.x - t.x, a.y - t.y) - Math.hypot(b.x - t.x, b.y - t.y)); const shortlist = graves.slice(0, Math.min(3, graves.length)); const unit = typeof global.stableUnit === "function" ? global.stableUnit(familyKeyOf(t), `grave-visit-${Math.floor(now / 10)}`) : Math.random(); const grave = shortlist[Math.min(shortlist.length - 1, Math.floor(unit * shortlist.length))] || shortlist[0]; if (!grave) return false; t.graveVisitTargetId = grave.id || ""; t.graveVisitPhase = "approach"; t.graveVisitUntil = 0; t.graveVisitReliefApplied = false; setSpecialBehavior(t, "visit_grave", `${grave.memorialName || "親しいたりない"}の墓参りに向かっている。`, grave); return true; } function relationAffinity(a, b) { if (!a || !b) return -Infinity; const aToB = Number(a.relationships?.[b.id]?.affinity); const bToA = Number(b.relationships?.[a.id]?.affinity); return Math.max(Number.isFinite(aToB) ? aToB : -Infinity, Number.isFinite(bToA) ? bToA : -Infinity); } function eligibleFamilyKeysAtDeath(worldRef, dead) { const related = new Set([...(dead?.parents || []), ...(dead?.children || [])].filter(Boolean)); const threshold = typeof FRIEND_AFFINITY_THRESHOLD === "number" ? FRIEND_AFFINITY_THRESHOLD : 14; for (const other of worldRef?.tarinai || []) { if (!other || other.dead || other === dead) continue; const key = familyKeyOf(other); if (!key) continue; if (related.has(key) || relationAffinity(dead, other) >= threshold) related.add(key); } related.delete(familyKeyOf(dead)); return Array.from(related).filter(key => liveByFamilyKey(worldRef, key)); } const MEMORIAL_BUILD_CHANCE = 0.35; const GRAVE_BREAK_FORCE = 720; function createGraveFromRequest(worldRef, req, builderFamilyKey = "", reason = "memorial-built") { if (!worldRef || !req || activeGraveFor(worldRef, req.deceasedFamilyKey)) return activeGraveFor(worldRef, req.deceasedFamilyKey); const grave = global.StructureRegistry?.create?.("grave", null, req.x, req.y, worldRef) || null; if (!grave) return null; grave.memorialFamilyKey = req.deceasedFamilyKey; grave.memorialName = req.deceasedName; grave.memorialType = req.deceasedType; grave.memorialDeathReason = req.deathReason; grave.memorialDeathTime = req.deathTime; grave.memorialBuilderFamilyKey = String(builderFamilyKey || ""); grave.memorialEligibleFamilyKeys = Array.from(new Set(req.eligibleFamilyKeys || [])); grave.memorialFavorite = Boolean(req.deceasedFavorite); grave.memorialTarinaiKing = Boolean(req.deceasedTarinaiKing); grave.memorialZunchiSlave = Boolean(req.deceasedZunchiSlave); grave.createdAt = Number(worldRef.time || 0) || 0; grave._naturalDecayLastWorldTime = grave.createdAt; const added = worldRef.addItem?.(grave, reason) || null; if (!added) return null; worldRef.markItemBucketsDirty?.(reason); worldRef.markSpatialDirty?.(reason); worldRef.markTerrainDirtyAt?.(grave.x, grave.y, 52, reason); worldRef.drawListDirty = true; return grave; } function captureDeath(worldRef, dead, reason = "") { if (!worldRef || !dead) return null; const deceasedFamilyKey = familyKeyOf(dead); if (!deceasedFamilyKey) return null; const requests = ensureRequests(worldRef); const existing = requests.find(req => req?.deceasedFamilyKey === deceasedFamilyKey && req.status !== "cancelled"); if (existing) return existing; const eligibleFamilyKeys = eligibleFamilyKeysAtDeath(worldRef, dead); const deceasedFavorite = Boolean(dead.favorite); if (!deceasedFavorite && !eligibleFamilyKeys.length) return null; if (!deceasedFavorite) { const chanceHit = typeof global.deterministicChance === "function" ? global.deterministicChance(worldRef, "memorial-build-at-death", MEMORIAL_BUILD_CHANCE, dead, deceasedFamilyKey) : Math.random() < MEMORIAL_BUILD_CHANCE; if (!chanceHit) return null; } const req = { id: `memorial-${deceasedFamilyKey}-${Math.round(Number(worldRef.time || 0) * 10)}`, deceasedFamilyKey, deceasedName: String(dead.name || "たりない"), deceasedType: String(dead.type || "smile"), deathReason: String(reason || dead.deathReason || ""), deathTime: dead.deathTime != null && Number.isFinite(Number(dead.deathTime)) ? Number(dead.deathTime) : Number(worldRef.time || 0), deceasedFavorite, deceasedTarinaiKing: Boolean(dead.isTarinaiChampion), deceasedZunchiSlave: Boolean(dead.isZunchiSlave), x: Number(dead.x || 0) || 0, y: Number(dead.y || 0) || 0, eligibleFamilyKeys, builderFamilyKey: "", splatId: "", status: deceasedFavorite ? "building_direct" : "pending", createdAt: Number(worldRef.time || 0) || 0, }; requests.push(req); if (deceasedFavorite) { const grave = createGraveFromRequest(worldRef, req, "", "favorite-memorial-direct"); if (grave) { req.status = "complete"; worldRef.spawnEffect?.("ring", grave.x, grave.y, { size: 44, life: 0.48, color: "rgba(171,154,118,0.44)" }); } else req.status = "cancelled"; } return req; } function liveSplatForRequest(worldRef, req) { if (!worldRef || !req) return null; let best = null; let bestD = Infinity; for (const item of worldRef.residuesOfType?.("splat") || []) { if (!item || item.dead || Number(item.amount || 0) <= 0) continue; if (item._memorialReservedBy && item._memorialReservedBy !== req.id) continue; const d = Math.hypot((Number(item.x || 0) || 0) - req.x, (Number(item.y || 0) || 0) - req.y); if (d <= 190 && d < bestD) { best = item; bestD = d; } } return best; } function isCritical(t) { if (!t || t.dead) return true; if (t.burning || (t.burnTimer || 0) > 0.02 || t.sleepDisease || t.fightDisease) return true; if ((t.fightTimer || 0) > 0.04 || (t.birthRitualTimer || 0) > 0.04 || (t.intimidateTimer || 0) > 0.04 || (t.eatTimer || 0) > 0.04) return true; return ["panic", "fight", "birth_ritual", "intimidate", "ant_attack", "frozen"].includes(String(t.state || "")); } function setSpecialBehavior(t, state, text, target = null) { t.setActionState?.(state, { target, reason: text, wake: true, sleeping: false }); t.thought = text; t.target = target; if (typeof global.setBehaviorText === "function") { global.setBehaviorText(t, { need: (state === "mourn_grave" || state === "visit_grave") ? "social" : "fulfill", actionId: state, actionLabel: text, reasonText: text, target, phase: "perform", source: "behavior", }); } } function clearBuilderAssignment(t, req = null) { if (req && req.builderFamilyKey === familyKeyOf(t)) req.builderFamilyKey = ""; const worldRef = t?.world; const splat = req?.splatId ? worldRef?.residueByRuntimeId?.(req.splatId) : null; if (splat && splat._memorialReservedBy === req?.id) splat._memorialReservedBy = ""; if (t) { t.memorialRequestId = ""; t.memorialBuildProgress = 0; if (t.state === "build_memorial") t.goIdle?.("墓作りをやめた"); } } function assignMemorial(t) { const worldRef = t?.world; if (!worldRef || !t || isCritical(t)) return false; const key = familyKeyOf(t); if (!key) return false; for (const req of ensureRequests(worldRef)) { if (!req || req.status !== "pending" || !req.eligibleFamilyKeys?.includes?.(key)) continue; if (activeGraveFor(worldRef, req.deceasedFamilyKey)) { req.status = "complete"; continue; } if (req.builderFamilyKey) { const builder = liveByFamilyKey(worldRef, req.builderFamilyKey); if (builder && builder.memorialRequestId === req.id) continue; req.builderFamilyKey = ""; } const splat = liveSplatForRequest(worldRef, req); if (!splat) continue; req.builderFamilyKey = key; req.splatId = splat.id || ""; splat._memorialReservedBy = req.id; t.memorialRequestId = req.id; t.memorialBuildProgress = 0; setSpecialBehavior(t, "build_memorial", BUILD_TEXT, splat); return true; } return false; } function completeMemorial(t, req, splat) { const worldRef = t?.world; if (!worldRef || !req || !splat) return false; const grave = createGraveFromRequest(worldRef, req, familyKeyOf(t), "memorial-built"); if (!grave) return false; splat.amount = 0; splat.hp = 0; splat._memorialReservedBy = ""; worldRef.removeResidue?.(splat, "memorial-material-consumed"); req.status = "complete"; req.builderFamilyKey = ""; req.splatId = ""; t.memorialRequestId = ""; t.memorialBuildProgress = 0; t.target = null; if (typeof applyNeedRelief === "function") applyNeedRelief(t, { social: -48, fulfill: -8 }); t.goIdle?.("親しいたりないの墓を作った。"); worldRef.spawnEffect?.("ring", grave.x, grave.y, { size: 38, life: 0.42, color: "rgba(128,120,112,0.42)" }); return true; } function updateMemorialMotion(t, dt) { const worldRef = t?.world; const req = requestById(worldRef, t?.memorialRequestId); if (!req || req.status !== "pending" || req.builderFamilyKey !== familyKeyOf(t)) { if (t?.memorialRequestId) clearBuilderAssignment(t, req); return false; } if (isCritical(t) && t.state !== "build_memorial") return false; let splat = req.splatId ? worldRef.residueByRuntimeId?.(req.splatId) : null; if (!splat || splat.dead || splat.type !== "splat" || Number(splat.amount || 0) <= 0) { splat = liveSplatForRequest(worldRef, req); if (!splat) { clearBuilderAssignment(t, req); return false; } req.splatId = splat.id || ""; splat._memorialReservedBy = req.id; } setSpecialBehavior(t, "build_memorial", BUILD_TEXT, splat); const d = Math.hypot((splat.x || 0) - t.x, (splat.y || 0) - t.y); const reach = Math.max(28, (t.radius || 22) + (splat.r || 20) * 0.55); if (d > reach) return false; t.vx *= Math.pow(0.18, Math.max(0.016, dt) * 60); t.vy *= Math.pow(0.18, Math.max(0.016, dt) * 60); t.memorialBuildProgress = Math.max(0, Number(t.memorialBuildProgress || 0)) + dt; if (t.memorialBuildProgress >= 9.0) completeMemorial(t, req, splat); return true; } function maintainMourning(t, dt = 0) { const now = Number(t?.world?.time || 0) || 0; if (!t || !Number.isFinite(Number(t.graveMourningUntil)) || now >= Number(t.graveMourningUntil)) { if (t?.state === "mourn_grave") t.goIdle?.("泣き止んだ"); return false; } setSpecialBehavior(t, "mourn_grave", MOURN_TEXT, null); if (dt > 0) { t.vx *= Math.pow(0.15, dt * 60); t.vy *= Math.pow(0.15, dt * 60); } return true; } function maintainBellCall(t) { const now = Number(t?.world?.time || 0) || 0; if (!t || !t.bellCallTarget || now >= Number(t.bellCallUntil || 0)) { if (t?.state === "bell_call") t.goIdle?.("呼び鈴の音が止んだ"); t && (t.bellCallTarget = null); return false; } if (isCritical(t) && t.state !== "bell_call") return false; const target = t.bellCallTarget; const d = Math.hypot(target.x - t.x, target.y - t.y); if (d <= Math.max(32, (t.radius || 22) * 1.15)) { t.bellCallTarget = null; t.target = null; t.goIdle?.("呼び鈴の鳴った場所に集まった。"); return false; } setSpecialBehavior(t, "bell_call", BELL_TEXT, target); return true; } function updateAi(t) { if (!t || t.dead) return false; if (maintainMourning(t, 0)) return true; if (t.memorialRequestId) { const req = requestById(t.world, t.memorialRequestId); if (req?.status === "pending") { setSpecialBehavior(t, "build_memorial", BUILD_TEXT, req.splatId ? t.world?.residueByRuntimeId?.(req.splatId) : t.target); return true; } clearBuilderAssignment(t, req); } if (maintainBellCall(t)) return true; if (assignMemorial(t)) return true; if (maintainGraveVisit(t)) return true; return maybeStartGraveVisit(t); } function updateMotion(t, dt) { if (!t || t.dead) return { handled: false }; if (maintainMourning(t, dt)) return { handled: true }; if (t.memorialRequestId || t.state === "build_memorial") { const handled = updateMemorialMotion(t, dt); return { handled }; } if (maintainBellCall(t)) return { handled: false }; if (t.graveVisitTargetId || t.state === "visit_grave") { const grave = graveById(t.world, t.graveVisitTargetId); if (!grave) { clearGraveVisit(t, "墓がなくなった。"); return { handled: false }; } const d = Math.hypot((grave.x || 0) - t.x, (grave.y || 0) - t.y); const reach = Math.max(38, (t.radius || 22) + (grave.r || 18) * 0.82); if (t.graveVisitPhase === "visiting" || d <= reach) { if (t.graveVisitPhase !== "visiting") { const now = Number(t.world?.time || 0) || 0; const unit = typeof global.stableUnit === "function" ? global.stableUnit(familyKeyOf(t), `grave-visit-duration-${grave.id || "grave"}-${Math.floor(now)}`) : Math.random(); t.graveVisitPhase = "visiting"; t.graveVisitUntil = now + 4.5 + unit * 4.0; if (!t.graveVisitReliefApplied && typeof applyNeedRelief === "function") { applyNeedRelief(t, { social: -42, fulfill: -6 }); t.graveVisitReliefApplied = true; } setSpecialBehavior(t, "visit_grave", `${grave.memorialName || "親しいたりない"}を悼んでいる。`, grave); } t.vx *= Math.pow(0.12, Math.max(0.016, dt) * 60); t.vy *= Math.pow(0.12, Math.max(0.016, dt) * 60); maintainGraveVisit(t); return { handled: true }; } maintainGraveVisit(t); return { handled: false }; } return { handled: false }; } function ringAt(worldRef, x, y, requestedSize = "medium") { if (!worldRef) return 0; const size = ["small", "medium", "large"].includes(requestedSize) ? requestedSize : "medium"; const range = BELL_RANGES[size]; const duration = BELL_DURATIONS[size]; const now = Number(worldRef.time || 0) || 0; global.TarinaiAudio?.bell?.(); const px = Number(x || 0) || 0; const py = Number(y || 0) || 0; const target = { id: `bell-call-${Math.round(now * 1000)}-${Math.round(px)}-${Math.round(py)}`, type: "bell_call", x: px, y: py, dead: false }; let affected = 0; for (const t of worldRef.tarinai || []) { if (!t || t.dead) continue; if (Math.hypot(t.x - px, t.y - py) > range) continue; t.bellCallTarget = target; t.bellCallUntil = Math.max(Number(t.bellCallUntil || 0), now + duration); if (t.graveVisitTargetId) { t.graveVisitTargetId = ""; t.graveVisitUntil = 0; t.graveVisitPhase = ""; } if (!isCritical(t)) setSpecialBehavior(t, "bell_call", BELL_TEXT, target); affected += 1; } worldRef.spawnEffect?.("ring", px, py, { size: range, life: 0.62, color: "rgba(219,164,70,0.52)" }); worldRef.spawnEffect?.("ring", px, py, { size: Math.min(92, 34 + range * 0.08), life: 0.48, color: "rgba(244,197,100,0.58)" }); worldRef.drawListDirty = true; return affected; } const NATURAL_ASCENSION_SPRITES = Object.freeze(["sleep", "normal_happy"]); const REMOVAL_ASCENSION_SPRITES = Object.freeze(["hurt", "flee", "flee_fear2", "cold_1"]); function ascensionSpriteFor(worldRef, grave, sprites, salt = "grave-ascension") { const list = Array.isArray(sprites) && sprites.length ? sprites : ["sleep"]; const unit = typeof global.stableUnit === "function" ? global.stableUnit(grave?.memorialFamilyKey || grave?.id || "grave", `${salt}-${Math.floor(Number(worldRef?.time || 0) || 0)}`) : Math.random(); return list[Math.min(list.length - 1, Math.floor(Math.max(0, Math.min(0.999999, unit)) * list.length))] || list[0]; } function spawnGraveAscension(worldRef, grave, options = {}) { if (!worldRef || !grave) return false; const natural = options.natural === true; const spriteId = ascensionSpriteFor(worldRef, grave, natural ? NATURAL_ASCENSION_SPRITES : REMOVAL_ASCENSION_SPRITES, natural ? "grave-natural-ascension" : "grave-removal-ascension"); worldRef.spawnEffect?.("ascend_soul", grave.x, grave.y - Math.max(2, (grave.r || 14) * 0.25), { vx: 0, vy: -34, size: Math.max(18, (grave.r || 14) * 1.45), life: 2.4, importance: 3, allowOffscreen: true, forceVisual: true, spriteId, }); return true; } function applyExternalForce(worldRef, grave, magnitude = 0, breaker = null, reason = "external-force") { if (!worldRef || !grave || grave.dead || grave.type !== "grave") return false; const force = Math.max(0, Number(magnitude || 0) || 0); if (force < GRAVE_BREAK_FORCE) return false; const removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, grave, { reason: `grave-${reason}`, userReason: "強い衝撃で墓が壊れた", wake: true, breaker, })); if (removed) { worldRef.spawnEffect?.("ring", grave.x, grave.y, { size: 42, life: 0.28, color: "rgba(130,124,116,0.46)" }); worldRef.spawnEffect?.("fight", grave.x, grave.y, { size: 18, life: 0.24, color: "rgba(118,108,98,0.52)" }); } return removed; } function onGraveDestroyed(worldRef, grave, breaker = null) { if (!worldRef || !grave || grave.type !== "grave" || grave._memorialDestroyedHandled) return false; grave._memorialDestroyedHandled = true; if (grave._naturalDecaySilent) return false; spawnGraveAscension(worldRef, grave, { natural: false }); const eligible = new Set(Array.isArray(grave.memorialEligibleFamilyKeys) ? grave.memorialEligibleFamilyKeys : []); const now = Number(worldRef.time || 0) || 0; let count = 0; for (const t of worldRef.tarinai || []) { if (!t || t.dead || !eligible.has(familyKeyOf(t))) continue; if (t.graveVisitTargetId === grave.id) { t.graveVisitTargetId = ""; t.graveVisitUntil = 0; t.graveVisitPhase = ""; } t.graveMourningUntil = Math.max(Number(t.graveMourningUntil || 0), now + 5.5); t.bellCallTarget = null; t.memorialRequestId = ""; setSpecialBehavior(t, "mourn_grave", MOURN_TEXT, null); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.75); t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.5); t.stress = Math.min(100, (Number(t.stress || 0) || 0) + 12); worldRef.spawnBubble?.(t.x, t.y - (t.radius || 20) * 1.15, "……", "rgba(84,112,166,0.82)"); count += 1; } if (count) worldRef.log?.(`${grave.memorialName || "親しいたりない"}の墓が壊され、親しいたりないたちが泣いている。`, "death"); void breaker; return count > 0; } function removeGraveNaturally(worldRef, grave, day) { if (!worldRef || !grave || grave.dead || grave.type !== "grave" || grave.memorialFavorite) return false; const disappears = typeof global.deterministicChance === "function" ? global.deterministicChance(worldRef, "grave-natural-decay-0444", 0.25, grave.memorialFamilyKey || grave.id, day) : Math.random() < 0.25; if (!disappears) return false; grave._naturalDecaySilent = true; spawnGraveAscension(worldRef, grave, { natural: true }); const removed = Boolean(global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, grave, { reason: "grave-natural-decay", userReason: "いつの間にか消えた", wake: false, panicOwner: false, })); if (removed) worldRef.spawnEffect?.("ring", grave.x, grave.y, { size: 30, life: 0.36, color: "rgba(164,156,143,0.26)", importance: 2 }); return removed; } function updateWorldNaturalDecay(worldRef) { if (!worldRef) return 0; const now = Math.max(0, Number(worldRef.time || 0) || 0); const hasPrevious = Number.isFinite(Number(worldRef._graveNaturalDecayLastWorldTime)); const previous = hasPrevious ? Number(worldRef._graveNaturalDecayLastWorldTime) : now; worldRef._graveNaturalDecayLastWorldTime = now; if (!hasPrevious || now <= previous) return 0; const dayLength = Math.max(1, Number((typeof CONFIG !== "undefined" ? CONFIG.dayLength : 120) || 120) || 120); const targetFraction = (4 * 60 + 44) / (24 * 60); const firstDay = Math.max(0, Math.floor(previous / dayLength)); const lastDay = Math.max(firstDay, Math.floor(now / dayLength)); let removedCount = 0; for (let day = firstDay; day <= lastDay; day += 1) { if (Number(worldRef._graveNaturalDecayLastCheckedDay) === day) continue; const targetTime = day * dayLength + dayLength * targetFraction; if (!(previous < targetTime && now >= targetTime)) continue; worldRef._graveNaturalDecayLastCheckedDay = day; const graveBucket = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType("grave") : null; const graves = graveBucket && typeof graveBucket.length === "number" ? Array.from(graveBucket) : (worldRef.items || []).filter(item => item && !item.dead && item.type === "grave"); for (const grave of graves) if (removeGraveNaturally(worldRef, grave, day)) removedCount += 1; } return removedCount; } // Kept for compatibility with older direct callers. Grave decay is now // world-scheduled, so adding more graves does not add per-grave update work. function updateGraveNaturalDecay(grave, worldRef) { void grave; void worldRef; return false; } function formatWorldTime(timeValue) { const value = Number(timeValue); if (!Number.isFinite(value)) return ""; const dayLength = Math.max(1, Number((typeof CONFIG !== "undefined" ? CONFIG.dayLength : 120) || 120) || 120); const completedDays = Math.max(0, Math.floor(value / dayLength)); const season = ["春", "夏", "秋", "冬"][Math.floor((completedDays % 20) / 5)] || "春"; const seasonDay = (completedDays % 5) + 1; const progress = (((value % dayLength) + dayLength) % dayLength) / dayLength; const totalMinutes = Math.floor(progress * 24 * 60 + 1e-7) % (24 * 60); const hh = String(Math.floor(totalMinutes / 60)).padStart(2, "0"); const mm = String(totalMinutes % 60).padStart(2, "0"); return `${season}${seasonDay}日 ${hh}:${mm}`; } function graveTooltipLines(grave) { if (!grave) return []; const lines = ["墓"]; if (grave.memorialName) lines.push(`故人: ${grave.memorialName}`); const died = formatWorldTime(grave.memorialDeathTime); if (died) lines.push(`死亡日時: ${died}`); if (grave.memorialDeathReason) lines.push(`死因: ${grave.memorialDeathReason}`); return lines; } global.TarinaiMemorialBellSystem = Object.freeze({ BUILD_TEXT, MOURN_TEXT, BELL_TEXT, GRAVE_VISIT_APPROACH_TEXT, GRAVE_VISIT_TEXT, BELL_RANGES, MEMORIAL_BUILD_CHANCE, GRAVE_BREAK_FORCE, captureDeath, ringAt, updateAi, updateMotion, applyExternalForce, onGraveDestroyed, updateGraveNaturalDecay, updateWorldNaturalDecay, graveTooltipLines, formatWorldTime, }); })(typeof window !== "undefined" ? window : globalThis);