"use strict"; // Runtime behavior for fire, firecracker, flame spread, and fire contact effects. (function (global) { function updateFirecracker(item, dt, worldRef) { item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt); if (item.fuseTimer <= 0) { if (worldRef?.explodeFirecracker) worldRef.explodeFirecracker(item); else item.amount = 0; } } const FIRE_BURNING_TEXT = "\u71c3\u3048\u3066\u9003\u3052\u307e\u3069\u3063\u3066\u3044\u308b\u3002"; const FIRE_SEEN_TEXT = "\u708e\u3092\u898b\u3066\u9a5a\u3044\u3066\u3044\u308b\u3002"; const FIRE_CONTACT_INTERVAL = 0.35; const FIRE_PANIC_INTERVAL = 1.20; const FIRE_WATER_INTERVAL = 0.16; const FIRE_CLUSTER_SCAN_INTERVAL = 0.45; const FIRE_INTERACTION_BUDGET_DESKTOP = 18; const FIRE_INTERACTION_BUDGET_MOBILE = 10; const FIRE_MAX_ACTIVE_DESKTOP = 72; const FIRE_MAX_ACTIVE_MOBILE = 44; const GRASS_FIRE_EXTINGUISH_RATE = 0.56; function fireTempMultiplier(worldRef, x, y, entity = null) { const weather = String(worldRef?.weather || ""); const temp = Number(worldRef?.temperatureAt?.(x, y, entity) ?? worldRef?.feltTemperatureFor?.(entity) ?? worldRef?.temperature ?? global.CONFIG?.standardTemperature ?? 15); let mul = 1; if (weather === "light_rain") mul *= 2; if (Number.isFinite(temp) && temp <= 10) mul *= 2; return mul; } function pushFireEffect(worldRef, x, y, size = 12, seed = null) { if (!worldRef?.effects || typeof Effect !== "function") return; if ((worldRef.effects.length || 0) > 180) return; const a = global.deterministicAngle(worldRef, "fire-effect-angle", seed || x, y, worldRef.time || 0); const speed = global.deterministicRange(worldRef, "fire-effect-speed", 3, 15, seed || x, y); worldRef.effects.push(new Effect("flame", x, y, { vx: Math.cos(a) * speed * 0.35, vy: -Math.max(4, speed * 0.65), size: Math.max(4, size * 0.58), life: global.deterministicRange(worldRef, "fire-effect-life", 0.18, 0.34, seed || x, y), color: "rgba(236,46,24,0.78)", })); } function fireInteractionBudget(worldRef) { const frame = Math.floor((Number(worldRef?.time || 0) || 0) * 60); if (!worldRef._fireInteractionBudget || worldRef._fireInteractionBudget.frame !== frame) { const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760); worldRef._fireInteractionBudget = { frame, remaining: mobile ? FIRE_INTERACTION_BUDGET_MOBILE : FIRE_INTERACTION_BUDGET_DESKTOP, }; } return worldRef._fireInteractionBudget; } function useFireInteractionBudget(worldRef, cost = 1) { const budget = fireInteractionBudget(worldRef); if (!budget || budget.remaining < cost) return false; budget.remaining -= cost; return true; } function contactWaterDrop(worldRef, x, y, radius = 16) { if (!worldRef) return null; const list = typeof worldRef.nearbyItems === "function" ? [...(worldRef.nearbyItems(x, y, radius + 28, true) || [])] : (worldRef.items || []); for (const it of list) { if (!it || it.dead || it.type !== "water") continue; const rr = Math.max(radius, 10) + Math.max(it.r || 10, 10); if (distXY(x, y, it.x, it.y) <= rr) return it; } return null; } function consumeWaterForFire(worldRef, water, amount = 8) { if (!water) return false; water.amount = Math.max(0, Number(water.amount || 0) - amount); worldRef?.effects?.push(new Effect("ring", water.x, water.y, { size: Math.max(10, (water.r || 10) * 1.4), life: 0.18, color: "rgba(108,192,236,0.58)" })); worldRef?.markItemBucketsDirty?.("fire-water"); worldRef?.markSpatialDirty?.("fire-water"); return true; } function igniteTarinaiByFire(t, source = null, worldRef = null, force = false) { if (!t || t.dead) return false; if (typeof t.igniteFire === "function") return t.igniteFire(source, force); t.burning = true; t.burnTimer = Math.max(t.burnTimer || 0, 5.0); t.thought = FIRE_BURNING_TEXT; t.lastPanicCause = "burning"; t.lastPanicDetail = FIRE_BURNING_TEXT; t.fearTimer = Math.max(t.fearTimer || 0, 1.5); t.sleeping = false; t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(source || null, true) : null, reason: FIRE_BURNING_TEXT, wake: true }); worldRef?.emit?.("fire:ignite", { tarinai: t, source }); return true; } function panicTarinaiFromFire(t, source = null, worldRef = null) { if (!t || t.dead || t.burning || (t.burnTimer || 0) > 0.02) return false; const now = Number(worldRef?.time || t.world?.time || 0) || 0; if (now < Number(t.firePanicCooldownUntil || 0)) return false; t.firePanicCooldownUntil = now + 0.85; t.firePanicTimer = Math.max(t.firePanicTimer || 0, 1.4); t.lastPanicCause = "fire_seen"; t.lastPanicDetail = FIRE_SEEN_TEXT; t.thought = FIRE_SEEN_TEXT; if (typeof t.enterPanic === "function") t.enterPanic({ threat: source, target: source, reason: FIRE_SEEN_TEXT, fear: 1.25, wake: true, cause: "fire_seen", surpriseTimer: 0.55, awakeLockTimer: 0.35 }); else { t.fearTimer = Math.max(t.fearTimer || 0, 1.25); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(source || null, true) : null, reason: FIRE_SEEN_TEXT, wake: true }); } return true; } function igniteAntByFire(ant, source = null, worldRef = null) { if (!ant || ant.dead) return false; if (typeof ant.igniteFire === "function") return ant.igniteFire(source); ant.burning = true; ant.burnTimer = Math.max(ant.burnTimer || 0, 5.0); ant.state = "panic"; ant.targetId = ""; ant.targetRef = null; worldRef?.emit?.("fire:ignite-ant", { ant, source }); return true; } function grassFireExtinguishChance(worldRef, grass, dt = 0.016) { const baseDt = Math.max(Number(dt || 0) || 0, FIRE_CONTACT_INTERVAL); const weather = String(worldRef?.weather || ""); const fresh = clamp(Number(grass?.growth ?? grass?.health ?? 0.65) || 0, 0, 1); const rainMul = weather === "light_rain" ? 1.75 : 1; return clamp(baseDt * GRASS_FIRE_EXTINGUISH_RATE * rainMul * (1 + fresh * 0.45), 0, 0.58); } function tryGrassExtinguishFire(grass, fire, worldRef = null, dt = 0.016) { if (!grass || grass.dead || grass.type !== "grass" || !fire || fire.dead || fire.type !== "fire") return false; const chance = grassFireExtinguishChance(worldRef, grass, dt); if (!global.deterministicChance(worldRef, "grass-fire-extinguish", chance, grass, fire)) return false; const reduction = Math.max(18, Math.max(grass.r || 16, 12) * 0.72 + Math.max(fire.r || 14, 12) * 0.52); fire.amount = Math.max(0, Number(fire.amount || 0) - reduction); fire.flameLife = Math.max(0, Number(fire.flameLife || 0) - 0.85); grass.health = clamp((grass.health ?? 1) - 0.035, 0, 1); if ((fire.amount || 0) <= 24 || (fire.flameLife || 0) <= 0.35) fire.amount = 0; worldRef?.effects?.push(new Effect("ring", fire.x, fire.y, { size: Math.max(18, (fire.r || 14) * 1.8), life: 0.18, color: "rgba(118,188,94,0.46)" })); worldRef?.markTerrainDirtyAt?.(grass.x, grass.y, Math.max(grass.r || 17, 28), "grass-extinguish-fire"); worldRef?.markItemBucketsDirty?.("grass-extinguish-fire"); worldRef?.markSpatialDirty?.("grass-extinguish-fire"); worldRef.drawListDirty = true; return true; } function igniteGrassByFire(grass, source = null, worldRef = null, dt = 0.016) { if (!grass || grass.dead || grass.type !== "grass") return false; grass.burning = true; grass.burnTimer = Math.max(grass.burnTimer || 0, 2.8); grass.health = clamp((grass.health ?? 1) - dt * 0.30, 0, 1); grass.amount = Math.max(0, Number(grass.amount || 0) - dt * 18); if (grass.health <= 0.02 || grass.amount <= 0.5) grass.amount = 0; if (worldRef && (worldRef.time || 0) > Number(grass.nextFireSpreadAt || -Infinity)) { grass.nextFireSpreadAt = (worldRef.time || 0) + 1.6; if (typeof worldRef.spawnFireAt === "function") { const a = global.deterministicAngle(worldRef, "grass-fire-spread", grass, source); worldRef.spawnFireAt(grass.x + Math.cos(a) * (grass.r || 14) * 0.55, grass.y + Math.sin(a) * (grass.r || 14) * 0.35, { amount: 60, radius: 12, life: 4.2, source: grass }); } } worldRef?.markTerrainDirtyAt?.(grass.x, grass.y, Math.max(grass.r || 17, 26), "grass-fire"); return true; } function igniteZunchiByFire(zunchi, source = null, worldRef = null, dt = 0.016) { if (!zunchi || zunchi.dead || zunchi.type !== "zunchi") return false; const wasBurning = Boolean(zunchi.burning || (zunchi.burnTimer || 0) > 0.02); zunchi.burning = true; zunchi.burnTimer = Math.max(Number(zunchi.burnTimer || 0) || 0, 4.6); zunchi.spawnGrace = 0; zunchi.freshness = clamp((zunchi.freshness ?? 1) - dt * 0.55, 0, 1); zunchi.amount = Math.max(0, Number(zunchi.amount || 0) - dt * 34); zunchi.stageTimer = Math.max(0, Number(zunchi.stageTimer || 0) + dt * 2.4); if (!wasBurning) { pushFireEffect(worldRef, zunchi.x, zunchi.y - Math.max(5, (zunchi.r || 14) * 0.2), Math.max(10, zunchi.r || 14), zunchi); worldRef?.emit?.("fire:ignite-zunchi", { item: zunchi, source }); } if ((zunchi.amount || 0) <= 0.5) zunchi.amount = 0; worldRef?.markTerrainDirtyAt?.(zunchi.x, zunchi.y, Math.max(zunchi.r || 15, 28), "zunchi-fire"); worldRef?.markItemBucketsDirty?.("zunchi-fire"); worldRef?.markSpatialDirty?.("zunchi-fire"); worldRef.drawListDirty = true; return true; } function updateFireContacts(item, dt, worldRef) { if (!item || !worldRef) return false; const r = Math.max(14, Number(item.r || 0) || 14); const spreadRadius = r + 16; let changed = false; for (const t of worldRef.nearbyTarinai?.(item.x, item.y, spreadRadius + 32, true) || []) { if (!t || t.dead) continue; const d = distXY(item.x, item.y, t.x, t.y); if (d <= spreadRadius + Math.max(t.radius || 18, 12) * 0.72) changed = igniteTarinaiByFire(t, item, worldRef, false) || changed; } for (const ant of worldRef.nearbyAnts?.(item.x, item.y, spreadRadius + 20, true) || []) { if (!ant || ant.dead) continue; const d = distXY(item.x, item.y, ant.x, ant.y); if (d <= spreadRadius + Math.max(ant.r || 5, 4)) changed = igniteAntByFire(ant, item, worldRef) || changed; } const nearby = typeof worldRef.nearbyItems === "function" ? (worldRef.nearbyItems(item.x, item.y, spreadRadius + 38, true) || []) : (worldRef.items || []); for (const it of nearby) { if (!it || it.dead || it === item) continue; if (it.type === "grass" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 16, 12)) { const extinguished = tryGrassExtinguishFire(it, item, worldRef, dt); changed = extinguished || changed; if (!extinguished && (item.amount || 0) > 0) changed = igniteGrassByFire(it, item, worldRef, dt) || changed; } if (it.type === "zunchi" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 15, 12)) changed = igniteZunchiByFire(it, item, worldRef, dt) || changed; if (it.type === "water" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 10, 8)) { consumeWaterForFire(worldRef, it, Math.max(6, r * 0.45)); item.amount = 0; changed = true; } } return changed; } function panicFromNearbyFire(item, worldRef) { if (!item || !worldRef) return; const r = Math.max(170, (item.r || 14) + 170); for (const t of worldRef.nearbyTarinai?.(item.x, item.y, r, true) || []) { if (!t || t.dead) continue; panicTarinaiFromFire(t, item, worldRef); } } function updateFire(item, dt, worldRef) { if (!item || item.dead || !worldRef) return false; dt = Math.max(0, Math.min(0.08, Number(dt || 0) || 0)); item.amount = Math.max(0, Number(item.amount || 0) - dt * 4.2); item.flameLife = Math.max(0, Number(item.flameLife ?? 8.0) - dt); item.flameSpreadCooldown = Math.max(0, Number(item.flameSpreadCooldown || 0) - dt); item.flamePanicCooldown = Math.max(0, Number(item.flamePanicCooldown || 0) - dt); item.flameWaterCooldown = Math.max(0, Number(item.flameWaterCooldown || 0) - dt); item.flameClusterCooldown = Math.max(0, Number(item.flameClusterCooldown || 0) - dt); item.vx = 0; item.vy = 0; const lowFire = (item.amount || 0) < 24 || item.flameLife < 1.2; if (item.flameWaterCooldown <= 0) { item.flameWaterCooldown = FIRE_WATER_INTERVAL; const water = contactWaterDrop(worldRef, item.x, item.y, Math.max(item.r || 14, 12)); if (water) { consumeWaterForFire(worldRef, water, Math.max(8, (item.r || 14) * 0.6)); item.amount = 0; return true; } } const chance = dt * 0.012 * fireTempMultiplier(worldRef, item.x, item.y, item); if (global.deterministicChance(worldRef, "fire-random-extinguish", chance, item)) { item.amount = 0; return true; } if (item.flameLife <= 0) item.amount = Math.min(item.amount, 1); if (item.flameClusterCooldown <= 0) { mergeNearbyFires(worldRef, item, Math.max(24, (item.r || 14) * 2.4), 4); item.flameClusterCooldown = FIRE_CLUSTER_SCAN_INTERVAL; } if (!lowFire && item.flameSpreadCooldown <= 0 && useFireInteractionBudget(worldRef, 1)) { updateFireContacts(item, dt, worldRef); item.flameSpreadCooldown = FIRE_CONTACT_INTERVAL; } if (!lowFire && item.flamePanicCooldown <= 0 && useFireInteractionBudget(worldRef, 1)) { panicFromNearbyFire(item, worldRef); item.flamePanicCooldown = FIRE_PANIC_INTERVAL; } if (!lowFire && global.deterministicChance(worldRef, "fire-effect", dt * 0.72, item)) pushFireEffect(worldRef, item.x, item.y - (item.r || 14) * 0.2, Math.max(8, (item.r || 14) * 0.9), item); return true; } function mergeNearbyFires(worldRef, fire, radius = 36, maxMerges = 4) { if (!worldRef || !fire || fire.dead) return 0; const nearby = typeof worldRef.nearbyItems === "function" ? (worldRef.nearbyItems(fire.x, fire.y, radius, true) || []) : (worldRef.items || []); let merged = 0; for (const other of nearby) { if (!other || other === fire || other.dead || other.type !== "fire") continue; if (distXY(fire.x, fire.y, other.x, other.y) > radius + Math.max(other.r || 14, 12)) continue; fire.amount = Math.min(260, Math.max(fire.amount || 0, other.amount || 0) + Math.min(34, Math.max(0, Number(other.amount || 0) * 0.35))); fire.flameLife = Math.max(fire.flameLife || 0, other.flameLife || 0); fire.r = Math.min(42, Math.max(fire.r || 14, other.r || 14)); other.amount = 0; merged += 1; if (merged >= maxMerges) break; } if (merged) { worldRef.markItemBucketsDirty?.("fire-merge"); worldRef.markSpatialDirty?.("fire-merge"); worldRef.drawListDirty = true; } return merged; } function activeFireLimit() { const mobile = Boolean(global.TarinaiInputMode?.snapshot?.().touchFirst || (global.innerWidth || 9999) <= 760); return mobile ? FIRE_MAX_ACTIVE_MOBILE : FIRE_MAX_ACTIVE_DESKTOP; } function activeFires(worldRef) { return worldRef?.itemsOfType?.("fire") || (worldRef?.items || []).filter(it => it && !it.dead && it.type === "fire"); } function foldIntoExistingFire(worldRef, x, y, opts = {}) { const fires = activeFires(worldRef); if (!fires.length) return null; let best = null; let bestD = Infinity; const exclude = opts.exclude || null; for (const fire of fires) { if (!fire || fire.dead) continue; if (exclude && fire === exclude) continue; const d = distXY(x, y, fire.x, fire.y); if (d < bestD) { best = fire; bestD = d; } } if (!best) return null; best.amount = Math.min(280, Math.max(Number(best.amount || 0), Number(opts.amount || 0) * 0.45) + 14); best.flameLife = Math.max(Number(best.flameLife || 0), Math.max(1.0, Number(opts.life || 3.5) * 0.75)); best.r = Math.min(44, Math.max(best.r || 14, Number(opts.radius || 14))); best.vx = 0; best.vy = 0; worldRef.drawListDirty = true; return best; } function compactWorldFires(worldRef, opts = {}) { const fires = activeFires(worldRef).filter(fire => fire && !fire.dead); const limit = Math.max(8, Number(opts.limit || activeFireLimit()) || activeFireLimit()); if (fires.length <= limit) return 0; const cellSize = Math.max(46, Number(opts.cellSize || 72) || 72); const cells = new Map(); for (const fire of fires) { const key = `${Math.floor((fire.x || 0) / cellSize)}:${Math.floor((fire.y || 0) / cellSize)}`; let group = cells.get(key); if (!group) { group = []; cells.set(key, group); } group.push(fire); } let removed = 0; for (const group of cells.values()) { if (group.length <= 1) continue; group.sort((a, b) => ((b.amount || 0) + (b.flameLife || 0) * 8 + (b.r || 0)) - ((a.amount || 0) + (a.flameLife || 0) * 8 + (a.r || 0))); const keep = group[0]; for (let i = 1; i < group.length; i += 1) { const fire = group[i]; keep.amount = Math.min(300, Math.max(keep.amount || 0, fire.amount || 0) + Math.min(18, Math.max(0, Number(fire.amount || 0) * 0.20))); keep.flameLife = Math.max(keep.flameLife || 0, fire.flameLife || 0); keep.r = Math.min(46, Math.max(keep.r || 14, fire.r || 14)); fire.amount = 0; removed += 1; } } if (fires.length - removed > limit) { const survivors = fires.filter(fire => !fire.dead).sort((a, b) => ((a.amount || 0) + (a.flameLife || 0) * 8) - ((b.amount || 0) + (b.flameLife || 0) * 8)); let remaining = survivors.length; for (const fire of survivors) { if (remaining <= limit) break; if (fire.amount <= 0) continue; foldIntoExistingFire(worldRef, fire.x, fire.y, { ...fire, exclude: fire }); fire.amount = 0; removed += 1; remaining -= 1; } } if (removed) { worldRef.compactItems?.(); worldRef.markItemBucketsDirty?.("fire-compact"); worldRef.markSpatialDirty?.("fire-compact"); worldRef.drawListDirty = true; } return removed; } function spawnFireAt(worldRef, x, y, opts = {}) { if (!worldRef || typeof Item !== "function") return null; const currentFires = activeFires(worldRef); if (currentFires.length >= activeFireLimit()) return foldIntoExistingFire(worldRef, x, y, opts); const mergeRadius = Math.max(70, Number(opts.mergeRadius || opts.radius || 16) * 4.5); const nearby = typeof worldRef.nearbyItems === "function" ? (worldRef.nearbyItems(x, y, mergeRadius, true) || []) : (worldRef.items || []); for (const existing of nearby) { if (!existing || existing.dead || existing.type !== "fire") continue; if (distXY(x, y, existing.x, existing.y) > mergeRadius + Math.max(existing.r || 14, 12)) continue; existing.amount = Math.min(260, Math.max(Number(existing.amount || 0), Number(opts.amount || 0) * 0.55) + 18); existing.flameLife = Math.max(Number(existing.flameLife || 0), Math.max(1.2, Number(opts.life || 4.0) * 0.85)); existing.r = Math.min(42, Math.max(existing.r || 14, Number(opts.radius || 14))); existing.vx = 0; existing.vy = 0; worldRef.drawListDirty = true; return existing; } const fire = new Item("fire", x, y); const scale = Math.max(0.45, Math.min(2.0, Number(opts.scale || 1) || 1)); fire.r = Math.max(9, Number(opts.radius || fire.r || 14) * scale); fire.amount = Math.max(12, Number(opts.amount || fire.amount || 100) * scale); fire.flameLife = Math.max(1.2, Number(opts.life || fire.flameLife || 7.5)); fire.vx = 0; fire.vy = 0; worldRef.addItem?.(fire, "spawn-fire", { terrain: false }); worldRef.drawListDirty = true; return fire; } function scatterFireFrom(item, worldRef) { if (!item || !worldRef) return false; const existing = activeFires(worldRef).length; const limit = activeFireLimit(); const room = Math.max(0, limit - existing); const desired = Math.max(5, Math.round(8 * Math.max(0.75, Math.min(1.4, (item.r || 15) / 15)))); const count = Math.max(3, Math.min(desired, room > 0 ? room : 3)); for (let i = 0; i < count; i++) { const a = (Math.PI * 2 * i / count) + global.deterministicRange(worldRef, "flame-cracker-angle", -0.20, 0.20, item, i); const d = global.deterministicRange(worldRef, "flame-cracker-radius", 28, 118, item, i); const fire = spawnFireAt(worldRef, item.x + Math.cos(a) * d, item.y + Math.sin(a) * d, { radius: global.deterministicRange(worldRef, "flame-cracker-fire-radius", 10, 18, item, i), amount: global.deterministicRange(worldRef, "flame-cracker-fire-amount", 55, 120, item, i), life: global.deterministicRange(worldRef, "flame-cracker-fire-life", 4.0, 8.5, item, i), source: item, }); if (fire) { fire.vx = 0; fire.vy = 0; updateFireContacts(fire, FIRE_CONTACT_INTERVAL, worldRef); } pushFireEffect(worldRef, item.x + Math.cos(a) * Math.min(d, 28), item.y + Math.sin(a) * Math.min(d, 28), 14, item); } worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(42, (item.r || 15) * 3.0), life: 0.36, color: "rgba(255,126,54,0.58)" })); item.amount = 0; worldRef.markItemBucketsDirty?.("flame-firecracker"); worldRef.markSpatialDirty?.("flame-firecracker"); return true; } function updateFlameFirecracker(item, dt, worldRef) { item.fuseTimer = Math.max(0, (item.fuseTimer ?? 5) - dt); if (item.fuseTimer <= 0) { if (worldRef) scatterFireFrom(item, worldRef); else item.amount = 0; } return true; } global.TarinaiFireRuntimeSystem = Object.freeze({ updateFirecracker, updateFlameFirecracker, updateFire, fireBurningText: FIRE_BURNING_TEXT, fireSeenText: FIRE_SEEN_TEXT, fireTempMultiplier, contactWaterDrop, consumeWaterForFire, igniteTarinaiByFire, panicTarinaiFromFire, igniteAntByFire, igniteGrassByFire, spawnFireAt: (worldRef, x, y, opts = {}) => spawnFireAt(worldRef, x, y, opts), scatterFireFrom, compactWorldFires, }); })(typeof window !== "undefined" ? window : globalThis);