246 lines
12 KiB
JavaScript
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);
|