"use strict"; // Layer: entity-runtime/items/dynamic-system // Owns pushpin/oshibyo item motion, attachment, lodged damage, and detach // behavior. Prototype methods delegate here. (function (global) { function isPin(item) { return Boolean(global.TarinaiPinAttachmentSystem.isPin(item) || (item && typeof global.isPinType === "function" && global.isPinType(item.type))); } function ballById(worldRef, id) { if (!worldRef || !id) return null; return (worldRef.items || []).find(it => it && !it.dead && it.type === "ball" && it.id === id) || null; } function removeFromBall(item, worldRef = null) { const ball = ballById(worldRef, item?.pinBallId) || null; if (ball?.stuckPinIds) ball.stuckPinIds = ball.stuckPinIds.filter(id => id !== item.id); if (ball && ball.stuckPinId === item?.id) ball.stuckPinId = ""; if (item) item.pinBallId = ""; return ball; } function updateBallLodged(item, dt, worldRef) { const ball = ballById(worldRef, item.pinBallId); if (!ball) { detach(item, worldRef, "ball-lost"); return; } if (global.TarinaiPinAttachmentSystem.applyBallPose(item, ball, worldRef)) return; const angle = (ball.spin || 0) + (item.pinBallOffsetAngle || 0); const distance = Math.max(4, Number(item.pinBallOffsetDistance || (ball.r || 18) * 0.55) || (ball.r || 18) * 0.55); item.prevX = item.x; item.prevY = item.y; item.x = ball.x + Math.cos(angle) * distance; item.y = ball.y + Math.sin(angle) * distance; item.vx = ball.vx || 0; item.vy = ball.vy || 0; item.pinVisualSide = Math.cos(angle) >= 0 ? 1 : -1; item.spin = angle + (item.type === "oshibyo" ? 0.18 : Math.PI * 0.5); item.deletable = false; if (Number.isFinite(ball.spinVelocity)) item.spinVelocity = ball.spinVelocity; if (worldRef) worldRef.drawListDirty = true; } function attachToBall(item, ball, worldRef) { if (!isPin(item) || !ball || ball.dead || ball.type !== "ball" || item.dead) return false; if (item.pinState === "lodged" || item.pinState === "ball_lodged") return false; const now = worldRef?.time || 0; if ((item.noStickUntil || 0) > now) return false; const angle = deterministicAngle(worldRef, "pin-ball-stick-angle", item, ball, now); const dist = Math.max(4, (ball.r || 18) * deterministicRange(worldRef, "pin-ball-stick-distance", 0.24, 0.78, item, ball, now)); item.pinState = "ball_lodged"; item.pinBallId = ball.id; item.pinTargetId = ""; item.pinBallOffsetAngle = angle - (ball.spin || 0); item.pinBallOffsetDistance = dist; item.pinVisualSide = Math.cos(angle) >= 0 ? 1 : -1; item.deletable = false; item.noStickUntil = now + 0.16; ball.stuckPinIds = Array.isArray(ball.stuckPinIds) ? ball.stuckPinIds : []; if (!ball.stuckPinIds.includes(item.id)) ball.stuckPinIds.push(item.id); ball.stuckPinId = ball.stuckPinIds[0] || item.id; updateBallLodged(item, 0, worldRef); worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(12, (item.r || 8) * 1.4), life: 0.16, color: item.type === "oshibyo" ? "rgba(73,119,205,0.42)" : "rgba(214,72,72,0.42)" })); return true; } function transferFromBallToTarinai(item, ball, t, worldRef) { if (!item || item.dead || item.pinState !== "ball_lodged" || !ball || !t || t.dead) return false; const now = worldRef?.time || 0; if ((item._ballPinTransferredAt || -999) + 0.18 > now) return false; removeFromBall(item, worldRef); item.pinState = "loose"; item.pinTargetId = ""; item.noStickUntil = 0; item.noStickTargetId = ""; item.noStickTargetUntil = 0; item.x = ball.x + deterministicRange(worldRef, "pin-transfer-x", -(ball.r || 18) * 0.25, (ball.r || 18) * 0.25, item, ball, t); item.y = ball.y + deterministicRange(worldRef, "pin-transfer-y", -(ball.r || 18) * 0.25, (ball.r || 18) * 0.25, item, ball, t); item._ballPinTransferredAt = now; const ok = attach(item, t, worldRef, distXY(item.x, item.y, t.x, t.y)); if (!ok) attachToBall(item, ball, worldRef); return ok; } function transferBallPinsToTarinai(ball, t, worldRef) { if (!ball || !t || t.dead) return 0; const ids = Array.isArray(ball.stuckPinIds) ? [...ball.stuckPinIds] : (ball.stuckPinId ? [ball.stuckPinId] : []); let moved = 0; for (const id of ids) { const pin = (worldRef?.items || []).find(it => it && !it.dead && it.id === id && isPin(it)); if (pin && transferFromBallToTarinai(pin, ball, t, worldRef)) moved += 1; } return moved; } function tryBallPickup(ball, worldRef) { if (!ball || ball.dead || ball.type !== "ball" || !worldRef?.nearbyItems) return 0; const speed = Math.hypot(ball.vx || 0, ball.vy || 0); const range = (ball.r || 18) + speed * Math.max(0.016, Number(worldRef.dt || 0.016) || 0.016) + 28; const candidates = worldRef.nearbyItems(ball.x, ball.y, range) || []; let picked = 0; const px = Number.isFinite(ball.prevX) ? ball.prevX : ball.x; const py = Number.isFinite(ball.prevY) ? ball.prevY : ball.y; const sx = ball.x - px; const sy = ball.y - py; const len2 = sx * sx + sy * sy; for (const pin of candidates) { if (!isPin(pin) || pin.dead || pin.pinState === "lodged" || pin.pinState === "ball_lodged") continue; let hitX = ball.x; let hitY = ball.y; if (len2 > 1) { const u = clamp(((pin.x - px) * sx + (pin.y - py) * sy) / len2, 0, 1); hitX = px + sx * u; hitY = py + sy * u; } const hit = (ball.r || 18) + Math.max(5, pin.r || 8) + 2; if (distXY(hitX, hitY, pin.x, pin.y) > hit) continue; if (attachToBall(pin, ball, worldRef)) picked += 1; } return picked; } function update(item, dt, worldRef) { if (!isPin(item) || item.dead) return false; if (!worldRef) return false; if (item.pinState === "lodged") { updateLodged(item, dt, worldRef); return true; } if (item.pinState === "ball_lodged") { updateBallLodged(item, dt, worldRef); return true; } if (global.TarinaiPhysics.applyKinematicItemMotion) { global.TarinaiPhysics.applyKinematicItemMotion(item, worldRef, dt, { padding: 26, bounce: 0.44, spinBounce: -0.68, frictionBase: 0.18, frictionRate: 0.85, spinFrictionBase: 0.38, spinRestDamp: 0.08, stopSpeed: 0.05, zeroBelow: 7.5 }); global.TarinaiItemDynamicBallSystem.resolveObstacleCollisions(item, dt, worldRef); } else { item.prevX = item.x; item.prevY = item.y; item.x += (item.vx || 0) * dt; item.y += (item.vy || 0) * dt; } tryStick(item, worldRef); return true; } function tryStick(item, worldRef) { if (!worldRef?.tarinai?.length) return false; if (item.pinState === "lodged") return false; const now = worldRef.time || 0; if ((item.noStickUntil || 0) > now) return false; let best = null; let bestD = Infinity; const hitRange = Math.max(10, (item.r || 8) * 1.25); const candidates = worldRef.nearbyTarinai?.(item.x, item.y, hitRange + 32, true) || worldRef.tarinai; for (const t of candidates) { if (!t || t.dead) continue; if (item.noStickTargetId && item.noStickTargetId === t.id && (item.noStickTargetUntil || 0) > now) continue; const d = distXY(item.x, item.y, t.x, t.y); const hit = (t.radius || 16) * 0.86 + hitRange; if (d <= hit && d < bestD) { best = t; bestD = d; } } if (!best) return false; return attach(item, best, worldRef, bestD); } function oshibyoAnchorFor(t, visualSide, item = { type: "oshibyo" }) { return global.TarinaiPinAttachmentSystem.tarinaiAnchor(item, t, { visualSide }) || { x: t.x + visualSide * (t?.radius || 16) * 0.96, y: t.y + (t?.radius || 16) * 0.50, angle: visualSide * 0.48, distance: (t?.radius || 16) * 0.96, offsetY: (t?.radius || 16) * 0.48, visualSide, }; } function attach(item, t, worldRef, d = null) { if (!t || t.dead) return false; if (t.stuckPushpinId && t.stuckPushpinId !== item.id) return false; const behavior = pinBehaviorFor(item.type); const isOshibyo = Boolean(behavior?.blocksZunchi); const dx = item.x - t.x; const dy = item.y - t.y; const distToTarget = Number.isFinite(d) ? d : Math.hypot(dx, dy); item.pinState = "lodged"; item.deletable = false; item.pinTargetId = t.id; removeFromBall(item, worldRef); const sharedAnchor = global.TarinaiPinAttachmentSystem.tarinaiAnchor(item, t, { dx, dy, distance: distToTarget }); const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1); const faceSide = faceDir >= 0 ? 1 : -1; const visualSide = sharedAnchor?.visualSide ?? (isOshibyo ? -faceSide : faceSide); item.pinVisualSide = visualSide; const oshibyoAnchor = isOshibyo ? (sharedAnchor || oshibyoAnchorFor(t, visualSide, item)) : null; item.pinAttachAngle = isOshibyo ? oshibyoAnchor.angle : (sharedAnchor?.angle ?? Math.atan2(dy || -1, dx || visualSide)); item.pinAttachDistance = isOshibyo ? oshibyoAnchor.distance : (sharedAnchor?.distance ?? clamp(distToTarget || (t.radius || 16) * 0.58, (t.radius || 16) * 0.20, (t.radius || 16) * 0.74)); item.pinOffsetY = isOshibyo ? oshibyoAnchor.offsetY : (sharedAnchor?.offsetY ?? clamp(dy, -(t.radius || 16) * 0.74, (t.radius || 16) * 0.38)); item.pinDamageTick = 0; item.pinFallCheckTimer = 0; item.vx = 0; item.vy = 0; item.spinVelocity = 0; item.spin = item.pinAttachAngle; t.stuckPushpinId = item.id; t.sleeping = false; if (!isOshibyo) t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 7); if (!isOshibyo) t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.8); if (isOshibyo) { t.thought = "\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u3066\u305a\u3093\u3061\u304c\u51fa\u306a\u304f\u306a\u3063\u305f"; worldRef.log?.(`${t.name}\u306b\u304a\u3057\u308a\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] }); worldRef.effects?.push(new Effect("ring", t.x, t.y + (t.radius || 16) * 0.34, { size: Math.max(14, (t.radius || 16) * 0.58), life: 0.18, color: "rgba(73,119,205,0.36)" })); return true; } if (t.enterPanic) { t.enterPanic({ threat: item, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 1.3, stress: behavior?.stressOnAttach ?? 18, hurtTimer: 1.2, awakeLockTimer: 7, surpriseTimer: 0.8, cause: "pushpin_attach" }); } else { t.hurtTimer = Math.max(t.hurtTimer || 0, 1.2); t.fearTimer = Math.max(t.fearTimer || 0, 1.3); t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + deterministicRange(worldRef, "pin-panic-target-x", -80, 80, item, t), y: t.y + deterministicRange(worldRef, "pin-panic-target-y", -80, 80, item, t) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); if (t.addStress) t.addStress(behavior?.stressOnAttach ?? 18, { threshold: 8 }); } if (!isOshibyo && t.damage && (behavior?.damageOnAttach ?? 6) > 0) t.damage(behavior?.damageOnAttach ?? 6, "\u753b\u92f2", { source: item }); if ((worldRef.time || 0) >= (item.pinLogAt || -999) + 1.2) { item.pinLogAt = worldRef.time || 0; worldRef.log?.(`${t.name}\u306b\u753b\u92f2\u304c\u523a\u3055\u3063\u305f\u3002`, "accident", { participants: [t] }); } worldRef.effects?.push(new Effect("ring", t.x, t.y - (t.radius || 16) * 0.12, { size: Math.max(18, (t.radius || 16) * 0.90), life: 0.22, color: "rgba(214,72,72,0.50)" })); return true; } function updateLodged(item, dt, worldRef) { const t = worldRef.liveTarinaiById?.(item.pinTargetId) || null; if (!t) { detach(item, worldRef, "owner-lost"); return; } const behavior = pinBehaviorFor(item.type); const isOshibyo = Boolean(behavior?.blocksZunchi); t.stuckPushpinId = item.id; item.deletable = false; const sharedPose = global.TarinaiPinAttachmentSystem.applyTarinaiPose(item, t, worldRef); if (!sharedPose) { const faceDir = t.facingDir ? t.facingDir() : (t.facing || 1); const faceSide = faceDir >= 0 ? 1 : -1; const visualSide = isOshibyo ? -faceSide : faceSide; item.pinVisualSide = visualSide; const oshibyoAnchor = isOshibyo ? oshibyoAnchorFor(t, visualSide, item) : null; item.x = isOshibyo ? oshibyoAnchor.x : t.x + visualSide * (t.radius || 16) * 0.36; item.y = isOshibyo ? oshibyoAnchor.y : t.y - (t.radius || 16) * 0.04; item.prevX = item.x; item.prevY = item.y; item.spin = isOshibyo ? oshibyoAnchor.angle : visualSide * 0.28; item.vx = t.vx || 0; item.vy = t.vy || 0; } if (isOshibyo) { if ((t.oshiriByoZunchiStock || 0) >= 6 && t.state !== "eat" && t.state !== "sleep") t.thought = "\u304a\u3057\u308a\u92f2\u3067\u305a\u3093\u3061\u304c\u6e9c\u307e\u3063\u3066\u3064\u3089\u3044"; return; } item.pinDamageTick = (item.pinDamageTick || 0) + dt; while (item.pinDamageTick >= 0.55) { item.pinDamageTick -= 0.55; if (!isOshibyo && t.damage && (behavior?.damagePerTick ?? 1.4) > 0) t.damage(behavior?.damagePerTick ?? 1.4, "\u753b\u92f2", { source: item }); t.hurtTimer = Math.max(t.hurtTimer || 0, 0.50); if (t.addStress) t.addStress(behavior?.stressPerTick ?? 2.6, { threshold: 8, duration: 3.4 }); if (deterministicChance(worldRef, "pin-pain-bubble", 0.22, item, t)) t.bubble?.("!!", 0.6, "rgba(168,72,72,0.82)"); } t.sleeping = false; if (t.enterPanic) t.enterPanic({ threat: item, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", fear: 0.65, surpriseTimer: 0.22, awakeLockTimer: 2.4, cause: "pushpin_lodged" }); else { t.setActionState?.("panic", { target: t.panicDestination ? t.panicDestination(item) : { x: t.x + deterministicRange(worldRef, "pin-panic-target-x", -80, 80, item, t), y: t.y + deterministicRange(worldRef, "pin-panic-target-y", -80, 80, item, t) }, reason: "\u753b\u92f2\u304c\u523a\u3055\u3063\u3066\u30d1\u30cb\u30c3\u30af\u306b\u306a\u3063\u3066\u3044\u308b", wake: true }); t.fearTimer = Math.max(t.fearTimer || 0, 0.65); t.surpriseTimer = Math.max(t.surpriseTimer || 0, 0.22); t.awakeLockTimer = Math.max(t.awakeLockTimer || 0, 2.4); } item.pinFallCheckTimer = (item.pinFallCheckTimer || 0) + dt; while (item.pinFallCheckTimer >= 1.0) { item.pinFallCheckTimer -= 1.0; if (deterministicChance(worldRef, "pin-fall-out", 0.10, item, t)) { detach(item, worldRef, "fall"); return; } } } function detach(item, worldRef, reason = "released") { const carriedBall = removeFromBall(item, worldRef); const t = worldRef?.liveTarinaiById?.(item.pinTargetId) || null; const behavior = pinBehaviorFor(item.type); const wasOshibyo = Boolean(behavior?.blocksZunchi); const storedZunchi = wasOshibyo && t ? Math.max(0, Math.floor(t.oshiriByoZunchiStock || 0)) : 0; if (t && t.stuckPushpinId === item.id) t.stuckPushpinId = null; if (t && wasOshibyo) t.oshiriByoZunchiStock = 0; if (reason === "pinch") { const now = worldRef?.time || 0; item.noStickUntil = now + 1.0; item.noStickTargetId = t?.id || ""; item.noStickTargetUntil = now + 1.0; } else if (t) { const now = worldRef?.time || 0; item.noStickTargetId = t.id || ""; item.noStickTargetUntil = now + 0.55; } item.pinState = "loose"; item.pinTargetId = ""; item.pinDamageTick = 0; item.pinFallCheckTimer = 0; item.deletable = true; if (reason === "pinch") { item.vx = 0; item.vy = 0; item.spinVelocity = 0; } else { let nx = 0; let ny = 1; if (t) { const dx = Number(item.x || 0) - Number(t.x || 0); const dy = Number(item.y || 0) - Number(t.y || 0); const d = Math.hypot(dx, dy); if (d > 0.001) { nx = dx / d; ny = dy / d; } else { nx = Math.sign(item.pinVisualSide || 0) || (t.facingDir?.() || 1); ny = 0.28; } const len = Math.hypot(nx, ny) || 1; nx /= len; ny /= len; const dropR = Math.max(8, (item.r || 8) + (t.radius || 16) * 0.58); const pad = CONFIG.worldPadding || 30; item.x = clamp((t.x || 0) + nx * dropR, pad, Math.max(pad, (worldRef?.w || item.x) - pad)); item.y = clamp((t.y || 0) + ny * dropR + (wasOshibyo ? 4 : 0), pad, Math.max(pad, (worldRef?.h || item.y) - pad)); } else if (carriedBall) { const dx = Number(item.x || 0) - Number(carriedBall.x || 0); const dy = Number(item.y || 0) - Number(carriedBall.y || 0); const d = Math.hypot(dx, dy) || 1; nx = dx / d; ny = dy / d; } const baseVx = Number(carriedBall?.vx || t?.vx || 0) || 0; const baseVy = Number(carriedBall?.vy || t?.vy || 0) || 0; item.vx = clamp(baseVx * 0.45 + nx * 18, -90, 90); item.vy = clamp(baseVy * 0.45 + Math.max(12, ny * 18), -60, 110); item.spinVelocity = clamp((item.spinVelocity || 0) * 0.35 + (Math.sign(nx) || 1) * 0.9, -2.2, 2.2); worldRef?.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(14, (item.r || 16) * 0.92), life: 0.16, color: "rgba(214,112,112,0.40)" })); if (reason === "fall" && t) worldRef?.log?.(`${t.name}\u306E\u753B\u92F2\u304C\u629C\u3051\u843D\u3061\u305F\u3002`, "observe", { participants: [t] }); } if (wasOshibyo && storedZunchi > 0 && worldRef?.spawnBurstZunchi) { worldRef.spawnBurstZunchi(t || item, storedZunchi, item); } } global.TarinaiItemDynamicPinSystem = Object.freeze({ update, isPin, attach, detach, transferBallPinsToTarinai, tryBallPickup, }); })(typeof window !== "undefined" ? window : globalThis);