2026-06-21 14:09:35 +09:00
|
|
|
"use strict";
|
2026-06-27 19:22:38 +09:00
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
|
|
|
|
|
(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;
|
2026-06-28 13:40:41 +09:00
|
|
|
const candidates = this.nearbyAnts?.(x, y, radius + 24, true) || this.ants || [];
|
|
|
|
|
for (const ant of candidates) {
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
|
|
|
|
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) {
|
2026-06-27 19:22:38 +09:00
|
|
|
const a = deterministicAngle(this, "ant-push-overlap", ant, x, y);
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-28 23:07:40 +09:00
|
|
|
|
|
|
|
|
blastLoosePinsFrom(x, y, blastRadius, source = null, scale = 1, salt = "blast") {
|
|
|
|
|
let blastedPins = 0;
|
|
|
|
|
const pinSystem = globalThis.TarinaiItemDynamicPinSystem;
|
2026-06-29 16:21:58 +09:00
|
|
|
const pins = (this.items || []).filter(it => it && !it.dead && isPinType(it.type));
|
2026-06-28 23:07:40 +09:00
|
|
|
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;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-30 22:30:37 +09:00
|
|
|
spawnFireAt(x, y, opts = {}) {
|
|
|
|
|
return globalThis.TarinaiItemDynamicToolSystem?.spawnFireAt?.(this, x, y, opts) || null;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
scatterFireFrom(it) {
|
|
|
|
|
return globalThis.TarinaiItemDynamicToolSystem?.scatterFireFrom?.(it, this) || false;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
explodeFirecracker(it) {
|
2026-07-03 20:57:02 +09:00
|
|
|
if (!it || it.dead || (it.type !== "firecracker" && it.type !== "sticky_bomb")) return false;
|
2026-06-21 14:09:35 +09:00
|
|
|
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++) {
|
2026-06-27 19:22:38 +09:00
|
|
|
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), {
|
2026-06-21 14:09:35 +09:00
|
|
|
vx: Math.cos(a) * speed,
|
|
|
|
|
vy: Math.sin(a) * speed,
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
2026-06-22 02:03:19 +09:00
|
|
|
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); }
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
2026-06-27 19:22:38 +09:00
|
|
|
if (t.addStress) t.addStress(deterministicRange(this, "firecracker-startle-stress", 8, 15, it, t), { threshold: 8 });
|
2026-06-21 14:09:35 +09:00
|
|
|
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");
|
2026-06-23 18:11:42 +09:00
|
|
|
if (t.addStress) t.addStress(22 * p, { threshold: 8 });
|
2026-06-21 14:09:35 +09:00
|
|
|
t.hurtTimer = Math.max(t.hurtTimer, 1.5 + p * 1.8);
|
|
|
|
|
t.surpriseTimer = Math.max(t.surpriseTimer, 0.85);
|
2026-06-24 13:48:22 +09:00
|
|
|
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 }); }
|
2026-06-21 14:09:35 +09:00
|
|
|
let nx = dx / d;
|
|
|
|
|
let ny = dy / d;
|
|
|
|
|
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
2026-06-27 19:22:38 +09:00
|
|
|
const a = deterministicAngle(this, "drop-impact-overlap-normal", it, t);
|
2026-06-21 14:09:35 +09:00
|
|
|
nx = Math.cos(a);
|
|
|
|
|
ny = Math.sin(a);
|
|
|
|
|
}
|
|
|
|
|
const distanceBoost = p * p * 0.55 + p * 0.45;
|
2026-06-27 19:22:38 +09:00
|
|
|
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), {
|
2026-06-21 14:09:35 +09:00
|
|
|
panic: true,
|
|
|
|
|
target: { x, y },
|
|
|
|
|
fearTimer: 1.7 + p,
|
2026-06-24 13:48:22 +09:00
|
|
|
thought: "\u7206\u7af9\u306e\u7206\u98a8\u3067\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b",
|
2026-06-21 14:09:35 +09:00
|
|
|
});
|
|
|
|
|
t.fallTimer = Math.max(t.fallTimer, 1.45 + p * 1.85);
|
|
|
|
|
t.fallMax = Math.max(t.fallMax || 0, t.fallTimer);
|
2026-06-27 19:22:38 +09:00
|
|
|
t.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "firecracker-fall-dir", it, t);
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-29 16:21:58 +09:00
|
|
|
const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15;
|
2026-06-27 19:22:38 +09:00
|
|
|
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)");
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
2026-06-23 18:11:42 +09:00
|
|
|
if (typeof damageNearbyStructures === "function") damageNearbyStructures(this, x, y, blastRadius, 34 * (it.blastScale || 1), it);
|
2026-06-21 14:09:35 +09:00
|
|
|
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),
|
|
|
|
|
});
|
|
|
|
|
let blastedBalls = 0;
|
2026-07-03 00:45:22 +09:00
|
|
|
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;
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
2026-06-27 19:22:38 +09:00
|
|
|
const a = deterministicAngle(this, "firecracker-ball-overlap", it, ball);
|
2026-06-21 14:09:35 +09:00
|
|
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
|
|
|
|
}
|
2026-07-03 20:57:02 +09:00
|
|
|
if (ball.type === "balloon") {
|
|
|
|
|
globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(ball, this, it);
|
|
|
|
|
blastedBalls += 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
const blast = 430 + p * 960;
|
2026-06-27 19:22:38 +09:00
|
|
|
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));
|
2026-06-28 23:07:40 +09:00
|
|
|
ball.spinVelocity = clamp((ball.spinVelocity || 0) + deterministicRange(this, "firecracker-ball-spin", -24, 24, it, ball), -38, 38);
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-28 23:07:40 +09:00
|
|
|
const blastedPins = this.blastLoosePinsFrom(x, y, blastRadius, it, it.blastScale || 1, "firecracker");
|
2026-06-21 14:09:35 +09:00
|
|
|
audio.explode();
|
2026-06-28 23:07:40 +09:00
|
|
|
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");
|
2026-07-03 20:57:02 +09:00
|
|
|
return true;
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
explodeDiseaseTarinai(t) {
|
|
|
|
|
if (!t || t.dead) return;
|
|
|
|
|
const x = t.x, y = t.y;
|
2026-06-21 22:29:00 +09:00
|
|
|
const blastScale = 0.68;
|
|
|
|
|
const blastRadius = 240 * blastScale;
|
2026-06-21 14:09:35 +09:00
|
|
|
t.explosionDisease = false;
|
|
|
|
|
t.explosionDiseaseTimer = 0;
|
2026-06-21 22:29:00 +09:00
|
|
|
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++) {
|
2026-06-27 19:22:38 +09:00
|
|
|
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, {
|
2026-06-21 22:29:00 +09:00
|
|
|
vx: Math.cos(a) * speed,
|
|
|
|
|
vy: Math.sin(a) * speed,
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 22:29:00 +09:00
|
|
|
color: i % 3 === 0 ? "rgba(255,218,70,0.78)" : "rgba(112,72,42,0.72)",
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-21 22:29:00 +09:00
|
|
|
o.damage(5 + q * 18, "\u7206\u767a\u75c5");
|
2026-06-23 18:11:42 +09:00
|
|
|
if (o.addStress) o.addStress(22 * q, { threshold: 8 });
|
2026-06-21 14:09:35 +09:00
|
|
|
let nx = dx / d;
|
|
|
|
|
let ny = dy / d;
|
|
|
|
|
if (!Number.isFinite(nx) || !Number.isFinite(ny) || Math.abs(nx) + Math.abs(ny) < 0.001) {
|
2026-06-27 19:22:38 +09:00
|
|
|
const a = deterministicAngle(this, "disease-explosion-overlap-normal", t, o);
|
2026-06-21 14:09:35 +09:00
|
|
|
nx = Math.cos(a);
|
|
|
|
|
ny = Math.sin(a);
|
|
|
|
|
}
|
2026-06-21 22:29:00 +09:00
|
|
|
const distanceBoost = q * q * 0.55 + q * 0.45;
|
2026-06-27 19:22:38 +09:00
|
|
|
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), {
|
2026-06-21 14:09:35 +09:00
|
|
|
panic: true,
|
|
|
|
|
target: { x, y },
|
2026-06-21 22:29:00 +09:00
|
|
|
fearTimer: 1.7 + q,
|
2026-06-24 13:48:22 +09:00
|
|
|
thought: "\u7206\u767a\u75c5\u306e\u7206\u767a\u306b\u5dfb\u304d\u8fbc\u307e\u308c\u3066\u3044\u308b",
|
2026-06-21 14:09:35 +09:00
|
|
|
});
|
2026-06-21 22:29:00 +09:00
|
|
|
o.hurtTimer = Math.max(o.hurtTimer || 0, 1.5 + q * 1.8);
|
|
|
|
|
o.surpriseTimer = Math.max(o.surpriseTimer || 0, 0.85);
|
2026-06-24 13:48:22 +09:00
|
|
|
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 }); }
|
2026-06-21 22:29:00 +09:00
|
|
|
o.fallTimer = Math.max(o.fallTimer || 0, 1.45 + q * 1.85);
|
|
|
|
|
o.fallMax = Math.max(o.fallMax || 0, o.fallTimer);
|
2026-06-27 19:22:38 +09:00
|
|
|
o.fallDir = (dx >= 0 ? 1 : -1) * deterministicSigned(this, "disease-explosion-fall-dir", t, o);
|
2026-06-21 22:29:00 +09:00
|
|
|
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);
|
2026-06-29 16:21:58 +09:00
|
|
|
const explosionChance = window.TarinaiDiseaseRegistry.infectionChance("disease_explosion", 0.15) ?? 0.15;
|
2026-06-27 19:22:38 +09:00
|
|
|
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)");
|
2026-06-21 22:29:00 +09:00
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
|
});
|
2026-07-03 00:45:22 +09:00
|
|
|
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;
|
2026-06-21 22:29:00 +09:00
|
|
|
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) {
|
2026-06-27 19:22:38 +09:00
|
|
|
const a = deterministicAngle(this, "disease-explosion-ball-overlap", t, ball);
|
2026-06-21 22:29:00 +09:00
|
|
|
dx = Math.cos(a); dy = Math.sin(a); d = 1;
|
|
|
|
|
}
|
2026-07-03 20:57:02 +09:00
|
|
|
if (ball.type === "balloon") {
|
|
|
|
|
globalThis.TarinaiItemDynamicBallSystem?.popBalloon?.(ball, this, t);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-06-21 22:29:00 +09:00
|
|
|
const blast = 430 + q * 960;
|
2026-06-27 19:22:38 +09:00
|
|
|
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);
|
2026-06-21 22:29:00 +09:00
|
|
|
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)" }));
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
2026-06-28 23:07:40 +09:00
|
|
|
this.blastLoosePinsFrom(x, y, blastRadius, t, blastScale, "disease-explosion");
|
2026-06-21 14:09:35 +09:00
|
|
|
t.die("\u7206\u767a\u75c5");
|
|
|
|
|
audio.explode();
|
|
|
|
|
this.log(`${t.name}\u306f\u7206\u767a\u75c5\u3067\u7206\u767a\u3057\u305f\u3002`, "accident", { participants: [t] });
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-28 23:07:40 +09:00
|
|
|
waterHoseRadius() {
|
|
|
|
|
return 138;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
applyWaterHose(x, y, dx = 0, dy = 0, dt = 0.08) {
|
2026-06-28 23:07:40 +09:00
|
|
|
const radius = this.waterHoseRadius?.() || 138;
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-26 19:04:32 +09:00
|
|
|
} 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));
|
2026-06-28 23:07:40 +09:00
|
|
|
if (it.amount <= 0.5) {
|
|
|
|
|
it.amount = 0;
|
|
|
|
|
it.hp = 0;
|
|
|
|
|
it.growth = 0;
|
|
|
|
|
it.grassStage = -1;
|
|
|
|
|
}
|
2026-06-26 19:04:32 +09:00
|
|
|
cleaned += p * 0.72;
|
2026-07-01 16:55:49 +09:00
|
|
|
} 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;
|
2026-06-26 19:04:32 +09:00
|
|
|
} else if (it.type === "grass_bed") {
|
|
|
|
|
it.amount = 0;
|
|
|
|
|
it.hp = 0;
|
2026-06-29 16:21:58 +09:00
|
|
|
global.TarinaiStructureLifecycle.deleteItem(this, it, { reason: "water-hose-grass-bed", userReason: "\u6d17\u6d44\u3067\u6d41\u3055\u308c\u305f", wake: true });
|
2026-06-26 19:04:32 +09:00
|
|
|
cleaned += p * 0.95;
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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);
|
2026-06-27 19:22:38 +09:00
|
|
|
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);
|
2026-06-21 14:09:35 +09:00
|
|
|
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.24);
|
|
|
|
|
t.applyWaterEffect?.(dt * (1.0 + p * 1.8), "hose");
|
2026-07-01 16:55:49 +09:00
|
|
|
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;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
2026-06-27 19:22:38 +09:00
|
|
|
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)" }));
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
if (cleaned > 0.25) {
|
|
|
|
|
this.drawListDirty = true;
|
2026-06-26 19:04:32 +09:00
|
|
|
this.markItemBucketsDirty?.("water-hose-clean");
|
|
|
|
|
this.markTerrainDirtyAt?.(x, y, radius + 84, "water-hose-clean");
|
|
|
|
|
this.markSpatialDirty?.("water-hose-clean");
|
2026-06-28 23:07:40 +09:00
|
|
|
this.compactItems?.();
|
2026-06-21 14:09:35 +09:00
|
|
|
this.updateItemCounts();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-22 02:03:19 +09:00
|
|
|
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);
|
2026-07-09 16:08:11 +09:00
|
|
|
let spawned = 0;
|
2026-06-22 02:03:19 +09:00
|
|
|
for (let i = 0; i < n; i++) {
|
2026-06-27 19:22:38 +09:00
|
|
|
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 it = new Item("zunchi", clamp(sx + deterministicRange(this, "zunchi-burst-x", -8, 8, source, pin, i), 40, this.w - 40), clamp(sy + deterministicRange(this, "zunchi-burst-y", -8, 8, source, pin, i), 40, this.h - 40));
|
2026-06-22 02:03:19 +09:00
|
|
|
it.amount = 240;
|
2026-06-27 19:22:38 +09:00
|
|
|
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);
|
2026-06-22 02:03:19 +09:00
|
|
|
it.stage = "fresh";
|
2026-07-09 16:08:11 +09:00
|
|
|
if (this.addItem?.(it, "zunchi-burst")) spawned += 1;
|
2026-06-22 02:03:19 +09:00
|
|
|
}
|
2026-07-09 16:08:11 +09:00
|
|
|
if (!spawned) return 0;
|
2026-06-22 02:03:19 +09:00
|
|
|
this.effects?.push(new Effect("zunchi_miasma", sx, sy, { size: 28, life: 0.65, color: "rgba(74,91,50,0.48)" }));
|
2026-06-24 13:48:22 +09:00
|
|
|
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] : [] });
|
2026-06-22 02:03:19 +09:00
|
|
|
audio.place?.("zunchi");
|
2026-06-24 13:48:22 +09:00
|
|
|
this.ensureSpatial?.("zunchi-burst");
|
2026-07-09 16:08:11 +09:00
|
|
|
return spawned;
|
2026-06-22 02:03:19 +09:00
|
|
|
},
|
|
|
|
|
|
2026-06-24 18:49:17 +09:00
|
|
|
spawnZunchi(x, y, source = null) {
|
2026-06-21 14:09:35 +09:00
|
|
|
const it = new Item("zunchi", clamp(x, 40, this.w - 40), clamp(y, 40, this.h - 40));
|
|
|
|
|
it.amount = 260;
|
2026-06-24 18:49:17 +09:00
|
|
|
it.stage = "fresh";
|
|
|
|
|
it.spawnGrace = Math.max(it.spawnGrace || 0, 10.0);
|
|
|
|
|
it.eatProtectedUntil = (this.time || 0) + 90;
|
2026-07-09 16:08:11 +09:00
|
|
|
const added = this.addItem?.(it, "zunchi-spawn");
|
|
|
|
|
if (!added) return null;
|
2026-06-21 14:09:35 +09:00
|
|
|
audio.place?.("zunchi");
|
2026-07-09 16:08:11 +09:00
|
|
|
return added;
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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, {
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 14:09:35 +09:00
|
|
|
size: 8,
|
|
|
|
|
text,
|
|
|
|
|
color,
|
|
|
|
|
}));
|
|
|
|
|
this.effectCounts.bubble = (this.effectCounts.bubble || 0) + 1;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
spawnHeadbuttEffect(x, y) {
|
|
|
|
|
this.effects.push(new Effect("fight", x, y, {
|
2026-06-27 19:22:38 +09:00
|
|
|
size: deterministicRange(this, "headbutt-effect-size", 10, 16, x, y),
|
|
|
|
|
life: deterministicRange(this, "headbutt-effect-life", 0.20, 0.34, x, y),
|
2026-06-21 14:09:35 +09:00
|
|
|
color: "rgba(105, 62, 34, 0.86)",
|
|
|
|
|
}));
|
2026-06-27 19:22:38 +09:00
|
|
|
if (deterministicChance(this, "headbutt-audio", 0.5, x, y)) audio.fight();
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
spawnFallEffect(x, y, scale = 1) {
|
|
|
|
|
if ((this.effectCounts.fall || 0) > 18) return;
|
|
|
|
|
this.effects.push(new Effect("fall", x, y, {
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 14:09:35 +09:00
|
|
|
color: "rgba(154, 124, 80, 0.58)",
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
spawnEatEffect(x, y, color = "#f1dfb7") {
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 14:09:35 +09:00
|
|
|
color,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-06-27 19:22:38 +09:00
|
|
|
if (deterministicChance(this, "eat-ring", 0.08, x, y, color)) {
|
2026-06-21 14:09:35 +09:00
|
|
|
this.effects.push(new Effect("ring", x, y, { size: 5, life: 0.20, color }));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
spawnBleedEffect(x, y) {
|
2026-06-27 19:22:38 +09:00
|
|
|
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),
|
2026-06-21 14:09:35 +09:00
|
|
|
color: "rgba(80, 172, 55, 0.78)",
|
|
|
|
|
}));
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
isSleepFurniture(it) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return Boolean(globalThis.TarinaiNestSleepSystem.isSleepFurniture(it));
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
isTarinaiHiddenInNestBox(t) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return Boolean(globalThis.TarinaiNestSleepSystem.isTarinaiHiddenInNestBox(this, t));
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
bedOccupancy(bed) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return globalThis.TarinaiNestSleepSystem.bedOccupancy(this, bed) || 0;
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
bedComfort(bed) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return globalThis.TarinaiNestSleepSystem.bedComfort(this, bed) ?? 0.7;
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
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;
|
2026-06-27 19:22:38 +09:00
|
|
|
const impulseMag = Math.hypot(vx, vy);
|
|
|
|
|
if (entity instanceof Tarinai && (entity.birthRitualTimer || 0) > 0.04) {
|
|
|
|
|
this.cancelBirthRitualOnForce?.(entity, impulseMag, {
|
2026-06-28 13:40:41 +09:00
|
|
|
reason: options.birthCancelReason || options.thought || "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
2026-06-27 19:22:38 +09:00
|
|
|
target: options.target || null,
|
|
|
|
|
fear: options.fearTimer ?? 1.0,
|
|
|
|
|
cause: options.cause || "impulse",
|
|
|
|
|
panic: options.panic !== false,
|
|
|
|
|
silentLog: !!options.silentBirthCancel,
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
2026-06-24 13:48:22 +09:00
|
|
|
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); }
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
applyImpactDamage(target, amount, cause = "\u885d\u7a81", options = {}) {
|
|
|
|
|
if (!target || target.dead || !Number.isFinite(amount) || amount <= 0) return false;
|
2026-06-27 21:50:05 +09:00
|
|
|
const sourceLabel = (src) => {
|
|
|
|
|
if (!src) return "";
|
2026-06-30 22:30:37 +09:00
|
|
|
if (src instanceof Tarinai || (src.name && src.radius && !src.type)) return `\u300C${String(src.name || "\u305F\u308A\u306A\u3044")}\u300D`;
|
2026-06-27 21:50:05 +09:00
|
|
|
if (src.type && typeof toolLabel === "function") return toolLabel(src.type);
|
|
|
|
|
if (src.type) return String(src.type);
|
2026-06-28 23:07:40 +09:00
|
|
|
if (src.name) return String(src.name);
|
2026-06-27 21:50:05 +09:00
|
|
|
return "";
|
|
|
|
|
};
|
|
|
|
|
let normalized = options.cause || cause || "\u885d\u7a81";
|
|
|
|
|
const rawCause = String(cause || "");
|
2026-06-28 13:40:41 +09:00
|
|
|
const isPlainCollision = String(normalized).includes("\u885d\u7a81") || String(normalized).includes("collision") || rawCause.includes("\u6025\u6fc0\u306a\u901f\u5ea6\u5909\u5316");
|
2026-06-27 21:50:05 +09:00
|
|
|
const label = sourceLabel(options.source);
|
2026-06-28 13:40:41 +09:00
|
|
|
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");
|
2026-06-27 19:22:38 +09:00
|
|
|
if (target instanceof Tarinai && (target.birthRitualTimer || 0) > 0.04 && physicalHit && amount >= (options.birthCancelDamageThreshold ?? 8)) {
|
|
|
|
|
this.cancelBirthRitualOnForce?.(target, Math.max(180, amount * 18), {
|
2026-06-28 13:40:41 +09:00
|
|
|
reason: "\u5f37\u3044\u885d\u6483\u3067\u7e41\u6b96\u304c\u4e2d\u65ad\u3055\u308c\u305f",
|
2026-06-27 19:22:38 +09:00
|
|
|
target: options.source || null,
|
|
|
|
|
fear: 1.0,
|
|
|
|
|
cause: "impact_damage",
|
|
|
|
|
panic: true,
|
|
|
|
|
silentLog: !!options.silentBirthCancel,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (physicalHit) {
|
2026-06-26 19:04:32 +09:00
|
|
|
target.lastPhysicalCollisionDamageAt = this.time || 0;
|
|
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
if (target.damage) target.damage(amount, normalized);
|
|
|
|
|
else if (Number.isFinite(target.hp)) target.hp -= amount;
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 16:26:12 +09:00
|
|
|
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);
|
2026-06-27 19:22:38 +09:00
|
|
|
if (!deterministicChance(this, "fight-defeat", chance, victim, attacker, damage)) return false;
|
2026-06-21 16:26:12 +09:00
|
|
|
victim.defeatedById = attacker.id;
|
|
|
|
|
victim.fightWinnerId = attacker.id;
|
|
|
|
|
attacker.fightWinnerId = attacker.id;
|
|
|
|
|
attacker.defeatedById = null;
|
2026-06-27 19:22:38 +09:00
|
|
|
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));
|
2026-06-24 13:48:22 +09:00
|
|
|
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)); }
|
2026-06-21 16:26:12 +09:00
|
|
|
const dx = victim.x - attacker.x;
|
|
|
|
|
const dy = victim.y - attacker.y;
|
|
|
|
|
const d = Math.hypot(dx, dy) || 1;
|
2026-06-27 19:22:38 +09:00
|
|
|
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);
|
2026-06-21 22:29:00 +09:00
|
|
|
this.spawnBubble?.(victim.x, victim.y - victim.radius * 1.25, "\u306f\u3041\u3041\u3041\uff01", "rgba(84,66,86,0.80)");
|
2026-06-21 16:26:12 +09:00
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
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;
|
2026-06-28 13:40:41 +09:00
|
|
|
this.maybeApplyTarinaiChampionByFightRecord?.(winner, loser);
|
|
|
|
|
this.recordFamily?.(winner);
|
|
|
|
|
this.recordFamily?.(loser);
|
2026-06-25 22:30:35 +09:00
|
|
|
this.markFightPairCooldown?.(winner, loser, CONFIG.fightCooldown + 8);
|
|
|
|
|
for (const t of [winner, loser]) {
|
2026-06-27 19:22:38 +09:00
|
|
|
t.fightCooldown = Math.max(t.fightCooldown || 0, CONFIG.fightCooldown + deterministicRange(this, "fight-outcome-cooldown", 4, 8, t, winner, loser));
|
2026-06-29 16:21:58 +09:00
|
|
|
clearForcedBehaviorQueue(t, e => e && e.id === "fight_rival");
|
2026-06-25 22:30:35 +09:00
|
|
|
}
|
2026-06-21 14:09:35 +09:00
|
|
|
winner.adjustPersonality?.("aggression", 0.018, "after winning fights.");
|
|
|
|
|
loser.adjustPersonality?.("aggression", -0.018, "after losing fights.");
|
2026-06-21 22:29:00 +09:00
|
|
|
this.maybeApplyZunchiSlaveByFightRecord?.(winner, loser);
|
|
|
|
|
this.maybeApplyZunchiSlaveByFightRecord?.(loser, winner);
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-06-27 19:22:38 +09:00
|
|
|
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) });
|
2026-06-21 14:09:35 +09:00
|
|
|
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] });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-28 13:40:41 +09:00
|
|
|
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;
|
2026-06-30 22:30:37 +09:00
|
|
|
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] });
|
2026-06-28 13:40:41 +09:00
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 22:29:00 +09:00
|
|
|
maybeApplyZunchiSlaveByFightRecord(tarinai, other = null) {
|
2026-06-28 13:40:41 +09:00
|
|
|
if (!tarinai || tarinai.dead || tarinai.isTarinaiChampion || tarinai.isZunchiSlave) return false;
|
2026-06-21 22:29:00 +09:00
|
|
|
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;
|
2026-06-22 02:03:19 +09:00
|
|
|
if (winRate > 0.30) return false;
|
2026-06-21 22:29:00 +09:00
|
|
|
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\u30925\u56de\u7d4c\u9a13\u3057\u3001\u52dd\u738750%\u4ee5\u4e0b\u3067\u305a\u3093\u3061\u3069\u308c\u3044\u306b\u306a\u3063\u305f\u3002`, "fight", { participants: other ? [tarinai, other] : [tarinai] });
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2026-06-21 14:09:35 +09:00
|
|
|
itemDropImpact(it) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return global.TarinaiImpactResponseSystem.itemDropImpact(this, it);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
limitBallChasers() {
|
2026-07-03 00:45:22 +09:00
|
|
|
if (!this.itemCounts?.ball && !this.itemCounts?.balloon) return;
|
2026-06-21 14:09:35 +09:00
|
|
|
const groups = new Map();
|
|
|
|
|
for (const t of this.tarinai || []) {
|
2026-07-03 00:45:22 +09:00
|
|
|
if (!t || t.dead || t.state !== "play_ball" || !t.target || t.target.dead || (t.target.type !== "ball" && t.target.type !== "balloon")) continue;
|
2026-06-21 14:09:35 +09:00
|
|
|
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)) {
|
2026-07-03 00:45:22 +09:00
|
|
|
t.goIdle((ball.type === "balloon" ? "水風船" : "ボール") + "が混み合っているので眺めている");
|
2026-06-27 19:22:38 +09:00
|
|
|
t.wanderAngle = Math.atan2(t.y - ball.y, t.x - ball.x) + deterministicRange(this, "ball-chaser-release-angle", -0.45, 0.45, t, ball);
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
pokeBall(ball, x, y) {
|
2026-07-03 00:45:22 +09:00
|
|
|
if (!ball || ball.dead || (ball.type !== "ball" && ball.type !== "balloon")) return false;
|
2026-06-21 14:09:35 +09:00
|
|
|
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) {
|
2026-06-27 19:22:38 +09:00
|
|
|
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);
|
2026-06-21 14:09:35 +09:00
|
|
|
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);
|
2026-07-03 00:45:22 +09:00
|
|
|
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));
|
2026-06-21 14:09:35 +09:00
|
|
|
ball.vx = (ball.vx || 0) + nx * impulse;
|
|
|
|
|
ball.vy = (ball.vy || 0) + ny * impulse;
|
2026-07-03 00:45:22 +09:00
|
|
|
ball.spinVelocity = clamp((ball.spinVelocity || 0) + (nx >= 0 ? 1 : -1) * ((isBalloon ? 2.4 : 5.6) + ball.pokeCombo * (isBalloon ? 0.55 : 1.15)), -38, 38);
|
2026-06-21 14:09:35 +09:00
|
|
|
ball.amount = Math.max(ball.amount || 999, 999);
|
|
|
|
|
audio.ballHit?.();
|
2026-07-03 00:45:22 +09:00
|
|
|
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)" }));
|
2026-07-05 18:01:36 +09:00
|
|
|
if (!isBalloon) this.log("ボールをつついて転がした。", "observe", { eventType: "ball_poke" });
|
2026-06-21 14:09:35 +09:00
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
resolveBallBallCollisions(dt) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return global.TarinaiCollisionResponseSystem.resolveBallBallCollisions(this, dt);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
resolveBallInteractions(dt) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return global.TarinaiCollisionResponseSystem.resolveBallInteractions(this, dt);
|
2026-06-21 14:09:35 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
resolveTarinaiHighSpeedCollisions(dt) {
|
2026-06-29 16:21:58 +09:00
|
|
|
return global.TarinaiCollisionResponseSystem.resolveTarinaiHighSpeedCollisions(this, dt);
|
2026-06-21 14:09:35 +09:00
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|