"use strict"; const HEALTH = (() => { const MAJOR_DAMAGE_WINDOW = 24; const WEAKENED_MAJOR_DAMAGE_MIN_AGE = 3; const WEAKENED_MAJOR_DAMAGE_MAX_AGE = 20; const MAJOR_DAMAGE_REACTION_WINDOW = 1.0; const WEAKENED_TRACE_WINDOW = 45; const WEAKENED_SUFFIX = "\u3067\u8870\u5f31"; const CAUSES = Object.freeze({ fight: "\u55a7\u5629", hunger: "\u98e2\u9913", stress: "\u30b9\u30c8\u30ec\u30b9\u904e\u591a", poke: "\u3064\u3064\u304b\u308c\u3059\u304e", firecracker: "\u7206\u7af9", fire: "\u708e\u4e0a", genkotsu: "\u3052\u3093\u3053\u3064", shooting: "\u5c04\u6483", stoneCrush: "\u77f3\u306b\u6f70\u3055\u308c\u305f", weakness: "\u8870\u5f31", poisonBlock: "\u6bd2\u30d6\u30ed\u30c3\u30af", accident: "\u4e8b\u6545", collision: "\u885d\u7a81", outOfBounds: "\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664", lifespan: "\u5929\u5bff\u3092\u5168\u3046\u3057\u305f", sleepDisease: "\u306d\u3080\u308a\u75c5", explosionDisease: "\u7206\u767a\u75c5", fightDisease: "\u304d\u305a\u3064\u304d\u75c5", zunchiDisease: "\u305a\u3093\u3061\u75c5", antEaten: "\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f", giantDrug: "\u5de8\u5927\u85ac", dwarfDrug: "\u77ee\u5c0f\u85ac", freeze: "\u51CD\u7D50", heatstroke: "\u71B1\u4E2D\u75C7", cleaning: "\u6383\u9664", electrocution: "\u611f\u96fb", }); const DISEASE_RE = /([^\u3001\u3002\s]+\u75c5)/; function causeLabel(reason = "") { const text = String(reason || ""); if (!text) return ""; if (/\u6383\u9664|\u30ed\u30dc\u6383\u9664\u6a5f|robot.?cleaner|cleaned/.test(text)) return CAUSES.cleaning; if (/\u611f\u96fb|\u901a\u96fb\u4e2d\u306e\u96fb\u7dda|electrocution|electric.?shock|shocked/i.test(text)) return CAUSES.electrocution; if (text.includes(WEAKENED_SUFFIX)) return text.replace(/\u3067\u8870\u5f31.*$/, ""); if (/\u51CD\u7D50|\u4F4E\u6E29|\u5BD2\u3055|cold|freeze|freezing/i.test(text)) return CAUSES.freeze; if (/\u71B1\u4E2D\u75C7|\u9AD8\u6E29|\u6691\u3055|heat|hot/i.test(text)) return CAUSES.heatstroke; if (/\u5c04\u6483|shoot/.test(text)) return CAUSES.shooting; if (/\u77f3\u306b\u6f70\u3055\u308c\u305f|\u77f3.*\u6f70|stone.*crush|crushed.*stone/.test(text)) return CAUSES.stoneCrush; const collisionWith = text.match(/([^\u3001\u3002\s]+?)(?:\u306b|\u3068\u306e)\u885d\u7a81/); if (collisionWith) return `${collisionWith[1]}\u3068\u306e\u885d\u7a81`; if (/\u55a7\u5629|fight|headbutt/.test(text)) return CAUSES.fight; if (/\u3052\u3093\u3053\u3064|genkotsu/.test(text)) return CAUSES.genkotsu; if (/\u6bd2\u30d6\u30ed\u30c3\u30af|poison.?block/.test(text)) return CAUSES.poisonBlock; if (/\u5de8\u5927\u85ac|giant/.test(text)) return CAUSES.giantDrug; if (/\u77ee\u5c0f\u85ac|dwarf/.test(text)) return CAUSES.dwarfDrug; if (/\u7206\u7af9|firecracker|explosion/.test(text)) return CAUSES.firecracker; if (/\u708e\u4e0a|\u706b|\u708e|\u71c3|burn|fire/.test(text)) return CAUSES.fire; if (/\u4ed5\u69d8\u306b\u3088\u308a\u753b\u9762\u5916\u3067\u524a\u9664|\u753b\u9762\u5916\u3067\u524a\u9664|out.*bounds|offscreen/.test(text)) return CAUSES.outOfBounds; if (/\u885d\u7a81|collision|\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316/.test(text)) return CAUSES.collision; if (/\u3064\u3064\u304b\u308c|\u3064\u3064\u304d|poke/.test(text)) return CAUSES.poke; if (/\u306d\u3080\u308a\u75c5|sleep.*disease/.test(text)) return CAUSES.sleepDisease; if (/\u7206\u767a\u75c5|explosion.*disease/.test(text)) return CAUSES.explosionDisease; if (/\u3051\u3093\u304b\u75c5|\u304d\u305a\u3064\u304d\u75c5|fight.*disease/.test(text)) return CAUSES.fightDisease; if (/\u305a\u3093\u3061\u75c5|zunchi_sick|zunchi.*\u75c5/.test(text)) return CAUSES.zunchiDisease; if (/\u30a2\u30ea\u306b\u98df\u3079\u3089\u308c\u305f|eaten.*ant|ant.*eaten/.test(text)) return CAUSES.antEaten; if (/\u75c5/.test(text)) { const disease = text.match(DISEASE_RE); return disease ? disease[1] : CAUSES.zunchiDisease; } if (/\u98e2|\u7a7a\u8179|\u98df\u3079\u7269|hunger/.test(text)) return CAUSES.hunger; if (/\u30b9\u30c8\u30ec\u30b9|stress/.test(text)) return CAUSES.stress; if (/\u77f3|stone/.test(text)) return CAUSES.stoneCrush; if (/\u30dc\u30fc\u30eb|\u843d\u3061|\u843d\u4e0b|\u9053\u5177|\u4e8b\u6545|\u5f53\u305f|ball|drop|accident/.test(text)) return CAUSES.accident; if (/\u5bff\u547d|\u5929\u5bff/.test(text)) return CAUSES.lifespan; return ""; } function recentDamageCause(t, maxAge = MAJOR_DAMAGE_WINDOW) { const now = t.world?.time || 0; if (t.lastDamageCause && now - (t.lastDamageAt || -999) <= maxAge) return t.lastDamageCause; return ""; } function rememberDamage(t, amount, reason = "") { const cause = causeLabel(reason) || CAUSES.accident; const now = t.world?.time || 0; t.lastDamageCause = cause; t.lastDamageAmount = Math.max(0, amount || 0); t.lastDamageAt = now; if (t.lastDamageAmount >= 6 || t.lastDamageAmount >= Math.max(4, (t.energy || 0) * 0.22)) { t.lastMajorDamageCause = cause; t.lastMajorDamageAt = now; } return cause; } function weakenedDeathReasonFor(cause = "") { const text = String(cause || "").trim(); if (!text) return ""; return text.includes(WEAKENED_SUFFIX) ? text : `${text}${WEAKENED_SUFFIX}`; } function hasRecentMajorDamage(t) { const now = t.world?.time || 0; return Boolean(t.lastMajorDamageCause && now - (t.lastMajorDamageAt || -999) <= MAJOR_DAMAGE_WINDOW); } function hasLingeringMajorDamage(t) { const now = t.world?.time || 0; const age = now - (t.lastMajorDamageAt || -999); return Boolean(t.lastMajorDamageCause && age >= WEAKENED_MAJOR_DAMAGE_MIN_AGE && age < WEAKENED_MAJOR_DAMAGE_MAX_AGE); } function isFightingContext(t, recent) { return Boolean(t.fightTimer > 0.04 || t.defeatedTimer > 0.04 || t.defeatedById || t.fightWinnerId || recent === CAUSES.fight); } function dominantDeathCause(t, reason = "", opts = {}) { const text = String(reason || "").trim(); const direct = causeLabel(text); const recentMajor = hasRecentMajorDamage(t); const recent = recentDamageCause(t, 20); const fighting = isFightingContext(t, recent); // Direct damage death: use the immediate/direct source, not a weakened suffix. if (opts.fromDamage) { if (direct) return direct; if (recentMajor && t.lastMajorDamageCause) return t.lastMajorDamageCause; if (recent) return recent; return CAUSES.accident; } if (direct === CAUSES.fire || recent === CAUSES.fire || t.lastMajorDamageCause === CAUSES.fire) return CAUSES.fire; if (direct === CAUSES.heatstroke || recent === CAUSES.heatstroke || t.lastMajorDamageCause === CAUSES.heatstroke) return CAUSES.heatstroke; if (direct === CAUSES.freeze || recent === CAUSES.freeze || t.lastMajorDamageCause === CAUSES.freeze) return CAUSES.freeze; if (recentMajor && t.lastMajorDamageCause === CAUSES.collision && !(t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) return CAUSES.collision; if (hasLingeringMajorDamage(t) && (!direct || direct === t.lastMajorDamageCause || t.energy <= 18 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 112)) { return weakenedDeathReasonFor(t.lastMajorDamageCause); } const weakenedTrace = recentDamageCause(t, WEAKENED_TRACE_WINDOW); const traceCanWeaken = weakenedTrace && weakenedTrace !== CAUSES.stress && weakenedTrace !== CAUSES.hunger && weakenedTrace !== CAUSES.lifespan && weakenedTrace !== CAUSES.outOfBounds; if (traceCanWeaken && (!direct || direct === CAUSES.stress || direct === CAUSES.hunger || direct === weakenedTrace) && (t.energy <= 20 || t.mood <= 16 || t.stress >= 124 || t.hunger >= 114)) { return weakenedDeathReasonFor(weakenedTrace); } if (direct && direct !== CAUSES.stress) return direct; const tempStatus = t.temperatureStatus || t.world?.temperatureStatusFor?.(t.feltTemperature ?? t.tempComfort ?? (CONFIG.standardTemperature ?? 15), t) || null; const recentTemperatureDamageAt = Number(t.lastTemperatureDamageAt || -999); const recentTemperatureDamage = (t.world?.time || 0) - recentTemperatureDamageAt <= 36; if (recentTemperatureDamage && tempStatus && (tempStatus.harmExcess || 0) > 0 && (t.energy <= 26 || t.stress >= 112 || t.mood <= 18)) { return tempStatus.direction === "cold" ? CAUSES.freeze : CAUSES.heatstroke; } if (t.zunchiDisease && ((t.zunchiDiseaseSeverity || 0) >= 0.30 || /\u75c5|zunchi/.test(text))) return CAUSES.zunchiDisease; if (t.hunger >= 108 || (t.hunger >= 94 && t.energy <= 26) || /\u98e2|\u7a7a\u8179|hunger/.test(text)) return CAUSES.hunger; if (fighting && (t.energy <= 44 || t.stress >= 112 || t.mood <= 22)) return CAUSES.fight; if (recent && recent !== CAUSES.stress && (t.energy <= 22 || t.mood <= 14 || t.stress >= 126)) return weakenedDeathReasonFor(recent); if (text.includes(CAUSES.lifespan) || text.includes("\u5bff\u547d")) return CAUSES.lifespan; if (direct) return direct; if (t.stress >= 118 || /\u30b9\u30c8\u30ec\u30b9|stress|\u6c17\u5206/.test(text)) return CAUSES.stress; if ((opts.lowHealth || t.energy <= 0.5) && hasLingeringMajorDamage(t)) return weakenedDeathReasonFor(t.lastMajorDamageCause); if (opts.lowHealth || t.energy <= 0.5) return CAUSES.accident; return ""; } function normalizeDeathReason(t, reason = "", opts = {}) { const text = String(reason || "").trim(); if (text.includes(WEAKENED_SUFFIX)) return text; const cause = dominantDeathCause(t, text, opts) || CAUSES.accident; if (cause.includes(WEAKENED_SUFFIX)) return cause; if (opts.fromDamage) return cause; const canWeaken = cause && !cause.endsWith("\u75c5") && cause !== CAUSES.hunger && cause !== CAUSES.stress && cause !== CAUSES.lifespan && cause !== CAUSES.outOfBounds && cause !== CAUSES.collision && cause !== CAUSES.genkotsu && cause !== CAUSES.fire && cause !== CAUSES.weakness && cause !== CAUSES.freeze && cause !== CAUSES.heatstroke && cause !== CAUSES.cleaning; if (canWeaken && (opts.weakened || (hasLingeringMajorDamage(t) && cause === t.lastMajorDamageCause))) return weakenedDeathReasonFor(cause); return cause; } const MAJOR_DAMAGE_REACTIONS = Object.freeze([ { id: "voice_major_damage_01", text: "\u3042\u3042\u3042\u3042\uff01" }, { id: "voice_major_damage_02", text: "\u3048\u3048\u3048\u3048\uff01" }, { id: "voice_major_damage_03", text: "\u306f\u3046\u3046\u3046\uff01" }, { id: "voice_major_damage_04", text: "\u306f\u3046\u306f\u3046\uff01" }, ]); function spawnSilentDamageBubble(t, text = "!", color = "rgba(142,58,58,0.86)") { const world = t?.world; if (!world) return false; // Use the normal Tarinai bubble path first so positioning, draw order, and // throttling match other player-visible speech bubbles. Major-damage // bubbles are forced because they must win over incidental bubbles such as // \u300c\u3076\u308a\u3085\u3063\u300d fired by the same damage event. if (typeof t.bubble === "function" && t.bubble(text, 1.65, color, { force: true })) { t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55); return true; } if (typeof Effect === "undefined") return false; if ((world.effectCounts?.bubble || 0) >= (CONFIG?.bubbleLimit || 80)) return false; const r = Math.max(16, t.radius || 22); world.effects?.push(new Effect("bubble", t.x, t.y - r * 1.38, { vx: (Math.random() - 0.5) * 2.4, vy: -4.8 - Math.random() * 2.8, life: 2.4 + Math.random() * 0.6, size: 9, text, color, })); world.effectCounts.bubble = (world.effectCounts.bubble || 0) + 1; t.nextBubbleAt = Math.max(t.nextBubbleAt || 0, (world.time || 0) + 1.55); t._damageBubblePriorityUntil = Math.max(Number(t._damageBubblePriorityUntil || 0), (world.time || 0) + 1.55); world.drawListDirty = true; return true; } function maybeTriggerMajorDamageReaction(t, actualLoss = 0, cause = "") { if (!t || !t.world || t.dead) return false; const maxEnergy = Math.max(1, typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100)); const loss = Math.max(0, Number(actualLoss) || 0); if (loss <= 0.01) return false; const now = t.world.time || 0; const cutoff = now - MAJOR_DAMAGE_REACTION_WINDOW; const entries = Array.isArray(t.recentDamageWindow) ? t.recentDamageWindow : []; entries.push({ at: now, amount: loss, cause: cause || "" }); const kept = []; let total = 0; let dominant = { amount: 0, cause: cause || "" }; for (const entry of entries) { const at = Number(entry?.at || 0); const amount = Math.max(0, Number(entry?.amount) || 0); if (at < cutoff || amount <= 0) continue; kept.push({ at, amount, cause: entry?.cause || "" }); total += amount; if (amount >= dominant.amount) dominant = { amount, cause: entry?.cause || "" }; } t.recentDamageWindow = kept.slice(-20); if (total < maxEnergy * 0.30) return false; const majorCause = causeLabel(dominant.cause || cause) || causeLabel(cause) || CAUSES.accident; t.lastMajorDamageCause = majorCause; t.lastMajorDamageAt = now; if (now < Number(t.nextMajorDamageVoiceAt || -999)) return false; t.nextMajorDamageVoiceAt = now + 1.0; const list = MAJOR_DAMAGE_REACTIONS; const reaction = list[Math.floor(Math.random() * list.length)] || list[0]; audio.play?.(reaction.id, { category: "voice", minGap: 0.85 }); spawnSilentDamageBubble(t, reaction.text, "rgba(146,58,58,0.88)"); t.hurtTimer = Math.max(t.hurtTimer || 0, 1.55); t.fearTimer = Math.max(t.fearTimer || 0, 2.6); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.55); if (t.enterPanic) t.enterPanic({ threat: null, reason: "\u5927\u304d\u306a\u30c0\u30e1\u30fc\u30b8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.12, wake: true, hurtTimer: 1.45, surpriseTimer: 0.55, cause: "major_damage", }); return true; } function maybeReleaseZunchiFromDamage(t, actualLoss = 0, cause = "") { if (!t || !t.world || t.dead) return false; const loss = Math.max(0, Number(actualLoss) || 0); if (loss <= 0.1) return false; const now = t.world.time || 0; if (now < Number(t.nextDamageZunchiAt || -999)) return false; const probability = Math.max(0.06, Math.min(0.82, 0.06 + loss / 48)); if (Math.random() >= probability) return false; t.nextDamageZunchiAt = now + Math.max(0.55, 1.55 - Math.min(0.9, loss / 38)); if (t.hasLodgedPinEffect?.("blocksZunchi")) { t.oshiriByoZunchiStock = Math.min(24, Math.max(0, t.oshiriByoZunchiStock || 0) + 1); t.thought = "\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u6211\u6162\u3057\u3066\u3044\u308b"; return false; } const side = t.facingDir ? t.facingDir() : (t.facing || 1); const jitter = (Math.random() - 0.5) * Math.max(4, (t.radius || 22) * 0.25); const x = t.x - side * (t.radius || 22) * (0.72 + Math.random() * 0.24); const y = t.y + (t.radius || 22) * (0.24 + Math.random() * 0.25) + jitter; // Damage-triggered release is involuntary and must never be redirected to a toilet. t.world.spawnZunchi?.(x, y, t); t.poopCount = (t.poopCount || 0) + 1; t.digest = Math.max(0, Number(t.digest || 0) - 1.0); // If this zunchi release was caused by a major-damage event, keep the pain // voice bubble visible and suppress the incidental \u300c\u3076\u308a\u3085\u3063\u300d bubble. if (now >= Number(t._damageBubblePriorityUntil || -999)) { t.bubble?.("\u3076\u308a\u3085\u3063", 1.1, "rgba(74,91,50,0.78)", { force: true }); } if (now > Number(t.nextDamageZunchiLogAt || -999)) { t.nextDamageZunchiLogAt = now + 10; t.world.log?.(`${t.name}\u306f\u30c0\u30e1\u30fc\u30b8\u3067\u305a\u3093\u3061\u3092\u653e\u51fa\u3057\u305f\u3002`, "accident", { participants: [t] }); } return true; } function applyDamage(t, amount, reason = "", options = {}) { const maxEnergy = typeof tarinaiMaxEnergy === "function" ? tarinaiMaxEnergy(t) : (Number(t.maxEnergy) || 100); t.maxEnergy = maxEnergy; const before = clamp(Number(t.energy) || 0, 0, maxEnergy); const loss = Math.max(0, amount || 0); t.energy = clamp(before - loss, 0, maxEnergy); const actualLoss = Math.max(0, before - t.energy); let cause = ""; if (actualLoss > 0.01) { globalThis.TarinaiAchievements?.recordDamageSource?.(t, options.source || null, { world: t.world, amount: actualLoss, reason }); cause = rememberDamage(t, actualLoss, reason); const plushie = (t.world?.ownedItemsFor?.(t.id) || t.world?.itemsOfType?.("plushie") || t.world?.items || []).find(item => item && !item.dead && item.isStructure && item.type === "plushie" && item.ownerId === t.id && item.carriedById === t.id); if (plushie && actualLoss >= 1) plushie.damage(actualLoss, t.world, null); if (cause === CAUSES.fight || /\u55a7\u5629|fight/.test(String(reason || ""))) t.recordBleedExposure(); const majorDamageReacted = maybeTriggerMajorDamageReaction(t, actualLoss, cause || reason); if (!majorDamageReacted) audio.damage?.(actualLoss); maybeReleaseZunchiFromDamage(t, actualLoss, cause || reason); if (t.sleepDisease) t.recoverSleepDisease("\u30c0\u30e1\u30fc\u30b8\u3067\u306d\u3080\u308a\u75c5\u304c\u6cbb\u3063\u305f"); if (t.wakeFromDamage) t.wakeFromDamage(cause || reason); if (t.maybeBecomeTimidAfterDamage) t.maybeBecomeTimidAfterDamage(cause || reason); t.showHpBar(); } if (t.energy <= 0.5) { t.die(reason || cause, { fromDamage: true }); } } function briefDeathReason(t, reason = t.deathReason) { const text = String(reason || ""); if (!text) return ""; const normalized = normalizeDeathReason(t, text); return normalized.length > 12 ? `${normalized.slice(0, 12)}\u2026` : normalized; } return Object.freeze({ recentDamageCause, rememberDamage, weakenedDeathReasonFor, dominantDeathCause, normalizeDeathReason, applyDamage, briefDeathReason, }); })(); window.HEALTH = HEALTH;