"use strict"; // Layer: entity-runtime/items/dynamic-system // Owns ball item motion and obstacle bounce behavior. Item.prototype ball // methods are compatibility facades that delegate to this system. (function (global) { function update(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; } 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 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); // 回転体は大きな輪郭を設計できるため、中心が近傍セル外でも当たる可能性がある。 // 中心ではなく外接半径で追加走査して、長い棒・大きな円形にも衝突させる。 if (Array.isArray(worldRef.items)) { for (const it of worldRef.items) { if (!it || it.dead || it === item || it.type !== "rotator" || seen.has(it)) continue; const reach = 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), 0.82, worldRef); } 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(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 { item.vx = (item.vx || 0) + nx * 24; item.vy = (item.vy || 0) + ny * 24; } item.vx *= 0.96; item.vy *= 0.96; item.spinVelocity = clamp((item.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 5.5, -38, 38); emitBounce(item, worldRef, obstacle); } function resolveRectBounce(item, rect, worldRef, source = null) { if (!rect) 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 minBounceSpeed = Math.max(0, Number(rect.minBounceSpeed || 0) || 0); if (rect.oriented && global.TarinaiPhysics?.bounceCircleOffRect) { const bounced = global.TarinaiPhysics.bounceCircleOffRect(item, rect, hitRadius, restitution, worldRef, { minBounceSpeed }); if (!bounced) return; item.vx *= velocityDamp; item.vy *= velocityDamp; emitBounce(item, worldRef, source || rect.item || rect); return; } const ensureMinNormalSpeed = (nx, ny) => { if (!(rect.bounce && minBounceSpeed > 0)) return; const normalSpeed = (item.vx || 0) * nx + (item.vy || 0) * ny; if (normalSpeed >= minBounceSpeed) return; const add = minBounceSpeed - normalSpeed; item.vx = (item.vx || 0) + nx * add; item.vy = (item.vy || 0) + ny * add; }; const cx = clamp(item.x, rect.left, rect.right); const cy = clamp(item.y, rect.top, rect.bottom); let dx = item.x - cx; let dy = item.y - cy; let d = Math.hypot(dx, dy); if (d >= hitRadius) { const px = item.prevX; const py = item.prevY; const expanded = { left: rect.left - hitRadius, right: rect.right + hitRadius, top: rect.top - hitRadius, bottom: rect.bottom + hitRadius }; if (!Number.isFinite(px) || !Number.isFinite(py) || !worldRef?.segmentIntersectsRect?.(px, py, item.x, item.y, expanded)) return; let nx = 0; let ny = 0; const vx = item.vx || 0; const vy = item.vy || 0; const candidates = []; const sx = item.x - px; const sy = item.y - py; if (px < expanded.left && sx > 0) candidates.push({ nx: -1, ny: 0, t: (expanded.left - px) / Math.max(sx, 0.001) }); if (px > expanded.right && sx < 0) candidates.push({ nx: 1, ny: 0, t: (px - expanded.right) / Math.max(-sx, 0.001) }); if (py < expanded.top && sy > 0) candidates.push({ nx: 0, ny: -1, t: (expanded.top - py) / Math.max(sy, 0.001) }); if (py > expanded.bottom && sy < 0) candidates.push({ nx: 0, ny: 1, t: (py - expanded.bottom) / Math.max(-sy, 0.001) }); if (candidates.length) { candidates.sort((a, b) => a.t - b.t); nx = candidates[0].nx; ny = candidates[0].ny; } else if (Math.abs(vx) >= Math.abs(vy)) { nx = vx >= 0 ? -1 : 1; } else { ny = vy >= 0 ? -1 : 1; } const toward = vx * nx + vy * ny; if (nx < 0) item.x = expanded.left - 0.5; else if (nx > 0) item.x = expanded.right + 0.5; if (ny < 0) item.y = expanded.top - 0.5; else if (ny > 0) item.y = expanded.bottom + 0.5; if (nx) item.y = clamp(item.y, expanded.top, expanded.bottom); if (ny) item.x = clamp(item.x, expanded.left, expanded.right); if (toward < 0) { item.vx = vx - (1 + restitution) * toward * nx; item.vy = vy - (1 + restitution) * toward * ny; } else { item.vx = vx + nx * 18; item.vy = vy + ny * 18; } ensureMinNormalSpeed(nx, ny); item.vx *= velocityDamp; item.vy *= velocityDamp; item.spinVelocity = clamp((item.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38); emitBounce(item, worldRef, source || rect.item || rect); return; } if (d < 0.001) { const left = Math.abs(item.x - rect.left); const right = Math.abs(rect.right - item.x); const top = Math.abs(item.y - rect.top); const bottom = Math.abs(rect.bottom - item.y); const m = Math.min(left, right, top, bottom); if (m === left) { dx = -1; dy = 0; } else if (m === right) { dx = 1; dy = 0; } else if (m === top) { dx = 0; dy = -1; } else { dx = 0; dy = 1; } d = 1; } const nx = dx / d; const ny = dy / d; const toward = (item.vx || 0) * nx + (item.vy || 0) * ny; item.x = cx + nx * (hitRadius + 0.5); item.y = cy + 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 { item.vx = (item.vx || 0) + nx * 18; item.vy = (item.vy || 0) + ny * 18; } ensureMinNormalSpeed(nx, ny); item.vx *= velocityDamp; item.vy *= velocityDamp; item.spinVelocity = clamp((item.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * 6.2, -38, 38); 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 = (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 })); } global.TarinaiItemDynamicBallSystem = Object.freeze({ update, resolveObstacleCollisions, applyHayDrag, resolveCircleBounce, resolveRectBounce, emitBounce, }); })(typeof window !== "undefined" ? window : globalThis);