"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);