249 lines
11 KiB
JavaScript
249 lines
11 KiB
JavaScript
"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);
|