"use strict"; // Layer: entity-runtime/items/dynamic-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; 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.72; item.spinVelocity *= -0.72; } if (item.x > worldRef.w - p) { item.x = worldRef.w - p; item.vx = -Math.abs(item.vx || 0) * 0.72; item.spinVelocity *= -0.72; } if (item.y < p) { item.y = p; item.vy = Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; } if (item.y > worldRef.h - p) { item.y = worldRef.h - p; item.vy = -Math.abs(item.vy || 0) * 0.72; item.spinVelocity *= -0.72; } resolveObstacleCollisions(item, dt, worldRef); item.x = clamp(item.x, p, worldRef.w - p); item.y = clamp(item.y, p, worldRef.h - p); const groundFriction = Math.pow(0.72, dt); item.vx *= groundFriction; item.vy *= groundFriction; } global.TarinaiItemDynamicPinSystem.tryBallPickup(item, worldRef); item.spin = (item.spin || 0) + (item.spinVelocity || 0) * dt + (item.vx || 0) * dt / Math.max(8, item.r || 18); item.spinVelocity *= Math.pow(0.65, dt); if (Math.hypot(item.vx || 0, item.vy || 0) < 0.18) { item.vx = 0; item.vy = 0; } 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 // \u5168item\u8d70\u67fb\u3067\u306f\u306a\u304f\u3001\u6a5f\u69cbtype bucket\u3060\u3051\u3092\u8ffd\u52a0\u78ba\u8a8d\u3059\u308b\u3002 const mechanicalTypes = ["rotator", "poison_block", "reciprocator"]; for (const type of mechanicalTypes) { const bucket = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType(type) : (worldRef.items || []); for (const it of bucket || []) { if (!it || it.dead || it === item || !global.TarinaiMechanicalSystem.isMechanicalType(it.type) || seen.has(it)) continue; const reach = global.TarinaiMechanicalSystem.reach(it) || worldRef.rotatorExtent?.(it) || Math.max(64, it.r || 64); if (distXY(item.x, item.y, it.x, it.y) <= searchRadius + reach + 36) { items.push(it); seen.add(it); } } } for (const it of items) { if (!it || it === item || it.dead) continue; 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), 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; for (const rect of rects) resolveRectBounce(item, rect, worldRef, it); } } function applyHayDrag(item, bed, dt, worldRef) { const rx = Math.max(26, (bed.r || 37) * 1.72 + (item.r || 18) * 0.40); const ry = Math.max(18, (bed.r || 37) * 0.92 + (item.r || 18) * 0.34); const dx = item.x - bed.x; const dy = item.y - bed.y; const inside = (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) <= 1; if (!inside) return; 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)); if ((item.lastHaySlowLogAt || -999) + 3.5 < (worldRef.time || 0) && Math.hypot(item.vx || 0, item.vy || 0) > 120) { item.lastHaySlowLogAt = worldRef.time || 0; worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, item.r * 0.95), life: 0.20, color: "rgba(214,184,96,0.42)" })); } } function resolveCircleBounce(item, obstacle, hitRadius, restitution, worldRef) { let dx = item.x - obstacle.x; let dy = item.y - obstacle.y; let d = Math.hypot(dx, dy); if (d >= hitRadius) { const px = item.prevX; const py = item.prevY; if (!Number.isFinite(px) || !Number.isFinite(py)) return; const sx = item.x - px; const sy = item.y - py; const len2 = sx * sx + sy * sy; if (len2 <= 0.0001) return; const t = clamp(((obstacle.x - px) * sx + (obstacle.y - py) * sy) / len2, 0, 1); const cx = px + sx * t; const cy = py + sy * t; dx = cx - obstacle.x; dy = cy - obstacle.y; d = Math.hypot(dx, dy); if (d >= hitRadius) return; if (d < 0.001) { const speed = Math.hypot(item.vx || 0, item.vy || 0); if (speed > 0.001) { dx = -(item.vx || 0) / speed; dy = -(item.vy || 0) / speed; d = 1; } else { dx = -sx / Math.sqrt(len2); dy = -sy / Math.sqrt(len2); d = 1; } } } if (d < 0.001) { const speed = Math.hypot(item.vx || 0, item.vy || 0); if (speed > 0.001) { dx = -(item.vx || 0) / speed; dy = -(item.vy || 0) / speed; d = 1; } else { dx = Math.cos(item.seed || 0); dy = Math.sin(item.seed || 0); d = 1; } } const nx = dx / d; const ny = dy / d; const toward = (item.vx || 0) * nx + (item.vy || 0) * ny; item.x = obstacle.x + nx * (hitRadius + 0.5); item.y = obstacle.y + ny * (hitRadius + 0.5); if (toward < 0) { item.vx = (item.vx || 0) - (1 + restitution) * toward * nx; item.vy = (item.vy || 0) - (1 + restitution) * toward * ny; } else { const push = item.type === "balloon" ? 12 : 24; item.vx = (item.vx || 0) + nx * push; item.vy = (item.vy || 0) + ny * push; } 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); } function resolveRectBounce(item, rect, worldRef, source = null) { if (!rect) return; const bounceCircleOffRect = global.TarinaiPhysics?.bounceCircleOffRect; if (typeof bounceCircleOffRect !== "function") return; const hitRadius = (item.r || 18) + 2; const restitution = Number.isFinite(rect.restitution) ? rect.restitution : (item.type === "balloon" ? 0.82 : 1.25); 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: item.type === "balloon" ? 10 : 18, spinKick: item.type === "balloon" ? 2.4 : 6.2, spinLimit: 38, }); if (!bounced) return; item.vx *= velocityDamp; item.vy *= velocityDamp; emitBounce(item, worldRef, source || rect.item || rect); } function emitBounce(item, worldRef, obstacle) { const now = worldRef?.time || 0; if ((item.lastObstacleBounceAt || -999) + 0.08 > now) return; item.lastObstacleBounceAt = now; 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) { return normalizedItemAngle(angle, fallback); } function fanWindAngleFor(item) { return normalizeAngleLocal(item?.angle, item?.fanSwingCenter ?? 0); } function ensureFanBodyAngle(item) { if (!item || item.type !== "fan") return 0; const body = Number(item.fanBodyAngle); if (Number.isFinite(body)) return normalizeAngleLocal(body, item.angle || 0); item.fanBodyAngle = normalizeAngleLocal(item.angle || item.fanSwingCenter || 0, 0); return item.fanBodyAngle; } function updateFanSwing(item, dt) { if (!item || item.dead || item.type !== "fan") return false; ensureFanBodyAngle(item); if (!item.fanSwingOn) { item.fanSwingCenter = Number.isFinite(Number(item.fanSwingCenter)) ? Number(item.fanSwingCenter) : fanWindAngleFor(item); item.angle = normalizeAngleLocal(item.fanSwingCenter, item.angle || 0); return false; } const center = normalizeAngleLocal(item.fanSwingCenter, fanWindAngleFor(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; } const FAN_WIND_ITEM_TYPES = Object.freeze(["balloon", "ball", "pushpin", "oshibyo", "water", "zunda_juice", "mercury", "zunchi", "fire"]); function fanWindItemCandidates(fan, worldRef, cx, cy, radius) { if (!worldRef) return []; if (typeof worldRef.itemsOfType === "function") { const out = []; const seen = new Set(); const r2 = Math.max(1, Number(radius || 0) || 0) ** 2; for (const type of FAN_WIND_ITEM_TYPES) { for (const it of worldRef.itemsOfType(type) || []) { if (!it || it.dead || it === fan || seen.has(it.id || it)) continue; const dx = Number(it.x || 0) - cx; const dy = Number(it.y || 0) - cy; const reach = radius + Math.max(0, Number(it.r || it.radius || 0) || 0); if (dx * dx + dy * dy > Math.max(r2, reach * reach)) continue; seen.add(it.id || it); out.push(it); } } return out; } return worldRef.nearbyItems?.(cx, cy, radius) || worldRef.items || []; } function fanWindAt(fan, x, y, opts = {}) { if (global.TarinaiSignalSystem?.powerOverride?.(fan) === false) return null; 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 = fanWindAngleFor(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; ensureFanBodyAngle(item); const powered = global.TarinaiSignalSystem?.powerOverride?.(item) !== false; if (!powered) { item.vx = 0; item.vy = 0; return true; } item.fanSpin = (item.fanSpin || 0) + 11.5 * frameDt; item.fanPulse = (item.fanPulse || 0) + 2.2 * frameDt; updateFanSwing(item, frameDt); const angle = fanWindAngleFor(item); const ax = Math.cos(angle); const ay = Math.sin(angle); const range = 320; const searchRadius = range + 40; const queryX = item.x + ax * range * 0.45; const queryY = item.y + ay * range * 0.45; const nearbyItems = fanWindItemCandidates(item, worldRef, queryX, queryY, 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?.(queryX, queryY, 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 spreadFireByFan(fan, fire, wind, dt, worldRef) { if (!fan || !fire || !worldRef || fire.dead || fire.type !== "fire") return false; const now = worldRef.time || 0; const falloff = Math.max(0, Number(wind?.falloff || 0) || 0); if (falloff < 0.16 || (fire.amount || 0) < 18) return false; const interval = Math.max(0.10, 0.34 - falloff * 0.18); if ((fire.lastFanSpreadAt || -999) + interval > now) return false; fire.lastFanSpreadAt = now; const ax = Math.cos(wind.angle); const ay = Math.sin(wind.angle); const side = global.deterministicRange(worldRef, "fan-fire-side", -22, 22, fan, fire, Math.round(now * 10)); const dist = global.deterministicRange(worldRef, "fan-fire-distance", 58, 118, fan, fire, Math.round(now * 10)) * (0.82 + falloff * 0.58); const x = clamp((fire.x || 0) + ax * dist - ay * side, CONFIG.worldPadding || 30, Math.max(CONFIG.worldPadding || 30, (worldRef.w || 0) - (CONFIG.worldPadding || 30))); const y = clamp((fire.y || 0) + ay * dist + ax * side, CONFIG.worldPadding || 30, Math.max(CONFIG.worldPadding || 30, (worldRef.h || 0) - (CONFIG.worldPadding || 30))); const spawned = global.TarinaiItemDynamicToolSystem?.spawnFireAt?.(worldRef, x, y, { radius: Math.max(8, (fire.r || 14) * (0.55 + falloff * 0.22)), amount: Math.max(18, Math.min(86, (fire.amount || 60) * (0.20 + falloff * 0.22))), life: 2.2 + falloff * 3.4, source: fan, mergeRadius: 24, }); if (spawned) { // A fan should carry embers forward instead of merely dimming the source flame. // The source loses a little fuel, while the downwind flame handles normal fire contacts. fire.amount = Math.max(4, (fire.amount || 0) - (2.2 + falloff * 3.8)); worldRef.effects?.push(new Effect("flame", fire.x + ax * Math.max(12, fire.r || 14), fire.y + ay * Math.max(12, fire.r || 14), { size: Math.max(7, (fire.r || 14) * 0.72), life: 0.22, color: "rgba(255,126,54,0.58)", vx: ax * (80 + falloff * 120), vy: ay * (80 + falloff * 120), })); worldRef.markItemBucketsDirty?.("fan-fire-spread"); worldRef.markSpatialDirty?.("fan-fire-spread"); worldRef.drawListDirty = true; return true; } return false; } 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") { spreadFireByFan(fan, target, wind, dt, worldRef); return; } 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); // Promote wind-driven objects out of the sleeping heap immediately. Without // this, velocity accumulates while the target waits for its next scheduled // update and the object jumps in periodic bursts. global.TarinaiItemUpdateScheduler?.wakeItem?.(worldRef, target, 0.24); 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" || it.type === "firecracker") { const extra = it.type === "fire" ? 16 : (it.type === "firecracker" || it.type === "flame_firecracker" ? 8 : 4); const hit = distXY(balloon.x, balloon.y, it.x, it.y) <= (balloon.r || 20) + (it.r || 10) + extra; if (hit) { popBalloon(balloon, worldRef, it); return; } } } } const WATER_BALLOON_SURPRISE_TEXT = "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u304a\u3069\u308d\u3044\u3066\u3044\u308b\u3002"; 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(12, Math.min(22, Math.round(baseR * 0.68))); 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.12, baseR * 0.95, balloon, i); const speed = deterministicRange(worldRef || balloon, "water-balloon-drop-speed", 280, 760, balloon, i); const x = balloon.x + Math.cos(a) * d; const y = balloon.y + Math.sin(a) * d; const vx = Math.cos(a) * speed + (Number(balloon.vx || 0) || 0) * 0.28; const vy = Math.sin(a) * speed + (Number(balloon.vy || 0) || 0) * 0.28; const flight = deterministicRange(worldRef || balloon, "water-balloon-drop-flight", 0.42, 0.88, balloon, i); const amount = deterministicRange(worldRef || balloon, "water-balloon-drop-amount", 24, 58, balloon, i); const drop = global.TarinaiWeatherSystem?.createRainWaterItem ? global.TarinaiWeatherSystem.createRainWaterItem(worldRef, { x, y, amount, vx, vy, splashFlightTimer: flight }) : new global.Item("water", x, y); if (!Number.isFinite(Number(drop.amount))) drop.amount = amount; if (!Number.isFinite(Number(drop.vx))) drop.vx = vx; if (!Number.isFinite(Number(drop.vy))) drop.vy = vy; if (!Number.isFinite(Number(drop.splashFlightTimer))) drop.splashFlightTimer = flight; const addedDrop = worldRef.addItem?.(drop, "water-balloon-splash", { terrain: false }) || null; if (!addedDrop) continue; 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, "\u6c34\u98a8\u8239\u304c\u5272\u308c\u3066\u9a5a\u3044\u305f"); 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" ? "\u706b" : (cause?.type === "firecracker" ? "\u7206\u7af9" : (cause?.type === "shoot" ? "\u5c04\u6483" : (cause?.type === "genkotsu" ? "\u3052\u3093\u3053\u3064" : (cause && typeof isPinType === "function" && isPinType(cause.type) ? "\u92f2" : "\u5f37\u3044\u885d\u6483")))); if (worldRef?.relationNotice?.(balloon.id || "balloon", cause?.id || sourceLabel, "water-balloon-pop", 1.4)) { worldRef.log?.(`\u6c34\u98a8\u8239\u304c${sourceLabel}\u306b\u89e6\u308c\u3066\u5272\u308c\u305f\u3002`, "accident", { participants: [] }); } applyWaterBalloonSurprise(balloon, worldRef); worldRef?.markSpatialDirty?.("water-balloon-pop"); } global.TarinaiItemDynamicBallSystem = Object.freeze({ update, updateFan, fanWindAt, updateWaterDropMotion, resolveObstacleCollisions, popBalloon, }); })(typeof window !== "undefined" ? window : globalThis);