163 lines
7.6 KiB
JavaScript
163 lines
7.6 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: entity-runtime/items/dynamic-system
|
|
// Owns ball item motion and obstacle bounce behavior. Item.prototype ball
|
|
// methods 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);
|
|
global.TarinaiItemDynamicPinSystem.tryBallPickup(item, 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 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;
|
|
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), 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 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 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,
|
|
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 = (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,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|