"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 GRASS_FIRE_EXTINGUISH_RATE = 0.56; const NEST_BOX_FIRE_EVACUATE_AFTER = 10.0; const NEST_BOX_FIRE_MIN_LIFE = 14.0; const NEST_BOX_FIRE_WET_REIGNITE_DELAY = 1.65; const NEST_BOX_FIRE_WATER_INTERVAL = 0.16; const NEST_BOX_FIRE_EFFECT_INTERVAL = 0.28; 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; const effectCap = Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().effectCap || 220)); if ((worldRef.effects.length || 0) >= effectCap) 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.spawnEffect("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) { worldRef._fireInteractionBudget = { frame, remaining: Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().fireInteractionBudget || 14)), }; } 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); const already = Boolean(t.burning || (t.burnTimer || 0) > 0.02); if (already && !force) return false; const ignitedDuringBirthRitual = !already && Boolean((t.birthRitualTimer || 0) > 0.04 || t.state === "birth_ritual"); t.burning = true; const dangerSource = source?.roles?.danger ? source : (source?._achievementDangerSource?.roles?.danger ? source._achievementDangerSource : null); if (dangerSource) t._achievementDangerSource = dangerSource; t.burnTimer = Math.max(t.burnTimer || 0, 5.0); t.thought = FIRE_BURNING_TEXT; t.lastPanicCause = "burning"; if (source && source !== t) t.lastPanicThreat = source; 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 }); if (ignitedDuringBirthRitual) global.TarinaiAchievements?.recordIgnition?.({ world: worldRef || t.world, 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"; if (source && source !== t) t.lastPanicThreat = source; 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; return true; } function isIgnitableNestBox(item) { return Boolean(item && !item.dead && item.type === "nest_box"); } function nestBoxFireRadius(box) { return Math.max(30, (Number(box?.r || 42) || 42) * 1.36); } function nestBoxFireActive(box) { return Boolean(isIgnitableNestBox(box) && (box.burning || (box.burnTimer || 0) > 0.02 || (box.nestFireEvacuationTimer || 0) > 0.02)); } function igniteNestBoxByFire(box, source = null, worldRef = null) { if (!isIgnitableNestBox(box)) return false; const now = Number(worldRef?.time || box.world?.time || 0) || 0; if (now < Number(box.nestFireWetUntil || 0)) return false; const already = nestBoxFireActive(box); box.burning = true; box.burnTimer = Math.max(Number(box.burnTimer || 0) || 0, NEST_BOX_FIRE_MIN_LIFE); if (!already) { box.nestFireStartedAt = now; box.nestFireEvacuationTimer = NEST_BOX_FIRE_EVACUATE_AFTER; box.nestFireEvacuated = false; box.flameWaterCooldown = 0; box.nestFireEffectCooldown = 0; worldRef?.log?.("\u5de3\u7bb1\u306b\u706b\u304c\u71c3\u3048\u79fb\u3063\u305f\u3002", "danger"); worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.35, life: 0.30, color: "rgba(255,122,48,0.56)" })); } worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire"); worldRef?.markItemBucketsDirty?.("nest-box-fire"); worldRef?.markSpatialDirty?.("nest-box-fire"); if (worldRef) worldRef.drawListDirty = true; return !already; } function extinguishNestBoxFire(box, worldRef = null, reason = "water") { if (!isIgnitableNestBox(box) || !nestBoxFireActive(box)) return false; box.burning = false; box.burnTimer = 0; box.nestFireEvacuationTimer = 0; box.nestFireEvacuated = false; box.nestFireStartedAt = 0; box.nestFireWetUntil = reason === "water" ? ((Number(worldRef?.time || box.world?.time || 0) || 0) + NEST_BOX_FIRE_WET_REIGNITE_DELAY) : 0; box.flameWaterCooldown = 0; box.nestFireEffectCooldown = 0; worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.22, life: 0.24, color: "rgba(108,192,236,0.62)" })); worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-fire-extinguished"); worldRef?.markItemBucketsDirty?.("nest-box-fire-extinguished"); worldRef?.markSpatialDirty?.("nest-box-fire-extinguished"); if (worldRef) worldRef.drawListDirty = true; return true; } function evacuateBurningNestBox(box, worldRef = null, reason = "nest-box-fire") { if (!isIgnitableNestBox(box) || !worldRef || box.nestFireEvacuated) return 0; const occupants = global.TarinaiNestSleepSystem?.nestBoxOccupants ? global.TarinaiNestSleepSystem.nestBoxOccupants(worldRef, box, Infinity) : (worldRef.tarinai || []).filter(t => t && !t.dead && t.insideNestBoxId === box.id); box.nestFireEvacuated = true; let count = 0; const now = Number(worldRef?.time || box.world?.time || 0) || 0; for (const t of occupants || []) { if (!t || t.dead) continue; t._burningNestExitBoxId = box.id || t._burningNestExitBoxId || ""; t._burningNestExitUntil = Math.max(Number(t._burningNestExitUntil || 0) || 0, now + 1.8); t.leaveNestBox?.(box); igniteTarinaiByFire(t, box, worldRef, true); t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 1.2); t.fearTimer = Math.max(t.fearTimer || 0, 2.4); count += 1; } if (count) { worldRef.log?.(`\u708e\u4e0a\u3057\u305f\u5de3\u7bb1\u304b\u3089${count}\u5339\u306e\u305f\u308a\u306a\u3044\u304c\u8ffd\u3044\u51fa\u3055\u308c\u305f\u3002`, "danger", { participants: occupants }); worldRef.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.55, life: 0.32, color: "rgba(255,92,38,0.58)" })); worldRef.markSpatialDirty?.(reason); worldRef.drawListDirty = true; } return count; } function burnOutNestBox(box, worldRef = null, reason = "burnout") { if (!isIgnitableNestBox(box)) return false; if (!box.nestFireEvacuated) evacuateBurningNestBox(box, worldRef, "nest-box-fire-burnout-evacuate"); box.burning = false; box.burnTimer = 0; box.nestFireEvacuationTimer = 0; box.nestFireEvacuated = true; box.nestFireStartedAt = 0; box.nestFireWetUntil = 0; box.flameWaterCooldown = 0; box.nestFireEffectCooldown = 0; box.amount = 0; if (box.isStructure) { box.hp = 0; box.dead = true; } worldRef?.log?.("\u5de3\u7bb1\u304c\u713c\u5931\u3057\u305f\u3002", "danger"); worldRef?.effects?.push(new Effect("ring", box.x, box.y, { size: nestBoxFireRadius(box) * 1.45, life: 0.34, color: "rgba(90,54,38,0.58)" })); worldRef?.markTerrainDirtyAt?.(box.x, box.y, nestBoxFireRadius(box) + 18, "nest-box-burnout"); worldRef?.markItemBucketsDirty?.("nest-box-burnout"); worldRef?.markSpatialDirty?.("nest-box-burnout"); worldRef?.compactItems?.(); if (worldRef) worldRef.drawListDirty = true; return true; } function updateNestBoxFire(box, dt, worldRef) { if (!isIgnitableNestBox(box) || !nestBoxFireActive(box) || !worldRef) return false; dt = Math.max(0, Math.min(0.24, Number(dt || 0) || 0)); box.burning = true; box.burnTimer = Math.max(0, Number(box.burnTimer || 0) - dt); box.nestFireEvacuationTimer = Math.max(0, Number(box.nestFireEvacuationTimer || 0) - dt); box.flameWaterCooldown = Math.max(0, Number(box.flameWaterCooldown || 0) - dt); box.nestFireEffectCooldown = Math.max(0, Number(box.nestFireEffectCooldown || 0) - dt); if (box.flameWaterCooldown <= 0) { box.flameWaterCooldown = NEST_BOX_FIRE_WATER_INTERVAL; const water = contactWaterDrop(worldRef, box.x, box.y, nestBoxFireRadius(box)); if (water) { consumeWaterForFire(worldRef, water, Math.max(12, (box.r || 42) * 0.34)); extinguishNestBoxFire(box, worldRef, "water"); return true; } } if (!box.nestFireEvacuated && box.nestFireEvacuationTimer <= 0) { evacuateBurningNestBox(box, worldRef, "nest-box-fire-evacuate"); } if (box.nestFireEffectCooldown <= 0) { box.nestFireEffectCooldown = NEST_BOX_FIRE_EFFECT_INTERVAL; pushFireEffect(worldRef, box.x, box.y - (box.r || 42) * 0.35, Math.max(13, (box.r || 42) * 0.40), box); } if (box.burnTimer <= 0.01) { burnOutNestBox(box, worldRef, "burnout"); return true; } worldRef.drawListDirty = true; 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); } 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 || t.insideNestBoxId || worldRef.isTarinaiHiddenInNestBox?.(t)) 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.nearbyNonGrassItems === "function" ? (worldRef.nearbyNonGrassItems(item.x, item.y, spreadRadius + 38, true) || []) : (worldRef.items || []); for (const it of nearby) { if (!it || it.dead || it === item) continue; 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 === "nest_box" && distXY(item.x, item.y, it.x, it.y) <= spreadRadius + Math.max(it.r || 42, 28) * 1.15) changed = igniteNestBoxByFire(it, item, worldRef) || 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; } } for (const grass of worldRef.nearbyGrass?.(item.x, item.y, spreadRadius + 38, true) || []) { if (!grass || grass.dead || distXY(item.x, item.y, grass.x, grass.y) > spreadRadius + Math.max(grass.r || 16, 12)) continue; const extinguished = tryGrassExtinguishFire(grass, item, worldRef, dt); changed = extinguished || changed; if (!extinguished && (item.amount || 0) > 0) changed = igniteGrassByFire(grass, item, worldRef, dt) || changed; } 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 || t.insideNestBoxId || worldRef.isTarinaiHiddenInNestBox?.(t)) 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)); if (!fire._achievementDangerSource && other._achievementDangerSource) fire._achievementDangerSource = other._achievementDangerSource; 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() { return Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().activeFireLimit || 60)); } 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; const searchRadius = Math.max(96, Number(opts.mergeRadius || opts.radius || 48) * 5.0); const candidates = typeof worldRef?.nearbyItems === "function" ? (worldRef.nearbyItems(x, y, searchRadius, true) || []) : fires; const scan = candidates.length ? candidates : fires; for (const fire of scan) { if (!fire || fire.dead || fire.type !== "fire") 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); if (opts.source) best._achievementDangerSource = opts.source._achievementDangerSource || opts.source; 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; if (opts.source) existing._achievementDangerSource = opts.source._achievementDangerSource || opts.source; 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; if (opts.source) fire._achievementDangerSource = opts.source._achievementDangerSource || opts.source; const addedFire = worldRef.addItem?.(fire, "spawn-fire", { terrain: false }) || null; if (!addedFire) return null; worldRef.drawListDirty = true; return addedFire; } 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 sizeScale = clamp((Number(item.r || 15) || 15) / 15, 0.45, 3.0); const flameMass = Math.pow(sizeScale, 1.28); const desired = Math.max(2, Math.round(5 + 5.5 * flameMass)); const count = Math.max(2, Math.min(desired, room > 0 ? room : Math.max(2, Math.round(2 + 2.5 * Math.min(flameMass, 1.6))))); 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", 18 + 10 * sizeScale, 68 + 56 * flameMass, 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", 6 + 3 * sizeScale, 10 + 6.5 * flameMass, item, i), amount: global.deterministicRange(worldRef, "flame-cracker-fire-amount", 22 + 34 * flameMass, 46 + 84 * flameMass, item, i), life: global.deterministicRange(worldRef, "flame-cracker-fire-life", 2.4 + 1.0 * sizeScale, 4.6 + 3.2 * flameMass, 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) * (2.4 + sizeScale * 0.65)), life: 0.30 + sizeScale * 0.045, 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, panicTarinaiFromFire, igniteAntByFire, igniteGrassByFire, igniteNestBoxByFire, extinguishNestBoxFire, updateNestBoxFire, burnOutNestBox, nestBoxFireActive, spawnFireAt: (worldRef, x, y, opts = {}) => spawnFireAt(worldRef, x, y, opts), scatterFireFrom, compactWorldFires, }); })(typeof window !== "undefined" ? window : globalThis);