tarinai/js/physics_helpers.js
2026-07-10 16:40:06 +09:00

246 lines
12 KiB
JavaScript

"use strict";
(function (global) {
const Geometry = global.TarinaiGeometry;
function circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts = {}) {
if (Geometry.oneWayFenceAllows(obj, rect)) return false;
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
const minBounceSpeed = Math.max(0, Number(opts.minBounceSpeed ?? rect.minBounceSpeed ?? 0) || 0);
const local = Geometry.rectLocalPoint(rect, obj.x, obj.y);
let clx = clamp(local.x, -(rect.halfW || 0), rect.halfW || 0);
let cly = clamp(local.y, -(rect.halfH || 0), rect.halfH || 0);
let dx = local.x - clx;
let dy = local.y - cly;
let d = Math.hypot(dx, dy);
if (d >= hitRadius) {
const px = obj.prevX;
const py = obj.prevY;
if (!Number.isFinite(px) || !Number.isFinite(py)) return false;
const prev = Geometry.rectLocalPoint(rect, px, py);
const expanded = { left: -(rect.halfW || 0) - hitRadius, right: (rect.halfW || 0) + hitRadius, top: -(rect.halfH || 0) - hitRadius, bottom: (rect.halfH || 0) + hitRadius };
const insideNow = local.x >= expanded.left && local.x <= expanded.right && local.y >= expanded.top && local.y <= expanded.bottom;
const insidePrev = prev.x >= expanded.left && prev.x <= expanded.right && prev.y >= expanded.top && prev.y <= expanded.bottom;
if (!insideNow && !insidePrev) return false;
const sx = local.x - prev.x;
const sy = local.y - prev.y;
const candidates = [];
if (prev.x < expanded.left && sx > 0) candidates.push({ nx: -1, ny: 0, t: (expanded.left - prev.x) / Math.max(sx, 0.001) });
if (prev.x > expanded.right && sx < 0) candidates.push({ nx: 1, ny: 0, t: (prev.x - expanded.right) / Math.max(-sx, 0.001) });
if (prev.y < expanded.top && sy > 0) candidates.push({ nx: 0, ny: -1, t: (expanded.top - prev.y) / Math.max(sy, 0.001) });
if (prev.y > expanded.bottom && sy < 0) candidates.push({ nx: 0, ny: 1, t: (prev.y - expanded.bottom) / Math.max(-sy, 0.001) });
if (!candidates.length) return false;
candidates.sort((a, b) => a.t - b.t);
dx = candidates[0].nx;
dy = candidates[0].ny;
d = 1;
} else if (d < 0.001) {
const left = Math.abs(local.x + (rect.halfW || 0));
const right = Math.abs((rect.halfW || 0) - local.x);
const top = Math.abs(local.y + (rect.halfH || 0));
const bottom = Math.abs((rect.halfH || 0) - local.y);
const m = Math.min(left, right, top, bottom);
if (m === left) { dx = -1; dy = 0; clx = -(rect.halfW || 0); cly = local.y; }
else if (m === right) { dx = 1; dy = 0; clx = rect.halfW || 0; cly = local.y; }
else if (m === top) { dx = 0; dy = -1; clx = local.x; cly = -(rect.halfH || 0); }
else { dx = 0; dy = 1; clx = local.x; cly = rect.halfH || 0; }
d = 1;
}
const nxLocal = dx / d;
const nyLocal = dy / d;
const n = Geometry.rectWorldVector(rect, nxLocal, nyLocal);
const nx = n.x;
const ny = n.y;
const worldClosest = Geometry.rectWorldVector(rect, clx, cly);
obj.x = (rect.cx || 0) + worldClosest.x + nx * (hitRadius + 0.5);
obj.y = (rect.cy || 0) + worldClosest.y + ny * (hitRadius + 0.5);
const vx = obj.vx || 0;
const vy = obj.vy || 0;
applyNormalBounce(obj, nx, ny, vx, vy, restitution, 36, minBounceSpeed, rect.mechanical ? 0 : 7.0, 46);
if (rect.mechanical) {
global.TarinaiMechanicalSystem.applySurfaceVelocityToCircle(obj, rect, nx, ny, vx, vy, {
damping: 1,
surfaceScale: rect.rotator ? 0.28 : 0.30,
normalBoost: rect.rotator ? 20 : 14,
maxSpeed: 900,
passiveImpulseScale: 1.0,
spinKick: 7.0,
});
}
return true;
}
function applyNormalBounce(obj, nx, ny, vx, vy, restitution, fallbackImpulse, minBounceSpeed, spinKick, spinLimit) {
const toward = vx * nx + vy * ny;
if (toward < 0) {
obj.vx = vx - (1 + restitution) * toward * nx;
obj.vy = vy - (1 + restitution) * toward * ny;
} else {
obj.vx = vx + nx * fallbackImpulse;
obj.vy = vy + ny * fallbackImpulse;
}
if (minBounceSpeed > 0) {
const normalSpeed = (obj.vx || 0) * nx + (obj.vy || 0) * ny;
if (normalSpeed < minBounceSpeed) {
const add = minBounceSpeed - normalSpeed;
obj.vx = (obj.vx || 0) + nx * add;
obj.vy = (obj.vy || 0) + ny * add;
}
}
if (Number.isFinite(obj.spinVelocity) && spinKick) obj.spinVelocity = clamp((obj.spinVelocity || 0) + (nx >= 0 ? -1 : 1) * spinKick, -spinLimit, spinLimit);
return true;
}
function bounceInsideWorld(obj, worldRef, opts = {}) {
if (!obj || !worldRef) return false;
const p = Math.max(opts.padding ?? 28, CONFIG.worldPadding || 30);
const bx = opts.bounceX ?? opts.bounce ?? 0.72;
const by = opts.bounceY ?? opts.bounce ?? 0.72;
const spinBounce = opts.spinBounce ?? -0.72;
let bounced = false;
if (obj.x < p) { obj.x = p; obj.vx = Math.abs(obj.vx || 0) * bx; bounced = true; }
if (obj.x > worldRef.w - p) { obj.x = worldRef.w - p; obj.vx = -Math.abs(obj.vx || 0) * bx; bounced = true; }
if (obj.y < p) { obj.y = p; obj.vy = Math.abs(obj.vy || 0) * by; bounced = true; }
if (obj.y > worldRef.h - p) { obj.y = worldRef.h - p; obj.vy = -Math.abs(obj.vy || 0) * by; bounced = true; }
if (bounced && Number.isFinite(obj.spinVelocity)) obj.spinVelocity *= spinBounce;
return bounced;
}
function bounceCircleOffRect(obj, rect, radius, restitution = 4.20, worldRef = null, opts = {}) {
if (!obj || !rect) return false;
if (rect.oriented) return circleBounceOffOrientedRect(obj, rect, radius, restitution, worldRef, opts);
const hitRadius = Math.max(2, Number(radius || obj.r || obj.radius || 12) || 12);
const minBounceSpeed = Math.max(0, Number(opts.minBounceSpeed ?? rect.minBounceSpeed ?? 0) || 0);
const fallbackImpulse = Math.max(0, Number(opts.fallbackImpulse ?? 36) || 0);
const spinKick = Number(opts.spinKick ?? 7.0);
const spinLimit = Math.max(0, Number(opts.spinLimit ?? 46) || 46);
const cx = clamp(obj.x, rect.left, rect.right);
const cy = clamp(obj.y, rect.top, rect.bottom);
let dx = obj.x - cx;
let dy = obj.y - cy;
let d = Math.hypot(dx, dy);
if (d >= hitRadius) {
const px = obj.prevX;
const py = obj.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, obj.x, obj.y, expanded)) return false;
const vx = obj.vx || 0;
const vy = obj.vy || 0;
const candidates = [];
const sx = obj.x - px;
const sy = obj.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) });
let nx = 0;
let ny = 0;
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;
if (nx < 0) obj.x = expanded.left - 0.5;
else if (nx > 0) obj.x = expanded.right + 0.5;
if (ny < 0) obj.y = expanded.top - 0.5;
else if (ny > 0) obj.y = expanded.bottom + 0.5;
if (nx) obj.y = clamp(obj.y, expanded.top, expanded.bottom);
if (ny) obj.x = clamp(obj.x, expanded.left, expanded.right);
return applyNormalBounce(obj, nx, ny, vx, vy, restitution, fallbackImpulse, minBounceSpeed, spinKick, spinLimit);
}
if (d < 0.001) {
const left = Math.abs(obj.x - rect.left);
const right = Math.abs(rect.right - obj.x);
const top = Math.abs(obj.y - rect.top);
const bottom = Math.abs(rect.bottom - obj.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 vx = obj.vx || 0;
const vy = obj.vy || 0;
obj.x = cx + nx * (hitRadius + 0.5);
obj.y = cy + ny * (hitRadius + 0.5);
return applyNormalBounce(obj, nx, ny, vx, vy, restitution, fallbackImpulse, minBounceSpeed, spinKick, spinLimit);
}
function resolveFenceBounce(item, worldRef, dt = 0.016, opts = {}) {
if (!item || !worldRef?.nearbySolidObstacleRects) return false;
const speed = Math.hypot(item.vx || 0, item.vy || 0);
const radius = Math.max(4, (item.r || item.radius || 12) + (opts.obstaclePadding ?? 2));
const searchRadius = Math.max(96, radius + speed * Math.max(dt || 0.016, 0.016) + 90);
let bounced = false;
const rects = worldRef.nearbySolidObstacleRects(item.x, item.y, searchRadius, { include: it => it?.type === "bounce_fence" || it?.type === "bounce_fence_v", exclude: item, maxChecks: 16 }) || [];
for (const rect of rects) {
if (!rect?.bounce) continue;
if (bounceCircleOffRect(item, rect, radius, Number(rect.restitution || 4.20), worldRef, opts)) {
bounced = true;
const maxSpeed = opts.bounceFenceMaxSpeed ?? 1680;
item.vx = clamp(item.vx || 0, -maxSpeed, maxSpeed);
item.vy = clamp(item.vy || 0, -maxSpeed, maxSpeed);
const now = worldRef.time || 0;
if ((item.lastBounceFenceEffectAt || -999) + 0.08 <= now) {
item.lastBounceFenceEffectAt = now;
worldRef.effects?.push(new Effect("ring", item.x, item.y, { size: Math.max(12, radius * 1.15), life: 0.16, color: "rgba(82,153,230,0.45)" }));
}
}
}
return bounced;
}
function applyKinematicItemMotion(item, worldRef, dt, opts = {}) {
if (!item || !worldRef) return 0;
const stopSpeed = opts.stopSpeed ?? 0.05;
const speed = Math.hypot(item.vx || 0, item.vy || 0);
item.prevX = item.x;
item.prevY = item.y;
if (speed <= stopSpeed) {
item.vx = 0;
item.vy = 0;
if (Number.isFinite(item.spinVelocity)) {
item.spinVelocity *= Math.pow(opts.spinRestDamp ?? 0.08, dt);
item.spin = (item.spin || 0) + (item.spinVelocity || 0) * dt;
}
return speed;
}
item.x += (item.vx || 0) * dt;
item.y += (item.vy || 0) * dt;
bounceInsideWorld(item, worldRef, opts);
resolveFenceBounce(item, worldRef, dt, opts);
const ice = (worldRef?.groundType || "soil") === "ice";
const baseFriction = ice ? Math.max(Number(opts.frictionBase ?? 0.68), 0.93) : (opts.frictionBase ?? 0.68);
const frictionRate = ice ? Math.min(Number(opts.frictionRate ?? 1), 0.55) : (opts.frictionRate ?? 1);
const friction = Math.pow(baseFriction, dt * frictionRate);
item.vx *= friction;
item.vy *= friction;
if (opts.spin !== false) {
item.spin = (item.spin || 0) + (item.spinVelocity || 0) * dt + (item.vx || 0) * dt / Math.max(7, item.r || 16);
item.spinVelocity = (item.spinVelocity || 0) * Math.pow(opts.spinFrictionBase ?? 0.38, dt);
}
if (Math.hypot(item.vx || 0, item.vy || 0) < (opts.zeroBelow ?? 0)) {
item.vx = 0;
item.vy = 0;
if (Number.isFinite(item.spinVelocity)) item.spinVelocity *= 0.3;
}
worldRef.drawListDirty = true;
return speed;
}
global.TarinaiPhysics = {
oneWayFenceAllowsMotion(...args) {
return global.TarinaiGeometry.oneWayFenceAllowsMotion(...args);
},
oneWayFenceAllows(...args) {
return global.TarinaiGeometry.oneWayFenceAllows(...args);
},
bounceCircleOffRect,
applyKinematicItemMotion,
};
})(typeof window !== "undefined" ? window : globalThis);