"use strict"; (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_combat_effects.js"); Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ damageAntsInRadius(x, y, radius, damageFn, reason = "", opts = {}) { let hit = 0; const candidates = this.nearbyAnts?.(x, y, radius + 24, true) || this.ants || []; for (const ant of candidates) { if (!ant || ant.dead) continue; const d = Math.max(1, distXY(ant.x, ant.y, x, y)); if (d > radius + (ant.r || 4)) continue; const p = clamp(1 - d / Math.max(1, radius), 0, 1); if (p <= 0 && d > radius) continue; const damage = typeof damageFn === "function" ? damageFn(p, ant, d) : Number(damageFn || 0); if (damage > 0) { global.TarinaiAchievements?.recordAntDamageSource?.(ant, opts.source || null, { world: this, reason }); ant.hp = Math.max(0, (ant.hp ?? ant.maxHp ?? ANT_WORKER_HP ?? 32) - damage); ant.hpBarTimer = Math.max(ant.hpBarTimer || 0, 1.25); } if (opts.push) { let nx = (ant.x - x) / d; let ny = (ant.y - y) / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { const a = deterministicAngle(this, "ant-push-overlap", ant, x, y); nx = Math.cos(a); ny = Math.sin(a); } const push = typeof opts.push === "function" ? opts.push(p, ant, d) : Number(opts.push || 0); ant.vx = (ant.vx || 0) + nx * push; ant.vy = (ant.vy || 0) + ny * push; ant.x = clamp(ant.x + nx * Math.min(18, push * 0.018), CONFIG.worldPadding, this.w - CONFIG.worldPadding); ant.y = clamp(ant.y + ny * Math.min(18, push * 0.018), CONFIG.worldPadding, this.h - CONFIG.worldPadding); } hit += 1; if (ant.hp <= 0 && ant.dieAsCorpse) ant.dieAsCorpse(ant.homeNest?.()); } if (hit > 0) this.drawListDirty = true; return hit; }, blastLoosePinsFrom(x, y, blastRadius, source = null, scale = 1, salt = "blast") { let blastedPins = 0; const pinSystem = globalThis.TarinaiItemDynamicPinSystem; const pins = (this.items || []).filter(it => it && !it.dead && isPinType(it.type)); for (const pin of pins) { if (pin.pinState === "lodged") continue; let dx = pin.x - x; let dy = pin.y - y; let d = Math.hypot(dx, dy) || 1; if (d > blastRadius * 1.18) continue; const p = clamp(1 - d / (blastRadius * 1.18), 0, 1); if (d < 0.001) { const a = deterministicAngle(this, `${salt}-pin-overlap`, source || pin, pin); dx = Math.cos(a); dy = Math.sin(a); d = 1; } if (pin.pinState === "ball_lodged") pinSystem?.detach?.(pin, this, "blast"); const blast = (360 + p * 820) * (Number(scale) || 1); this.applyImpulse(pin, dx / d * blast + deterministicRange(this, `${salt}-pin-jitter-x`, -70, 70, source || pin, pin), dy / d * blast + deterministicRange(this, `${salt}-pin-jitter-y`, -70, 70, source || pin, pin) ); pin.spinVelocity = clamp((pin.spinVelocity || 0) + deterministicRange(this, `${salt}-pin-spin`, -18, 18, source || pin, pin), -38, 38); pin.noStickUntil = (this.time || 0) + 0.22; pin.lastPokedAt = this.time || 0; this.effects.push(new Effect("ring", pin.x, pin.y, { size: Math.max(12, (pin.r || 8) * 1.8), life: 0.18, color: "rgba(255,230,116,0.46)" })); blastedPins += 1; } return blastedPins; }, spawnFireAt(x, y, opts = {}) { return globalThis.TarinaiItemDynamicToolSystem?.spawnFireAt?.(this, x, y, opts) || null; }, scatterFireFrom(it) { return globalThis.TarinaiItemDynamicToolSystem?.scatterFireFrom?.(it, this) || false; }, explodeFirecracker(it) { if (!it || it.dead || (it.type !== "firecracker" && it.type !== "sticky_bomb")) return false; it.amount = 0; const x = it.x, y = it.y; const blastRadius = 240 * (it.blastScale || 1); this.effects.push(new Effect("explosion", x, y, { size: 86, life: 1.05, color: "rgba(255,182,66,0.92)" })); this.effects.push(new Effect("explosion", x, y, { size: 128, life: 0.82, color: "rgba(255,92,42,0.70)" })); for (let r = 0; r < 4; r++) { this.effects.push(new Effect("ring", x, y, { size: 30 + r * 24, life: 0.48 + r * 0.11, color: r % 2 ? "rgba(255,116,62,0.78)" : "rgba(255,240,124,0.88)" })); } this.blastZunchiFrom(x, y, blastRadius * 0.92, it.blastScale || 1); for (let i = 0; i < 34; i++) { const a = Math.PI * 2 * i / 34 + deterministicRange(this, "firecracker-spark-angle", -0.10, 0.10, it, i); const speed = deterministicRange(this, "firecracker-spark-speed", 90, 260, it, i); this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * deterministicRange(this, "firecracker-spark-x", 2, 22, it, i), y + Math.sin(a) * deterministicRange(this, "firecracker-spark-y", 2, 22, it, i), { vx: Math.cos(a) * speed, vy: Math.sin(a) * speed, size: deterministicRange(this, "firecracker-spark-size", 8, i % 3 === 0 ? 24 : 20, it, i), life: deterministicRange(this, "firecracker-spark-life", 0.26, 0.72, it, i), color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)", })); } for (const t of this.tarinai) { if (t.dead) continue; const wasSleeping = t.state === "sleep" || t.sleeping || t.state === "seek_bed"; t.sleeping = false; if (wasSleeping) { if (t.enterPanic) t.enterPanic({ target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", fear: 1.2, surpriseTimer: 0.95, wake: true, cause: "firecracker_wakeup" }); else { t.setActionState?.("panic", { target: { x, y, dead: false }, reason: "\u7206\u7af9\u3067\u305f\u305f\u304d\u8d77\u3053\u3055\u308c\u305f", wake: true }); t.surpriseTimer = Math.max(t.surpriseTimer, 0.95); t.fearTimer = Math.max(t.fearTimer, 1.2); } } if (t.addStress) t.addStress(deterministicRange(this, "firecracker-startle-stress", 8, 15, it, t), { threshold: 8 }); t.fearTimer = Math.max(t.fearTimer, 0.58); const dx = t.x - x; const dy = t.y - y; const d = Math.hypot(dx, dy) || 1; if (d > blastRadius) continue; const p = clamp(1 - d / blastRadius, 0, 1); t.damage(5 + p * 18, "\u7206\u7af9", { source: it }); if (t.addStress) t.addStress(22 * p, { threshold: 8 }); t.hurtTimer = Math.max(t.hurtTimer, 1.5 + p * 1.8); t.surpriseTimer = Math.max(t.surpriseTimer, 0.85); if (t.enterPanic) t.enterPanic({ target: { x, y }, reason: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.7 + p, wake: true, cause: "firecracker_blast" }); else { t.fearTimer = Math.max(t.fearTimer, 1.7 + p); t.setActionState?.("panic", { target: { x, y }, reason: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); } let nx = dx / d; let ny = dy / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t); nx = Math.cos(a); ny = Math.sin(a); } const distanceBoost = p * p * 0.55 + p * 0.45; const power = (260 + distanceBoost * 920) * deterministicRange(this, "firecracker-tarinai-power", 0.78, 1.24, it, t) * (it.blastScale || 1); this.applyImpulse(t, nx * power + deterministicRange(this, "firecracker-tarinai-jitter-x", -95, 95, it, t) * (0.6 + p), ny * power + deterministicRange(this, "firecracker-tarinai-jitter-y", -95, 95, it, t) * (0.6 + p), { panic: true, target: { x, y }, fearTimer: 1.7 + p, thought: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", }); t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85); t.fallMax = Math.max(t.fallMax || 0, t.fallTimer); t.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "firecracker-fall-dir", it, t); t.blastSpinTimer = Math.max(t.blastSpinTimer || 0, 1.35 + p * 0.70); t.blastSpinMax = Math.max(t.blastSpinMax || 0, t.blastSpinTimer); this.spawnFallEffect(t.x, t.y + t.radius * 0.45, 1.1 + p); const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15; if (!t.dead && p > 0.22 && deterministicChance(this, "firecracker-explosion-disease", t.diseaseChance ? t.diseaseChance(explosionChance) : explosionChance, it, t)) t.infectExplosionDisease?.(it); if (!t.dead && deterministicChance(this, "firecracker-panic-bubble", 0.45, it, t)) this.spawnBubble(t.x, t.y - t.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it); this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u7af9", { push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * (it.blastScale || 1), source: it, }); let blastedBalls = 0; for (const ball of (this.itemsOfType ? (this.itemsOfType("ball") || []).concat(this.itemsOfType("balloon") || []) : this.items)) { if (!ball || ball.dead || (ball.type !== "ball" && ball.type !== "balloon")) continue; let dx = ball.x - x; let dy = ball.y - y; let d = Math.hypot(dx, dy) || 1; if (d > blastRadius * 1.18) continue; const p = clamp(1 - d / (blastRadius * 1.18), 0, 1); if (d < 0.001) { const a = deterministicAngle(this, "firecracker-ball-overlap", it, ball); dx = Math.cos(a); dy = Math.sin(a); d = 1; } if (ball.type === "balloon") { globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(ball, this, it); blastedBalls += 1; continue; } const blast = 430 + p * 960; this.applyImpulse(ball, dx / d * blast + deterministicRange(this, "firecracker-ball-jitter-x", -80, 80, it, ball), dy / d * blast + deterministicRange(this, "firecracker-ball-jitter-y", -80, 80, it, ball)); ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "firecracker-ball-spin", -24, 24, it, ball), -38, 38); ball.lastPokedAt = this.time || 0; ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5); this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" })); blastedBalls += 1; } const blastedPins = this.blastLoosePinsFrom(x, y, blastRadius, it, it.blastScale || 1, "firecracker"); audio.explode(); this.log((blastedBalls || blastedPins) ? "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u8efd\u3044\u7269\u304c\u6025\u52a0\u901f\u3057\u305f\u3002" : "\u7206\u7af9\u304c\u6d3e\u624b\u306b\u306f\u3058\u3051\u3001\u8fba\u308a\u304c\u3056\u308f\u3064\u3044\u305f\u3002", "accident"); return true; }, explodeDiseaseTarinai(t) { if (!t || t.dead) return; const x = t.x, y = t.y; const blastScale = 0.68; const blastRadius = 240 * blastScale; t.explosionDisease = false; t.explosionDiseaseTimer = 0; this.effects.push(new Effect("explosion", x, y, { size: 86 * blastScale, life: 1.05, color: "rgba(255,182,66,0.92)" })); this.effects.push(new Effect("explosion", x, y, { size: 128 * blastScale, life: 0.82, color: "rgba(255,92,42,0.70)" })); for (let r = 0; r < 4; r++) { this.effects.push(new Effect("ring", x, y, { size: (30 + r * 24) * blastScale, life: 0.48 + r * 0.11, color: r % 2 ? "rgba(255,116,62,0.78)" : "rgba(255,240,124,0.88)" })); } this.blastZunchiFrom(x, y, blastRadius * 0.92, blastScale); for (let i = 0; i < 34; i++) { const a = Math.PI * 2 * i / 34 + deterministicRange(this, "disease-explosion-spark-angle", -0.10, 0.10, t, i); const speed = deterministicRange(this, "disease-explosion-spark-speed", 90, 260, t, i) * blastScale; this.effects.push(new Effect(i % 3 === 0 ? "explosion" : "fight", x + Math.cos(a) * deterministicRange(this, "disease-explosion-spark-x", 2, 22, t, i) * blastScale, y + Math.sin(a) * deterministicRange(this, "disease-explosion-spark-y", 2, 22, t, i) * blastScale, { vx: Math.cos(a) * speed, vy: Math.sin(a) * speed, size: deterministicRange(this, "disease-explosion-spark-size", 8, i % 3 === 0 ? 24 : 20, t, i) * blastScale, life: deterministicRange(this, "disease-explosion-spark-life", 0.26, 0.72, t, i), color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)", })); } for (const o of this.tarinai) { if (!o || o.dead || o === t) continue; const dx = o.x - x; const dy = o.y - y; const d = Math.hypot(dx, dy) || 1; if (d > blastRadius) continue; const q = clamp(1 - d / blastRadius, 0, 1); o.damage(5 + q * 18, "\u7206\u767a\u75c5"); if (o.addStress) o.addStress(22 * q, { threshold: 8 }); let nx = dx / d; let ny = dy / d; if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) { const a = deterministicAngle(this, "disease-explosion-overlap-normal", t, o); nx = Math.cos(a); ny = Math.sin(a); } const distanceBoost = q * q * 0.55 + q * 0.45; const power = (260 + distanceBoost * 920) * deterministicRange(this, "disease-explosion-tarinai-power", 0.78, 1.24, t, o) * blastScale; this.applyImpulse(o, nx * power + deterministicRange(this, "disease-explosion-tarinai-jitter-x", -95, 95, t, o) * (0.6 + q), ny * power + deterministicRange(this, "disease-explosion-tarinai-jitter-y", -95, 95, t, o) * (0.6 + q), { panic: true, target: { x, y }, fearTimer: 1.7 + q, thought: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", }); o.hurtTimer = Math.max(o.hurtTimer || 0, 1.5 + q * 1.8); o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.85); if (o.enterPanic) o.enterPanic({ target: { x, y }, reason: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", fear: 1.7 + q, wake: true, cause: "explosion_disease_blast" }); else { o.fearTimer = Math.max(o.fearTimer || 0, 1.7 + q); o.setActionState?.("panic", { target: { x, y }, reason: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b", wake: true }); } o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85); o.fallMax = Math.max(o.fallMax || 0, o.fallTimer); o.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "disease-explosion-fall-dir", t, o); o.blastSpinTimer = Math.max(o.blastSpinTimer || 0, 1.35 + q * 0.70); o.blastSpinMax = Math.max(o.blastSpinMax || 0, o.blastSpinTimer); this.spawnFallEffect(o.x, o.y + o.radius * 0.45, 1.1 + q); const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15; if (!o.dead && q > 0.22 && deterministicChance(this, "disease-explosion-spread", o.diseaseChance ? o.diseaseChance(explosionChance) : explosionChance, t, o)) o.infectExplosionDisease?.(t); if (!o.dead && deterministicChance(this, "disease-explosion-panic-bubble", 0.45, t, o)) this.spawnBubble(o.x, o.y - o.radius * 1.35, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); } this.damageAntsInRadius(x, y, blastRadius, p => 5 + p * 18, "\u7206\u767a\u75c5", { push: p => (260 + (p * p * 0.55 + p * 0.45) * 920) * 0.42 * blastScale, source: t, }); for (const ball of (this.itemsOfType ? (this.itemsOfType("ball") || []).concat(this.itemsOfType("balloon") || []) : this.items)) { if (!ball || ball.dead || (ball.type !== "ball" && ball.type !== "balloon")) continue; let dx = ball.x - x; let dy = ball.y - y; let d = Math.hypot(dx, dy) || 1; if (d > blastRadius * 1.18) continue; const q = clamp(1 - d / (blastRadius * 1.18), 0, 1); if (d < 0.001) { const a = deterministicAngle(this, "disease-explosion-ball-overlap", t, ball); dx = Math.cos(a); dy = Math.sin(a); d = 1; } if (ball.type === "balloon") { globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(ball, this, t); continue; } const blast = 430 + q * 960; this.applyImpulse(ball, dx / d * blast * blastScale + deterministicRange(this, "disease-explosion-ball-jitter-x", -80, 80, t, ball), dy / d * blast * blastScale + deterministicRange(this, "disease-explosion-ball-jitter-y", -80, 80, t, ball)); ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "disease-explosion-ball-spin", -24, 24, t, ball), -38, 38); ball.lastPokedAt = this.time || 0; ball.pokeCombo = Math.max(ball.pokeCombo || 0, 5); this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(18, ball.r * 1.2), life: 0.24, color: "rgba(255,230,116,0.58)" })); } this.blastLoosePinsFrom(x, y, blastRadius, t, blastScale, "disease-explosion"); t.die("\u7206\u767a\u75c5"); audio.explode(); this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident", { participants: [t] }); }, waterHoseRadius() { return 138; }, applyWaterHose(x, y, dx = 0, dy = 0, dt = 0.08) { const radius = this.waterHoseRadius?.() || 138; const speed = Math.hypot(dx, dy) || 1; const nx = speed > 1 ? dx / speed : Math.cos(this.time * 9.1); const ny = speed > 1 ? dy / speed : Math.sin(this.time * 7.7); let cleaned = 0; for (const it of this.nearbyItems(x, y, radius + 80)) { if (!it || it.dead) continue; const d = distXY(it.x, it.y, x, y); if (d > radius + (it.r || 12)) continue; const p = clamp(1 - d / (radius + (it.r || 12)), 0, 1); if (it.type === "zunchi" || it.type === "splat" || it.type === "trace") { it.amount -= dt * (it.type === "zunchi" ? 230 : 180) * (0.35 + p); cleaned += p; } else if (it.type === "grass") { it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 135 * (0.30 + p)); it.growth = Math.max(0, (Number(it.growth) || 0) - dt * 0.50 * (0.20 + p)); if (it.amount <= 0.5) { it.amount = 0; it.hp = 0; it.growth = 0; it.grassStage = -1; } cleaned += p * 0.72; } else if (it.type === "fire") { it.amount = Math.max(0, (Number(it.amount) || 0) - dt * 540 * (0.35 + p)); it.flameLife = Math.max(0, (Number(it.flameLife) || 0) - dt * 5.2 * (0.35 + p)); it.fireShrinkPulse = Math.max(Number(it.fireShrinkPulse || 0), 0.18); if (it.amount <= 4 || it.flameLife <= 0.05) { it.amount = 0; it.flameLife = 0; } cleaned += p * 1.15; } else if (it.type === "grass_bed") { it.amount = 0; it.hp = 0; global.TarinaiStructureLifecycle.deleteItem(this, it, { reason: "water-hose-grass-bed", userReason: "\u6d17\u6d44\u3067\u6d41\u3055\u308c\u305f", wake: true }); cleaned += p * 0.95; } } for (const ef of this.effects || []) { if (!ef || ef.dead) continue; if (ef.type !== "bleed" && ef.type !== "splat") continue; const d = distXY(ef.x || 0, ef.y || 0, x, y); if (d > radius + 22) continue; const p = clamp(1 - d / (radius + 22), 0, 1); ef.life = Math.min(ef.life || 0.1, Math.max(0.01, (ef.life || 0.1) - dt * (1.6 + p * 5.0))); cleaned += p * 0.55; } for (const t of this.nearbyTarinai(x, y, radius + 70)) { if (!t || t.dead || this.isTarinaiHiddenInNestBox(t)) continue; const d = distXY(t.x, t.y, x, y); if (d > radius + t.radius) continue; const p = clamp(1 - d / (radius + t.radius), 0, 1); t.vx += nx * (95 + 165 * p) + deterministicRange(this, "water-hose-jitter-x", -10, 10, t, x, y); t.vy += ny * (95 + 165 * p) + deterministicRange(this, "water-hose-jitter-y", -10, 10, t, x, y); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24); t.applyWaterEffect?.(dt * (1.0 + p * 1.8), "hose"); if (t.burning || (t.burnTimer || 0) > 0) { t.extinguishFire?.("\u6d17\u6d44\u3067\u6d88\u706b\u3055\u308c\u305f"); cleaned += p * 1.10; } } for (const ant of this.nearbyAnts?.(x, y, radius + 45) || []) { if (!ant || ant.dead) continue; const d = distXY(ant.x, ant.y, x, y); if (d > radius + (ant.r || 4)) continue; const p = clamp(1 - d / (radius + (ant.r || 4)), 0, 1); if (ant.burning || (ant.burnTimer || 0) > 0) { ant.extinguishFire?.("water-hose"); cleaned += p * 0.75; } } if (deterministicChance(this, "water-hose-ring", 0.72, x, y)) { this.effects.push(new Effect("ring", x + deterministicRange(this, "water-hose-ring-x", -18, 18, x, y), y + deterministicRange(this, "water-hose-ring-y", -12, 12, x, y), { size: deterministicRange(this, "water-hose-ring-size", 8, 22, x, y), life: deterministicRange(this, "water-hose-ring-life", 0.18, 0.32, x, y), color: "rgba(128,204,238,0.60)" })); } if (cleaned > 0.25) { this.drawListDirty = true; this.markItemBucketsDirty?.("water-hose-clean"); this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean"); this.markSpatialDirty?.("water-hose-clean"); this.compactItems?.(); this.updateItemCounts(); } }, spawnBurstZunchi(source, count = 1, pin = null) { const n = Math.max(1, Math.min(28, Math.floor(count || 1))); const sx = Number.isFinite(source?.x) ? source.x : (pin?.x || this.w * 0.5); const sy = Number.isFinite(source?.y) ? source.y : (pin?.y || this.h * 0.5); let spawned = 0; for (let i = 0; i < n; i++) { const a = -Math.PI * 0.15 + deterministicRange(this, "zunchi-burst-angle", -0.92, 0.92, source, pin, i) + (i / Math.max(1, n - 1) - 0.5) * 1.2; const spawnX = clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40); const spawnY = clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40); const sand = this.toiletSandAt?.(spawnX, spawnY) || null; if (sand) { sand.toiletFlash = 1; sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8); this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb"); continue; } const it = new Item("zunchi", spawnX, spawnY); it.amount = 240; it.vx = Math.cos(a) * deterministicRange(this, "zunchi-burst-speed-x", 260, 560, source, pin, i) + deterministicRange(this, "zunchi-burst-jitter-x", -40, 40, source, pin, i); it.vy = Math.sin(a) * deterministicRange(this, "zunchi-burst-speed-y", 200, 480, source, pin, i) - deterministicRange(this, "zunchi-burst-up", 30, 160, source, pin, i); it.achievementBurstOwnerId = source?.id || ""; it.achievementBurstNeverStopped = true; it.achievementBurstLaunchedAt = this.time || 0; if (pin?.type === "oshibyo") it._achievementDangerSource = pin; it.stage = "fresh"; if (this.addItem?.(it, "zunchi-burst")) spawned += 1; } if (!spawned) return 0; this.effects?.push(new Effect("zunchi_miasma", sx, sy, { size: 28, life: 0.65, color: "rgba(74,91,50,0.48)" })); this.log?.(`${source?.name || "\u305f\u308a\u306a\u3044"}\u304b\u3089\u6e9c\u307e\u3063\u3066\u3044\u305f\u305a\u3093\u3061\u304c\u5f3e\u3051\u98db\u3093\u3060\u3002`, "accident", { participants: source?.name ? [source] : [] }); audio.place?.("zunchi"); this.ensureSpatial?.("zunchi-burst"); return spawned; }, toiletSandAt(x, y) { const candidates = typeof this.itemsOfType === "function" ? this.itemsOfType("toilet") : (this.nearbyItems?.(x, y, 96) || []); for (const item of candidates || []) { if (!item || item.dead || item.type !== "toilet") continue; if (globalThis.TarinaiToiletSandSystem?.contains?.(item, x, y)) return item; } return null; }, spawnZunchi(x, y, source = null) { const spawnX = clamp(x, 40, this.w - 40); const spawnY = clamp(y, 40, this.h - 40); const sand = this.toiletSandAt?.(spawnX, spawnY) || null; if (sand) { sand.toiletFlash = 1; sand.awakeUntil = Math.max(sand.awakeUntil || 0, (this.time || 0) + 0.8); this.markTerrainDirtyAt?.(sand.x, sand.y, Math.max(64, (sand.r || 48) * 1.72), "toilet-sand-absorb"); return null; } const it = new Item("zunchi", spawnX, spawnY); it.amount = 260; it.stage = "fresh"; it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0); it.eatProtectedUntil = (this.time || 0) + 90; const added = this.addItem?.(it, "zunchi-spawn"); if (!added) return null; audio.place?.("zunchi"); return added; }, spawnBubble(x, y, text, color = "rgba(42,36,29,0.78)") { if ((this.effectCounts.bubble || 0) >= CONFIG.bubbleLimit) return; if (text === "z") text = "Zzz..."; audio.bubble(text); this.effects.push(new Effect("bubble", x, y, { vx: deterministicRange(this, "bubble-vx", -2, 2, x, y, text), vy: deterministicRange(this, "bubble-vy", -5, -2, x, y, text), life: deterministicRange(this, "bubble-life", 2.2, 3.2, x, y, text), size: 8, text, color, })); this.effectCounts.bubble = (this.effectCounts.bubble || 0) + 1; }, spawnHeadbuttEffect(x, y) { this.effects.push(new Effect("fight", x, y, { size: deterministicRange(this, "headbutt-effect-size", 10, 16, x, y), life: deterministicRange(this, "headbutt-effect-life", 0.20, 0.34, x, y), color: "rgba(105, 62, 34, 0.86)", })); if (deterministicChance(this, "headbutt-audio", 0.5, x, y)) audio.fight(); }, spawnFallEffect(x, y, scale = 1) { if ((this.effectCounts.fall || 0) > 18) return; this.effects.push(new Effect("fall", x, y, { vx: deterministicRange(this, "fall-effect-vx", -12, 12, x, y), vy: deterministicRange(this, "fall-effect-vy", -4, 3, x, y), size: deterministicRange(this, "fall-effect-size", 20, 32, x, y) * scale, life: deterministicRange(this, "fall-effect-life", 0.48, 0.72, x, y), color: "rgba(154, 124, 80, 0.58)", })); }, spawnEatEffect(x, y, color = "#f1dfb7") { if (deterministicChance(this, "eat-effect", 0.70, x, y, color)) { this.effects.push(new Effect("eat", x + deterministicRange(this, "eat-effect-x", -4, 4, x, y, color), y + deterministicRange(this, "eat-effect-y", -4, 4, x, y, color), { vx: deterministicRange(this, "eat-effect-vx", -10, 10, x, y, color), vy: deterministicRange(this, "eat-effect-vy", -18, -3, x, y, color), size: deterministicRange(this, "eat-effect-size", 2.2, 4.2, x, y, color), life: deterministicRange(this, "eat-effect-life", 0.18, 0.32, x, y, color), color, })); } if (deterministicChance(this, "eat-ring", 0.08, x, y, color)) { this.effects.push(new Effect("ring", x, y, { size: 5, life: 0.20, color })); } }, spawnBleedEffect(x, y) { this.effects.push(new Effect("bleed", x + deterministicRange(this, "bleed-effect-x", -3, 3, x, y), y + deterministicRange(this, "bleed-effect-y", -2, 3, x, y), { vx: deterministicRange(this, "bleed-effect-vx", -8, 8, x, y), vy: deterministicRange(this, "bleed-effect-vy", 3, 14, x, y), size: deterministicRange(this, "bleed-effect-size", 2.0, 3.8, x, y), life: deterministicRange(this, "bleed-effect-life", 0.34, 0.62, x, y), color: "rgba(80, 172, 55, 0.78)", })); }, isSleepFurniture(it) { return Boolean(globalThis.TarinaiNestSleepSystem.isSleepFurniture(it)); }, isTarinaiHiddenInNestBox(t) { return Boolean(globalThis.TarinaiNestSleepSystem.isTarinaiHiddenInNestBox(this, t)); }, bedOccupancy(bed) { return globalThis.TarinaiNestSleepSystem.bedOccupancy(this, bed) || 0; }, bedComfort(bed) { return globalThis.TarinaiNestSleepSystem.bedComfort(this, bed) ?? 0.7; }, relationNotice(aId, bId, kind, cooldown = 30) { const key = [aId, bId].sort().join(":") + `:${kind}`; const last = this.relationNotices?.[key] || -Infinity; if (this.time - last < cooldown) return false; if (!this.relationNotices) this.relationNotices = {}; this.relationNotices[key] = this.time; return true; }, applyImpulse(entity, vx = 0, vy = 0, options = {}) { if (!entity || entity.dead) return false; if (!Number.isFinite(vx) || !Number.isFinite(vy)) return false; const impulseMag = Math.hypot(vx, vy); if (entity instanceof Tarinai && (entity.birthRitualTimer || 0) > 0.04) { this.cancelBirthRitualOnForce?.(entity, impulseMag, { reason: options.birthCancelReason || options.thought || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f", target: options.target || null, fear: options.fearTimer ?? 1.0, cause: options.cause || "impulse", panic: options.panic !== false, silentLog: !!options.silentBirthCancel, }); } if (entity instanceof Tarinai || "impulseVx" in entity || "impulseVy" in entity) { entity.impulseVx = (Number.isFinite(entity.impulseVx) ? entity.impulseVx : 0) + vx; entity.impulseVy = (Number.isFinite(entity.impulseVy) ? entity.impulseVy : 0) + vy; } else { entity.vx = (Number.isFinite(entity.vx) ? entity.vx : 0) + vx; entity.vy = (Number.isFinite(entity.vy) ? entity.vy : 0) + vy; } if (options.panic && entity instanceof Tarinai) { if (entity.enterPanic) entity.enterPanic({ target: options.target || null, reason: options.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: options.fearTimer ?? 0.55, wake: true, cause: options.cause || "impulse" }); else { entity.setActionState?.("panic", { target: options.target || null, reason: options.thought || "\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); entity.fearTimer = Math.max(entity.fearTimer || 0, options.fearTimer ?? 0.55); } } return true; }, applyImpactDamage(target, amount, cause = "\u885d\u7a81", options = {}) { if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false; const sourceLabel = (src) => { if (!src) return ""; if (src instanceof Tarinai || (src.name && src.radius && !src.type)) return `\u300C${String(src.name || "\u305F\u308A\u306A\u3044")}\u300D`; if (src.type && typeof toolLabel === "function") return toolLabel(src.type); if (src.type) return String(src.type); if (src.name) return String(src.name); return ""; }; let normalized = options.cause || cause || "\u885d\u7a81"; const rawCause = String(cause || ""); const isPlainCollision = String(normalized).includes("\u885d\u7a81") || String(normalized).includes("collision") || rawCause.includes("\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316"); const label = sourceLabel(options.source); if (isPlainCollision && label) normalized = `${label}\u3068\u306e\u885d\u7a81`; const physicalHit = String(normalized).includes("\u885d\u7a81") || String(normalized).includes("collision") || rawCause.includes("\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316") || String(normalized).includes("\u7206\u7af9") || String(normalized).includes("\u843d\u3061\u3066\u304d\u305f"); if (target instanceof Tarinai && (target.birthRitualTimer || 0) > 0.04 && physicalHit && amount >= (options.birthCancelDamageThreshold ?? 8)) { this.cancelBirthRitualOnForce?.(target, Math.max(180, amount * 18), { reason: "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f", target: options.source || null, fear: 1.0, cause: "impact_damage", panic: true, silentLog: !!options.silentBirthCancel, }); } if (physicalHit) { target.lastPhysicalCollisionDamageAt = this.time || 0; } if (target.damage) target.damage(amount, normalized, options); else if (Number.isFinite(target.hp)) target.hp -= amount; return true; }, maybeDefeatFromFightDamage(victim, attacker, damage = 0) { if (!victim || !attacker || victim.dead || attacker.dead || victim.id === attacker.id) return false; if ((victim.fightTimer || 0) <= 0.04 || (attacker.fightTimer || 0) <= 0.04) return false; if (victim.defeatedById || (victim.defeatedTimer || 0) > 0.04) return false; const profile = victim.personalityProfile ? victim.personalityProfile() : { fight: 1 }; const aggressionHold = clamp((profile.fight || 1) - 1, -0.8, 1.3); const healthRisk = clamp((42 - (victim.energy || 0)) / 42, 0, 1.25); const damageRisk = clamp((damage || 0) / Math.max(2.5, 9 + (victim.energy || 0) * 0.045), 0, 1.4); const stressRisk = clamp(((victim.stress || 0) - 70) / 70, 0, 0.8); const chance = clamp(0.025 + damageRisk * 0.23 + healthRisk * 0.34 + stressRisk * 0.10 - aggressionHold * 0.12, 0.015, 0.78); if (!deterministicChance(this, "fight-defeat", chance, victim, attacker, damage)) return false; victim.defeatedById = attacker.id; victim.fightWinnerId = attacker.id; attacker.fightWinnerId = attacker.id; attacker.defeatedById = null; victim.fightTimer = Math.min(victim.fightTimer || 0, deterministicRange(this, "fight-defeat-victim-timer", 0.20, 0.42, victim, attacker)); attacker.fightTimer = Math.min(attacker.fightTimer || 0, deterministicRange(this, "fight-defeat-attacker-timer", 0.20, 0.42, victim, attacker)); if (victim.enterPanic) victim.enterPanic({ target: attacker, threat: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.1, wake: true, cause: "fight_lost" }); else { victim.setActionState?.("panic", { target: attacker, reason: "\u55a7\u5629\u306b\u8ca0\u3051\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); victim.fearTimer = Math.max(victim.fearTimer || 0, 1.1 * (profile.fear || 1)); } const dx = victim.x - attacker.x; const dy = victim.y - attacker.y; const d = Math.hypot(dx, dy) || 1; victim.vx += dx / d * deterministicRange(this, "fight-defeat-knockback-x", 38, 76, victim, attacker); victim.vy += dy / d * deterministicRange(this, "fight-defeat-knockback-y", 22, 56, victim, attacker); this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); return true; }, resolveFightOutcome(winner, loser) { if (!winner || !loser || winner.dead || loser.dead) return; const key = `${winner.id}:${loser.id}:${Math.floor(this.time / 2)}`; if (!this.resolvedFightIds) this.resolvedFightIds = {}; if (this.resolvedFightIds[key]) return; this.resolvedFightIds[key] = true; winner.defeatedById = null; winner.defeatedTimer = 0; winner.fearTimer = Math.max(0, winner.fearTimer - 0.75); winner.fightTargetIds = []; winner.fightTargetId = null; winner.adjustRelation(loser, -0.6, 0.02, "\u55a7\u5629\u306b\u52dd\u3063\u305f"); loser.adjustRelation(winner, -1.2, 7.2 * loser.personalityProfile().fear, "\u55a7\u5629\u306b\u8ca0\u3051\u305f"); winner.relationTo(loser.id).fightsWon = (winner.relationTo(loser.id).fightsWon || 0) + 1; loser.relationTo(winner.id).fightsLost = (loser.relationTo(winner.id).fightsLost || 0) + 1; winner.totalFightWins = (winner.totalFightWins || 0) + 1; loser.totalFightLosses = (loser.totalFightLosses || 0) + 1; this.maybeApplyTarinaiChampionByFightRecord?.(winner, loser); this.recordFamily?.(winner); this.recordFamily?.(loser); this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8); for (const t of [winner, loser]) { t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this, "fight-outcome-cooldown", 4, 8, t, winner, loser)); clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival"); } winner.adjustPersonality?.("aggression", 0.018, "after winning fights."); loser.adjustPersonality?.("aggression", -0.018, "after losing fights."); this.maybeApplyZunchiSlaveByFightRecord?.(winner, loser); this.maybeApplyZunchiSlaveByFightRecord?.(loser, winner); loser.fearTimer = Math.max(loser.fearTimer, 1.2 * loser.personalityProfile().fear); this.spawnBubble(loser.x, loser.y - loser.radius * 1.28, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)"); winner.affection = clamp(winner.affection + 0.8, 0, 80); if (typeof applyNeedRelief === "function") applyNeedRelief(winner, { safety: -deterministicRange(this, "fight-win-safety-relief", 12, 22, winner, loser), fulfill: -deterministicRange(this, "fight-win-fulfill-relief", 8, 16, winner, loser) }); if (this.relationNotice(winner.id, loser.id, "fight-result", 8)) { this.log(`${loser.name}\u306f${winner.name}\u306b\u8ca0\u3051\u3001\u305d\u306e\u3053\u3068\u3092\u899a\u3048\u305f\u3002`, "fight", { participants: [loser, winner] }); } }, maybeApplyTarinaiChampionByFightRecord(tarinai, other = null) { if (!tarinai || tarinai.dead || tarinai.isTarinaiChampion || tarinai.isZunchiSlave) return false; const wins = Number(tarinai.totalFightWins || 0); const losses = Number(tarinai.totalFightLosses || 0); const total = wins + losses; if (total < 6) return false; const winRate = total > 0 ? wins / total : 0; if (winRate < 0.75) return false; if (!tarinai.becomeTarinaiChampion?.({ fightRecord: true })) return false; this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.35, "\u738B\u8005", "rgba(144,96,24,0.84)"); this.log?.(`${tarinai.name}\u306F\u55A7\u5629${total}\u56DE\u3001\u52DD\u7387${Math.round(winRate * 100)}%\u3067\u305F\u308A\u306A\u3044\u738B\u8005\u306B\u306A\u3063\u305F\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] }); globalThis.TarinaiAchievements?.recordNaturalTarinaiKing?.({ world: this, tarinai, other, wins, losses, total, winRate }); return true; }, maybeApplyZunchiSlaveByFightRecord(tarinai, other = null) { if (!tarinai || tarinai.dead || tarinai.isTarinaiChampion || tarinai.isZunchiSlave) return false; const wins = Number(tarinai.totalFightWins || 0); const losses = Number(tarinai.totalFightLosses || 0); const total = wins + losses; if (total < 5) return false; const winRate = total > 0 ? wins / total : 0; if (winRate > 0.30) return false; const oldName = tarinai.formerName || tarinai.name; if (!tarinai.becomeZunchiSlave?.({ fightRecord: true, locked: true })) return false; this.spawnBubble?.(tarinai.x, tarinai.y - tarinai.radius * 1.20, "\u3076\u308a\u3085\u2026", "rgba(74,91,50,0.78)"); this.log?.(`${oldName}\u306f\u55a7\u5629\u3092${total}\u56de\u7d4c\u9a13\u3057\u3001\u52dd\u738730%\u4ee5\u4e0b\u3067\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] }); globalThis.TarinaiAchievements?.recordNaturalZunchiSlave?.({ world: this, tarinai, other, wins, losses, total, winRate }); return true; }, itemDropImpact(it) { return global.TarinaiImpactResponseSystem.itemDropImpact(this, it); }, limitBallChasers() { if (!this.itemCounts?.ball && !this.itemCounts?.balloon) return; const groups = new Map(); for (const t of this.tarinai || []) { if (!t || t.dead || t.state !== "play_ball" || !t.target || t.target.dead || (t.target.type !== "ball" && t.target.type !== "balloon")) continue; const arr = groups.get(t.target) || []; arr.push(t); groups.set(t.target, arr); } for (const [ball, arr] of groups) { if (arr.length <= 5) continue; arr.sort((a, b) => { const ap = ((a.currentPersonality?.openness || 0) >= 0.5) ? -120 : 0; const bp = ((b.currentPersonality?.openness || 0) >= 0.5) ? -120 : 0; return dist(a, ball) + ap - (dist(b, ball) + bp); }); for (const t of arr.slice(5)) { t.goIdle((ball.type === "balloon" ? "\u6c34\u98a8\u8239" : "\u30dc\u30fc\u30eb") + "\u304c\u6df7\u307f\u5408\u3063\u3066\u3044\u308b\u306e\u3067\u773a\u3081\u3066\u3044\u308b"); t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + deterministicRange(this, "ball-chaser-release-angle", -0.45, 0.45, t, ball); } } }, pokeBall(ball, x, y) { if (!ball || ball.dead || (ball.type !== "ball" && ball.type !== "balloon")) return false; const now = (typeof performance !== "undefined" && performance.now) ? performance.now() / 1000 : (this.time || 0); let dx = ball.x - x; let dy = ball.y - y; let d = Math.hypot(dx, dy); if (d < 0.001) { const a = (ball.lastPokeAngle ?? deterministicAngle(this, "ball-poke-overlap-base", ball, x, y)) + deterministicRange(this, "ball-poke-overlap-jitter", -0.55, 0.55, ball, x, y); dx = Math.cos(a); dy = Math.sin(a); d = 1; } const nx = dx / d; const ny = dy / d; const recent = now - (ball.lastPokedAt || -999) < 0.82; ball.pokeCombo = recent ? Math.min(10, (ball.pokeCombo || 0) + 1) : 1; ball.lastPokedAt = now; ball.lastPokeAngle = Math.atan2(ny, nx); const currentSpeed = Math.hypot(ball.vx || 0, ball.vy || 0); const isBalloon = ball.type === "balloon"; const impulse = (isBalloon ? 88 : 145) + ball.pokeCombo * (isBalloon ? 38 : 64) + Math.min(isBalloon ? 160 : 260, currentSpeed * (isBalloon ? 0.18 : 0.24)); ball.vx = (ball.vx || 0) + nx * impulse; ball.vy = (ball.vy || 0) + ny * impulse; ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * ((isBalloon ? 2.4 : 5.6) + ball.pokeCombo * (isBalloon ? 0.55 : 1.15)), -38, 38); ball.amount = Math.max(ball.amount || 999, 999); audio.ballHit?.(); this.effects.push(new Effect("ring", ball.x, ball.y, { size: Math.max(16, ball.r * (0.95 + ball.pokeCombo * 0.05)), life: 0.22, color: isBalloon ? "rgba(94,188,236,0.56)" : "rgba(255,255,255,0.58)" })); if (!isBalloon) this.log("\u30dc\u30fc\u30eb\u3092\u3064\u3064\u3044\u3066\u8ee2\u304c\u3057\u305f\u3002", "observe", { eventType: "ball_poke" }); return true; }, resolveBallBallCollisions(dt) { return global.TarinaiCollisionResponseSystem.resolveBallBallCollisions(this, dt); }, resolveBallInteractions(dt) { return global.TarinaiCollisionResponseSystem.resolveBallInteractions(this, dt); }, resolveTarinaiHighSpeedCollisions(dt, opts = {}) { return global.TarinaiCollisionResponseSystem.resolveTarinaiHighSpeedCollisions(this, dt, opts); } })); })(typeof window !== "undefined" ? window : globalThis);