qqq
This commit is contained in:
parent
acf93ec6df
commit
ee22e1a929
49 changed files with 1380 additions and 338 deletions
|
|
@ -1,10 +1,20 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: entity-runtime/items/dynamic-system
|
||||
// Owns ball item motion and obstacle bounce behavior. Item.prototype ball
|
||||
// methods delegate to this system.
|
||||
// Owns ball-like item motion, balloon drift/pop behavior, and fan wind.
|
||||
// Item.prototype ball/balloon/fan methods delegate to this system.
|
||||
(function (global) {
|
||||
function isBallLike(item) {
|
||||
return !!item && !item.dead && (item.type === "ball" || item.type === "balloon");
|
||||
}
|
||||
|
||||
function update(item, dt, worldRef) {
|
||||
if (!isBallLike(item)) return false;
|
||||
if (item.type === "balloon") return updateBalloon(item, dt, worldRef);
|
||||
return updateBall(item, dt, worldRef);
|
||||
}
|
||||
|
||||
function updateBall(item, dt, worldRef) {
|
||||
if (!item || item.dead || item.type !== "ball") return false;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
|
|
@ -32,14 +42,58 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
function updateBalloon(item, dt, worldRef) {
|
||||
if (!item || item.dead || item.type !== "balloon") return false;
|
||||
const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
|
||||
// A balloon should never be completely inert: it slowly wobbles, then fans
|
||||
// and collisions can turn that small drift into visible motion.
|
||||
const phase = (worldRef?.time || 0) * 1.35 + (item.seed || 0) * 0.017 + (item.balloonWobble || 0);
|
||||
item.vx = (item.vx || 0) + Math.cos(phase * 0.83) * 6.5 * frameDt + Math.sin(phase * 0.31) * 2.4 * frameDt;
|
||||
item.vy = (item.vy || 0) + Math.sin(phase * 0.71) * 5.4 * frameDt;
|
||||
|
||||
const maxSpeed = 560;
|
||||
const speedBefore = Math.hypot(item.vx || 0, item.vy || 0);
|
||||
if (speedBefore > maxSpeed) {
|
||||
const s = maxSpeed / speedBefore;
|
||||
item.vx *= s;
|
||||
item.vy *= s;
|
||||
}
|
||||
|
||||
const moving = Math.hypot(item.vx || 0, item.vy || 0);
|
||||
if (moving > 0.04) {
|
||||
item.x += (item.vx || 0) * dt;
|
||||
item.y += (item.vy || 0) * dt;
|
||||
const p = Math.max(28, CONFIG.worldPadding || 30);
|
||||
if (item.x < p) { item.x = p; item.vx = Math.abs(item.vx || 0) * 0.58; item.spinVelocity *= -0.44; }
|
||||
if (item.x > worldRef.w - p) { item.x = worldRef.w - p; item.vx = -Math.abs(item.vx || 0) * 0.58; item.spinVelocity *= -0.44; }
|
||||
if (item.y < p) { item.y = p; item.vy = Math.abs(item.vy || 0) * 0.58; item.spinVelocity *= -0.44; }
|
||||
if (item.y > worldRef.h - p) { item.y = worldRef.h - p; item.vy = -Math.abs(item.vy || 0) * 0.58; item.spinVelocity *= -0.44; }
|
||||
resolveObstacleCollisions(item, dt, worldRef);
|
||||
item.x = clamp(item.x, p, worldRef.w - p);
|
||||
item.y = clamp(item.y, p, worldRef.h - p);
|
||||
const airFriction = Math.pow(0.47, frameDt);
|
||||
item.vx *= airFriction;
|
||||
item.vy *= airFriction;
|
||||
}
|
||||
|
||||
checkBalloonHazards(item, worldRef);
|
||||
item.spin = (item.spin || 0) + (item.spinVelocity || 0) * frameDt + (item.vx || 0) * frameDt / Math.max(12, item.r || 20) * 0.42;
|
||||
item.spinVelocity *= Math.pow(0.58, frameDt);
|
||||
if (Math.hypot(item.vx || 0, item.vy || 0) < 0.08) { item.vx = 0; item.vy = 0; }
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveObstacleCollisions(item, dt, worldRef) {
|
||||
if (!worldRef?.nearbyItems) return;
|
||||
const speed = Math.hypot(item.vx || 0, item.vy || 0);
|
||||
const searchRadius = Math.max(150, (item.r || 18) + speed * Math.max(dt || 0.016, 0.016) + 120);
|
||||
const items = worldRef.nearbyItems(item.x, item.y, searchRadius) || [];
|
||||
const seen = new Set(items);
|
||||
// \u5927\u304d\u306a\u6a5f\u69cb\u306f\u4e2d\u5fc3\u304c\u8fd1\u508d\u30bb\u30eb\u5916\u3067\u3082\u5f53\u305f\u308b\u53ef\u80fd\u6027\u304c\u3042\u308b\u3002
|
||||
// \u4e2d\u5fc3\u3067\u306f\u306a\u304f\u5916\u63a5\u534a\u5f84\u3067\u8ffd\u52a0\u8d70\u67fb\u3057\u3066\u3001\u9577\u3044\u68d2\u30fb\u5f80\u5fa9\u5e45\u306b\u3082\u885d\u7a81\u3055\u305b\u308b\u3002
|
||||
// 大きな機構は中心が近傍セル外でも当たる可能性がある。
|
||||
// 中心ではなく外接半径で追加走査して、長い棒・往復幅にも衝突させる。
|
||||
if (Array.isArray(worldRef.items)) {
|
||||
for (const it of worldRef.items) {
|
||||
if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem.isMechanicalType(it.type) || seen.has(it)) continue;
|
||||
|
|
@ -55,7 +109,14 @@
|
|||
if (it.type === "bed") {
|
||||
applyHayDrag(item, it, dt, worldRef);
|
||||
} else if (it.type === "stone") {
|
||||
resolveCircleBounce(item, it, (it.r || 20) * 1.08 + (item.r || 18), 0.82, worldRef);
|
||||
resolveCircleBounce(item, it, (it.r || 20) * 1.08 + (item.r || 18), item.type === "balloon" ? 0.48 : 0.82, worldRef);
|
||||
} else if (it.type === "fan" && item.type === "balloon") {
|
||||
const wind = fanWindAt(it, item.x, item.y);
|
||||
if (wind) {
|
||||
const a = itemAngleFor(it);
|
||||
item.vx += Math.cos(a) * 38 * wind.falloff * Math.max(0.016, dt || 0.016);
|
||||
item.vy += Math.sin(a) * 38 * wind.falloff * Math.max(0.016, dt || 0.016);
|
||||
}
|
||||
}
|
||||
const rects = worldRef.solidObstacleRects ? worldRef.solidObstacleRects(it) : [];
|
||||
if (!rects.length) continue;
|
||||
|
|
@ -70,7 +131,7 @@
|
|||
const dy = item.y - bed.y;
|
||||
const inside = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1;
|
||||
if (!inside) return;
|
||||
const slow = Math.pow(0.16, Math.max(0.016, dt || 0.016));
|
||||
const slow = Math.pow(item.type === "balloon" ? 0.28 : 0.16, Math.max(0.016, dt || 0.016));
|
||||
item.vx *= slow;
|
||||
item.vy *= slow;
|
||||
item.spinVelocity *= Math.pow(0.24, Math.max(0.016, dt || 0.016));
|
||||
|
|
@ -119,12 +180,14 @@
|
|||
item.vx = (item.vx || 0) - (1 + restitution) * toward * nx;
|
||||
item.vy = (item.vy || 0) - (1 + restitution) * toward * ny;
|
||||
} else {
|
||||
item.vx = (item.vx || 0) + nx * 24;
|
||||
item.vy = (item.vy || 0) + ny * 24;
|
||||
const push = item.type === "balloon" ? 12 : 24;
|
||||
item.vx = (item.vx || 0) + nx * push;
|
||||
item.vy = (item.vy || 0) + ny * push;
|
||||
}
|
||||
item.vx *= 0.96;
|
||||
item.vy *= 0.96;
|
||||
item.spinVelocity = clamp((item.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 5.5, -38, 38);
|
||||
const damp = item.type === "balloon" ? 0.84 : 0.96;
|
||||
item.vx *= damp;
|
||||
item.vy *= damp;
|
||||
item.spinVelocity = clamp((item.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * (item.type === "balloon" ? 2.2 : 5.5), -38, 38);
|
||||
emitBounce(item, worldRef, obstacle);
|
||||
}
|
||||
|
||||
|
|
@ -133,13 +196,13 @@
|
|||
const bounceCircleOffRect = global.TarinaiPhysics?.bounceCircleOffRect;
|
||||
if (typeof bounceCircleOffRect !== "function") return;
|
||||
const hitRadius = (item.r || 18) + 2;
|
||||
const restitution = Number.isFinite(rect.restitution) ? rect.restitution : 0.78;
|
||||
const velocityDamp = rect.bounce ? 1.0 : 0.94;
|
||||
const restitution = Number.isFinite(rect.restitution) ? rect.restitution : (item.type === "balloon" ? 0.54 : 0.78);
|
||||
const velocityDamp = rect.bounce ? 1.0 : (item.type === "balloon" ? 0.82 : 0.94);
|
||||
const minBounceSpeed = Math.max(0, Number(rect.minBounceSpeed || 0) || 0);
|
||||
const bounced = bounceCircleOffRect(item, rect, hitRadius, restitution, worldRef, {
|
||||
minBounceSpeed: rect.bounce ? minBounceSpeed : 0,
|
||||
fallbackImpulse: 18,
|
||||
spinKick: 6.2,
|
||||
fallbackImpulse: item.type === "balloon" ? 10 : 18,
|
||||
spinKick: item.type === "balloon" ? 2.4 : 6.2,
|
||||
spinLimit: 38,
|
||||
});
|
||||
if (!bounced) return;
|
||||
|
|
@ -152,12 +215,343 @@
|
|||
const now = worldRef?.time || 0;
|
||||
if ((item.lastObstacleBounceAt || -999) + 0.08 > now) return;
|
||||
item.lastObstacleBounceAt = now;
|
||||
const color = (obstacle?.type === "bounce_fence" || obstacle?.type === "bounce_fence_v") ? "rgba(82,153,230,0.46)" : (obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)");
|
||||
const color = item.type === "balloon" ? "rgba(248,122,166,0.36)" : ((obstacle?.type === "bounce_fence" || obstacle?.type === "bounce_fence_v") ? "rgba(82,153,230,0.46)" : (obstacle?.type === "stone" ? "rgba(165,165,150,0.45)" : "rgba(160,116,64,0.42)"));
|
||||
worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(13, (item.r || 18) * 0.82), life: 0.18, color }));
|
||||
}
|
||||
|
||||
function normalizeAngleLocal(angle = 0, fallback = 0) {
|
||||
if (typeof normalizedItemAngle === "function") return normalizedItemAngle(angle, fallback);
|
||||
const twoPi = Math.PI * 2;
|
||||
const n = Number(angle);
|
||||
const base = Number.isFinite(n) ? n : (Number.isFinite(Number(fallback)) ? Number(fallback) : 0);
|
||||
return ((base % twoPi) + twoPi) % twoPi;
|
||||
}
|
||||
|
||||
function updateFanSwing(item, dt) {
|
||||
if (!item || item.dead || item.type !== "fan") return false;
|
||||
if (!item.fanSwingOn) {
|
||||
item.fanSwingCenter = Number.isFinite(Number(item.fanSwingCenter)) ? Number(item.fanSwingCenter) : itemAngleFor(item);
|
||||
return false;
|
||||
}
|
||||
const center = normalizeAngleLocal(item.fanSwingCenter, itemAngleFor(item));
|
||||
const range = clamp(Number(item.fanSwingRange ?? (35 * Math.PI / 180)) || 0, 0, 150 * Math.PI / 180);
|
||||
const speed = clamp(Number(item.fanSwingSpeed ?? (48 * Math.PI / 180)) || 0, 0, 360 * Math.PI / 180);
|
||||
item.fanSwingCenter = center;
|
||||
item.fanSwingRange = range;
|
||||
item.fanSwingSpeed = speed;
|
||||
item.fanSwingPhase = Number(item.fanSwingPhase || 0) + speed * Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
item.angle = normalizeAngleLocal(center + Math.sin(item.fanSwingPhase || 0) * range, center);
|
||||
return true;
|
||||
}
|
||||
|
||||
function fanWindAt(fan, x, y, opts = {}) {
|
||||
if (!fan || fan.dead || fan.type !== "fan") return null;
|
||||
const range = Math.max(80, Number(opts.range || 300) || 300);
|
||||
const spread = Math.max(0.18, Number(opts.spread || 0.62) || 0.62);
|
||||
const dx = x - fan.x;
|
||||
const dy = y - fan.y;
|
||||
const angle = itemAngleFor(fan);
|
||||
const ax = Math.cos(angle);
|
||||
const ay = Math.sin(angle);
|
||||
const along = dx * ax + dy * ay;
|
||||
if (along < 8 || along > range) return null;
|
||||
const lateral = Math.abs(-dx * ay + dy * ax);
|
||||
const halfWidth = 26 + along * Math.tan(spread);
|
||||
if (lateral > halfWidth) return null;
|
||||
const distFalloff = clamp(1 - along / range, 0, 1);
|
||||
const edgeFalloff = clamp(1 - lateral / Math.max(1, halfWidth), 0, 1);
|
||||
const falloff = Math.pow(distFalloff, 0.64) * Math.pow(edgeFalloff, 0.34);
|
||||
if (falloff <= 0.02) return null;
|
||||
return { angle, along, lateral, halfWidth, range, falloff };
|
||||
}
|
||||
|
||||
function updateFan(item, dt, worldRef) {
|
||||
if (!item || item.dead || item.type !== "fan") return false;
|
||||
const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
const now = worldRef?.time || 0;
|
||||
item.prevX = item.x;
|
||||
item.prevY = item.y;
|
||||
item.fanSpin = (item.fanSpin || 0) + 11.5 * frameDt;
|
||||
item.fanPulse = (item.fanPulse || 0) + 2.2 * frameDt;
|
||||
updateFanSwing(item, frameDt);
|
||||
|
||||
const angle = itemAngleFor(item);
|
||||
const ax = Math.cos(angle);
|
||||
const ay = Math.sin(angle);
|
||||
const range = 320;
|
||||
const searchRadius = range + 40;
|
||||
const nearbyItems = worldRef?.nearbyItems?.(item.x + ax * range * 0.45, item.y + ay * range * 0.45, searchRadius) || [];
|
||||
for (const target of nearbyItems) {
|
||||
if (!target || target.dead || target === item) continue;
|
||||
const wind = fanWindAt(item, target.x, target.y, { range });
|
||||
if (!wind) continue;
|
||||
applyFanToItem(item, target, wind, frameDt, worldRef);
|
||||
}
|
||||
|
||||
const nearbyTarinai = worldRef?.nearbyTarinai?.(item.x + ax * range * 0.45, item.y + ay * range * 0.45, searchRadius) || [];
|
||||
for (const t of nearbyTarinai) {
|
||||
if (!t || t.dead || worldRef?.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const wind = fanWindAt(item, t.x, t.y, { range });
|
||||
if (!wind) continue;
|
||||
applyFanToTarinai(item, t, wind, frameDt, worldRef);
|
||||
}
|
||||
|
||||
if ((item.lastFanEffectAt || -999) + 0.18 < now) {
|
||||
item.lastFanEffectAt = now;
|
||||
const phase = Math.sin(item.fanPulse || 0);
|
||||
const px = item.x + ax * (42 + 24 * phase);
|
||||
const py = item.y + ay * (42 + 24 * phase);
|
||||
worldRef?.effects?.push(new Effect("ring", px, py, { size: 12 + 7 * Math.abs(phase), life: 0.18, color: "rgba(174,214,236,0.26)" }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyFanToItem(fan, target, wind, dt, worldRef) {
|
||||
const type = target.type || "";
|
||||
let massFactor = 0;
|
||||
if (type === "balloon") massFactor = 1.85;
|
||||
else if (type === "ball") massFactor = 0.72;
|
||||
else if (type === "pushpin" || type === "oshibyo") massFactor = 0.90;
|
||||
else if (type === "water" || type === "zunda_juice" || type === "mercury") massFactor = 0.42;
|
||||
else if (type === "zunchi") massFactor = 0.30;
|
||||
else if (type === "fire") massFactor = 0.18;
|
||||
else return;
|
||||
|
||||
const force = 236 * wind.falloff * massFactor;
|
||||
const ax = Math.cos(wind.angle);
|
||||
const ay = Math.sin(wind.angle);
|
||||
target.prevX = Number.isFinite(target.prevX) ? target.prevX : target.x;
|
||||
target.prevY = Number.isFinite(target.prevY) ? target.prevY : target.y;
|
||||
target.vx = clamp((target.vx || 0) + ax * force * dt, -820, 820);
|
||||
target.vy = clamp((target.vy || 0) + ay * force * dt, -820, 820);
|
||||
if (type === "balloon") target.spinVelocity = clamp((target.spinVelocity || 0) + 3.2 * wind.falloff * dt, -12, 12);
|
||||
else if (type === "ball") target.spinVelocity = clamp((target.spinVelocity || 0) + 5.4 * wind.falloff * dt, -38, 38);
|
||||
if (type === "fire") target.amount = Math.max(0, (target.amount ?? 1) - wind.falloff * dt * 1.0);
|
||||
worldRef?.markSpatialDirty?.("fan-wind-item");
|
||||
}
|
||||
|
||||
function applyFanToTarinai(fan, t, wind, dt, worldRef) {
|
||||
const ax = Math.cos(wind.angle);
|
||||
const ay = Math.sin(wind.angle);
|
||||
const radius = t.radius || 18;
|
||||
const speed = 30 * wind.falloff;
|
||||
t.vx = clamp((t.vx || 0) + ax * speed * dt, -360, 360);
|
||||
t.vy = clamp((t.vy || 0) + ay * speed * dt, -360, 360);
|
||||
t.fanBreezeTimer = Math.max(t.fanBreezeTimer || 0, 0.35);
|
||||
if (wind.falloff > 0.58 && (worldRef?.time || 0) - (t.lastFanSurpriseAt || -999) > 2.2) {
|
||||
t.lastFanSurpriseAt = worldRef?.time || 0;
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.12);
|
||||
if (typeof t.spawnBubble === "function") t.spawnBubble("~", { life: 0.55 });
|
||||
worldRef?.effects?.push(new Effect("ring", t.x + ax * radius * 0.4, t.y + ay * radius * 0.2, { size: Math.max(12, radius * 0.72), life: 0.16, color: "rgba(174,214,236,0.28)" }));
|
||||
}
|
||||
}
|
||||
|
||||
function checkBalloonHazards(balloon, worldRef) {
|
||||
if (!balloon || balloon.dead || balloon.type !== "balloon" || !worldRef?.nearbyItems) return;
|
||||
const now = worldRef.time || 0;
|
||||
if ((balloon.balloonPoppedAt || -999) + 0.5 > now) return;
|
||||
const radius = Math.max(26, (balloon.r || 20) + 22);
|
||||
for (const it of worldRef.nearbyItems(balloon.x, balloon.y, radius) || []) {
|
||||
if (!it || it.dead || it === balloon) continue;
|
||||
if ((typeof isPinType === "function" && isPinType(it.type) && it.pinState !== "lodged") || it.type === "fire" || it.type === "flame_firecracker") {
|
||||
const hit = distXY(balloon.x, balloon.y, it.x, it.y) <= (balloon.r || 20) + (it.r || 10) + (it.type === "fire" ? 16 : 4);
|
||||
if (hit) {
|
||||
popBalloon(balloon, worldRef, it);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WATER_BALLOON_SURPRISE_TEXT = "水風船が割れておどろいている。";
|
||||
|
||||
function tarinaiOpennessValue(t) {
|
||||
const v = (typeof global.effectivePersonalityValue === "function")
|
||||
? global.effectivePersonalityValue(t, "openness")
|
||||
: (t?.currentPersonality?.openness ?? t?.birthPersonality?.openness ?? t?.personality?.openness ?? 0);
|
||||
return Number(v) || 0;
|
||||
}
|
||||
|
||||
function shouldPanicFromWaterBalloon(t, strength, worldRef, balloon) {
|
||||
const openness = tarinaiOpennessValue(t);
|
||||
if (openness >= -0.08) return false;
|
||||
if (openness <= -0.48) return true;
|
||||
const chance = clamp(0.32 + Math.max(0, -openness) * 0.70 + Math.max(0, strength) * 0.26, 0.34, 0.92);
|
||||
return deterministicChance(worldRef || t?.world || null, "water-balloon-low-openness-panic", chance, t, balloon, Math.round(strength * 1000));
|
||||
}
|
||||
|
||||
function setWaterBalloonSurpriseBehavior(t, target, panic = false) {
|
||||
if (!t || t.dead) return false;
|
||||
t.lastPanicDetail = WATER_BALLOON_SURPRISE_TEXT;
|
||||
t.thought = WATER_BALLOON_SURPRISE_TEXT;
|
||||
if (typeof global.setBehavior === "function") {
|
||||
global.setBehavior(t, {
|
||||
actionId: panic ? "panic_escape" : "water_balloon_surprise",
|
||||
need: "safety",
|
||||
label: WATER_BALLOON_SURPRISE_TEXT,
|
||||
text: WATER_BALLOON_SURPRISE_TEXT,
|
||||
reason: WATER_BALLOON_SURPRISE_TEXT,
|
||||
target: target || null,
|
||||
phase: "perform",
|
||||
source: "behavior",
|
||||
tiedNeeds: ["safety"],
|
||||
lockSeconds: panic ? 2.6 : 1.1,
|
||||
minDuration: panic ? 1.2 : 0.55,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (typeof global.setBehaviorText === "function") {
|
||||
global.setBehaviorText(t, { need: "safety", actionId: panic ? "panic_escape" : "water_balloon_surprise", actionLabel: WATER_BALLOON_SURPRISE_TEXT, reasonText: WATER_BALLOON_SURPRISE_TEXT, target: target || null, phase: "perform", source: "behavior" });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function spawnWaterFromBalloon(balloon, worldRef) {
|
||||
if (!balloon || !worldRef) return 0;
|
||||
const baseR = Math.max(14, Number(balloon.r || 20) || 20);
|
||||
const count = Math.max(7, Math.min(13, Math.round(baseR * 0.42)));
|
||||
let spawned = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (typeof global.Item !== "function") break;
|
||||
const a = deterministicAngle(worldRef || balloon, "water-balloon-splash-angle", balloon, i);
|
||||
const d = deterministicRange(worldRef || balloon, "water-balloon-splash-dist", baseR * 0.10, baseR * 0.58, balloon, i);
|
||||
const speed = deterministicRange(worldRef || balloon, "water-balloon-drop-speed", 150, 420, balloon, i);
|
||||
const drop = new global.Item("water", balloon.x + Math.cos(a) * d, balloon.y + Math.sin(a) * d);
|
||||
drop.r = deterministicRange(worldRef || balloon, "water-balloon-drop-radius", 5.5, 10.5, balloon, i);
|
||||
drop.amount = deterministicRange(worldRef || balloon, "water-balloon-drop-amount", 10, 28, balloon, i);
|
||||
drop.prevX = drop.x;
|
||||
drop.prevY = drop.y;
|
||||
drop.vx = Math.cos(a) * speed + (Number(balloon.vx || 0) || 0) * 0.18;
|
||||
drop.vy = Math.sin(a) * speed + (Number(balloon.vy || 0) || 0) * 0.18;
|
||||
drop.splashFlightTimer = deterministicRange(worldRef || balloon, "water-balloon-drop-flight", 0.30, 0.62, balloon, i);
|
||||
drop.splashSourceId = balloon.id || "";
|
||||
if (worldRef.addItem?.(drop, "water-balloon-splash", { terrain: false })) spawned += 1;
|
||||
else { worldRef.items?.push?.(drop); spawned += 1; }
|
||||
if (i < 8) {
|
||||
worldRef.effects?.push(new Effect("fight", drop.x, drop.y, {
|
||||
size: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-size", 3.5, 7.0, balloon, i),
|
||||
life: deterministicRange(worldRef || balloon, "water-balloon-drop-spark-life", 0.24, 0.42, balloon, i),
|
||||
color: "rgba(96,196,244,0.66)",
|
||||
vx: drop.vx * 0.42,
|
||||
vy: drop.vy * 0.42,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (spawned) {
|
||||
worldRef.markItemBucketsDirty?.("water-balloon-splash");
|
||||
worldRef.markSpatialDirty?.("water-balloon-splash");
|
||||
worldRef.markTerrainDirtyAt?.(balloon.x, balloon.y, Math.max(70, baseR * 5.2), "water-balloon-splash");
|
||||
}
|
||||
return spawned;
|
||||
}
|
||||
|
||||
function updateWaterDropMotion(item, dt, worldRef) {
|
||||
if (!item || item.dead || item.type !== "water") return false;
|
||||
const frameDt = Math.max(0.016, Number(dt || 0.016) || 0.016);
|
||||
const speed = Math.hypot(item.vx || 0, item.vy || 0);
|
||||
if (speed <= 0.05 && !(item.splashFlightTimer > 0)) return false;
|
||||
item.prevX = Number.isFinite(Number(item.x)) ? item.x : 0;
|
||||
item.prevY = Number.isFinite(Number(item.y)) ? item.y : 0;
|
||||
item.x += (Number(item.vx || 0) || 0) * frameDt;
|
||||
item.y += (Number(item.vy || 0) || 0) * frameDt;
|
||||
const pad = Math.max(28, CONFIG.worldPadding || 30);
|
||||
const r = Math.max(5, Number(item.r || 8) || 8);
|
||||
if (worldRef) {
|
||||
if (item.x < pad) { item.x = pad; item.vx = Math.abs(item.vx || 0) * 0.30; }
|
||||
if (item.x > worldRef.w - pad) { item.x = worldRef.w - pad; item.vx = -Math.abs(item.vx || 0) * 0.30; }
|
||||
if (item.y < pad) { item.y = pad; item.vy = Math.abs(item.vy || 0) * 0.30; }
|
||||
if (item.y > worldRef.h - pad) { item.y = worldRef.h - pad; item.vy = -Math.abs(item.vy || 0) * 0.30; }
|
||||
}
|
||||
item.splashFlightTimer = Math.max(0, Number(item.splashFlightTimer || 0) - frameDt);
|
||||
const damp = item.splashFlightTimer > 0 ? Math.pow(0.78, frameDt * 60) : Math.pow(0.58, frameDt * 60);
|
||||
item.vx *= damp;
|
||||
item.vy *= damp;
|
||||
if (Math.hypot(item.vx || 0, item.vy || 0) < 8 && item.splashFlightTimer <= 0) {
|
||||
item.vx = 0;
|
||||
item.vy = 0;
|
||||
}
|
||||
if (worldRef && ((item.lastSplashTrailAt || -999) + 0.12 < (worldRef.time || 0)) && Math.hypot(item.vx || 0, item.vy || 0) > 72) {
|
||||
item.lastSplashTrailAt = worldRef.time || 0;
|
||||
worldRef.effects?.push(new Effect("fight", item.x, item.y, { size: Math.max(2.8, r * 0.55), life: 0.18, color: "rgba(112,206,248,0.36)", vx: -(item.vx || 0) * 0.05, vy: -(item.vy || 0) * 0.05 }));
|
||||
}
|
||||
worldRef?.markSpatialDirty?.("water-drop-motion");
|
||||
worldRef?.markTerrainDirtyAt?.(item.x, item.y, Math.max(28, r * 3.2), "water-drop-motion");
|
||||
if (worldRef) worldRef.drawListDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyWaterBalloonSurprise(balloon, worldRef) {
|
||||
for (const t of worldRef?.nearbyTarinai?.(balloon.x, balloon.y, 165) || []) {
|
||||
if (!t || t.dead || worldRef?.isTarinaiHiddenInNestBox?.(t)) continue;
|
||||
const d = Math.max(1, distXY(balloon.x, balloon.y, t.x, t.y));
|
||||
const strength = clamp(1 - d / 165, 0, 1);
|
||||
t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.34 + strength * 0.44);
|
||||
if (typeof t.addStress === "function") t.addStress(0.18 + strength * 0.42, "水風船が割れて驚いた");
|
||||
if (typeof t.spawnBubble === "function" && strength > 0.35) t.spawnBubble("!", { life: 0.72 });
|
||||
const awayX = (t.x - balloon.x) / d;
|
||||
const awayY = (t.y - balloon.y) / d;
|
||||
t.vx = clamp((t.vx || 0) + awayX * 68 * strength, -360, 360);
|
||||
t.vy = clamp((t.vy || 0) + awayY * 68 * strength, -360, 360);
|
||||
|
||||
const panic = shouldPanicFromWaterBalloon(t, strength, worldRef, balloon);
|
||||
if (panic) {
|
||||
const threat = { x: balloon.x, y: balloon.y, dead: true, type: "water_balloon_pop" };
|
||||
const destination = t.panicDestination?.(threat, true) || { x: t.x + awayX * 190, y: t.y + awayY * 190, dead: false, panic: true };
|
||||
t.enterPanic?.({
|
||||
threat,
|
||||
destination,
|
||||
target: destination,
|
||||
reason: WATER_BALLOON_SURPRISE_TEXT,
|
||||
cause: "water_balloon_pop",
|
||||
fearTimer: 1.35 + strength * 1.30,
|
||||
stress: 14 + strength * 24,
|
||||
surpriseTimer: 0.70 + strength * 0.45,
|
||||
wake: true,
|
||||
forceDestination: true,
|
||||
bubble: "!!",
|
||||
bubbleCooldown: 0.7,
|
||||
}) || t.setActionState?.("panic", { target: destination, reason: WATER_BALLOON_SURPRISE_TEXT, wake: true, sleeping: false });
|
||||
t.fearTimer = Math.max(t.fearTimer || 0, 1.85 + strength * 1.40);
|
||||
t.panicStartedAt = worldRef?.time || t.panicStartedAt || 0;
|
||||
t.panicHardStopAt = Math.max(t.panicHardStopAt || 0, (worldRef?.time || 0) + 5.5);
|
||||
t.lastPanicCause = "water_balloon_pop";
|
||||
setWaterBalloonSurpriseBehavior(t, destination, true);
|
||||
} else {
|
||||
setWaterBalloonSurpriseBehavior(t, null, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function popBalloon(balloon, worldRef, cause = null) {
|
||||
if (!balloon || balloon.dead) return;
|
||||
const now = worldRef?.time || 0;
|
||||
balloon.balloonPoppedAt = now;
|
||||
// Item.dead is a getter based on amount, so mark the water balloon gone
|
||||
// through amount/hp instead of assigning to .dead directly.
|
||||
balloon.amount = 0;
|
||||
balloon.hp = 0;
|
||||
worldRef?.effects?.push(new Effect("ring", balloon.x, balloon.y, { size: Math.max(32, (balloon.r || 20) * 1.9), life: 0.28, color: "rgba(76,178,236,0.62)" }));
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const a = deterministicAngle(worldRef || balloon, "water-balloon-pop-piece", balloon, i);
|
||||
const s = deterministicRange(worldRef || balloon, "water-balloon-pop-speed", 24, 96, balloon, i);
|
||||
worldRef?.effects?.push(new Effect("fight", balloon.x, balloon.y, { size: deterministicRange(worldRef || balloon, "water-balloon-pop-size", 4, 10, balloon, i), life: 0.28, color: "rgba(95,204,246,0.72)", vx: Math.cos(a) * s, vy: Math.sin(a) * s }));
|
||||
}
|
||||
spawnWaterFromBalloon(balloon, worldRef);
|
||||
const sourceLabel = cause?.type === "fire" || cause?.type === "flame_firecracker" ? "火" : (cause?.type === "genkotsu" ? "げんこつ" : (cause && typeof isPinType === "function" && isPinType(cause.type) ? "鋲" : "何か"));
|
||||
if (worldRef?.relationNotice?.(balloon.id || "balloon", cause?.id || sourceLabel, "water-balloon-pop", 1.4)) {
|
||||
worldRef.log?.(`水風船が${sourceLabel}に触れて割れた。`, "accident", { participants: [] });
|
||||
}
|
||||
applyWaterBalloonSurprise(balloon, worldRef);
|
||||
worldRef?.markSpatialDirty?.("water-balloon-pop");
|
||||
}
|
||||
|
||||
global.TarinaiItemDynamicBallSystem = Object.freeze({
|
||||
update,
|
||||
updateFan,
|
||||
updateWaterDropMotion,
|
||||
fanWindAt,
|
||||
resolveObstacleCollisions,
|
||||
popBalloon,
|
||||
});
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue