feat(cheap-gen): 阶段一B #2+#13 physics 几何挂实例 + ProjectileState px/py→x/y
附A #2:physics 实例原为空 {}、几何运动函数仅模块级导出,AI 直觉 physics.integrateProjectile(...)
取不到。把 6 个运动原语(integrateProjectile/springStep/criticalDamping/moveWithConstraint/
clampSpeed/applyFriction)+ Projectile 类挂上实例(引模块级纯函数,不依赖 this),api.d.ts 接口同补。
附A #13:ProjectileState 位置 px/py → x/y(向母语直觉,与 Vec2/命中矩形的 x/y 统一)。impl/test/
PLUGIN.md 同步(px/py 仅 ProjectileState 字段,replace_all 安全)。
注:physics 当前无 prompt/skill 指引(便宜档 niche、projectile 罕用),本次是完整性 + 正确性
(实例可用 + 字段直觉),不改生成侧行为。验证:physics-lite 16/16 + 确定性(实例 integrateProjectile/
clampSpeed 可调、ProjectileState 返 x/y)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6d31c0d81
commit
d79e1b9f89
@ -41,11 +41,11 @@ import {
|
||||
|
||||
// —— 抛体弹道(有状态,逐帧 step) ——
|
||||
const proj = new Projectile(
|
||||
{ px: 0, py: 0, vx: 5, vy: -12 },
|
||||
{ x: 0, y: 0, vx: 5, vy: -12 },
|
||||
{ gravity: { x: 0, y: 20 }, fixedStep: 1 / 120 }
|
||||
);
|
||||
proj.step(0.016); // 推进一帧,返回新状态
|
||||
const st = proj.state; // { px, py, vx, vy }
|
||||
const st = proj.state; // { x, y, vx, vy }
|
||||
|
||||
// —— 弹簧朝目标 10 收敛(临界阻尼=最快不振荡) ——
|
||||
const k = 100, cc = criticalDamping(k); // 2√(k·1)=20
|
||||
|
||||
@ -29,10 +29,10 @@ export interface Vec2 {
|
||||
|
||||
/** 抛体状态:位置 + 速度。 */
|
||||
export interface ProjectileState {
|
||||
/** 位置横坐标。 */
|
||||
px: number;
|
||||
/** 位置纵坐标。 */
|
||||
py: number;
|
||||
/** 位置横坐标(#13:px→x,向母语直觉对齐)。 */
|
||||
x: number;
|
||||
/** 位置纵坐标(#13:py→y)。 */
|
||||
y: number;
|
||||
/** 速度横分量(单位/秒)。 */
|
||||
vx: number;
|
||||
/** 速度纵分量(单位/秒)。 */
|
||||
|
||||
@ -90,8 +90,8 @@ function semiImplicitStep(s, gx, gy, h) {
|
||||
// 半隐式:v += g*h; p += v*h。symplectic,能量行为比显式欧拉稳。
|
||||
s.vx += gx * h;
|
||||
s.vy += gy * h;
|
||||
s.px += s.vx * h;
|
||||
s.py += s.vy * h;
|
||||
s.x += s.vx * h;
|
||||
s.y += s.vy * h;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,7 +104,7 @@ function semiImplicitStep(s, gx, gy, h) {
|
||||
export function integrateProjectile(state, dtSeconds, opts) {
|
||||
const { gx, gy, fixedStep } = normProjectileOpts(opts);
|
||||
// 工作副本(不改入参)。
|
||||
const s = { px: state.px, py: state.py, vx: state.vx, vy: state.vy };
|
||||
const s = { x: state.x, y: state.y, vx: state.vx, vy: state.vy };
|
||||
let remaining = dtSeconds > 0 ? dtSeconds : 0;
|
||||
// 整步推进。
|
||||
while (remaining >= fixedStep - EPS) {
|
||||
@ -129,7 +129,7 @@ export class Projectile {
|
||||
*/
|
||||
constructor(init, opts) {
|
||||
/** @type {ProjectileState} 内部状态。 */
|
||||
this._s = { px: init.px, py: init.py, vx: init.vx, vy: init.vy };
|
||||
this._s = { x: init.x, y: init.y, vx: init.vx, vy: init.vy };
|
||||
const n = normProjectileOpts(opts);
|
||||
this._gx = n.gx;
|
||||
this._gy = n.gy;
|
||||
@ -138,7 +138,7 @@ export class Projectile {
|
||||
|
||||
/** @returns {ProjectileState} 当前状态快照(只读拷贝)。 */
|
||||
get state() {
|
||||
return { px: this._s.px, py: this._s.py, vx: this._s.vx, vy: this._s.vy };
|
||||
return { x: this._s.x, y: this._s.y, vx: this._s.vx, vy: this._s.vy };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,7 +161,7 @@ export class Projectile {
|
||||
* @param {ProjectileState} init
|
||||
*/
|
||||
reset(init) {
|
||||
this._s = { px: init.px, py: init.py, vx: init.vx, vy: init.vy };
|
||||
this._s = { x: init.x, y: init.y, vx: init.vx, vy: init.vy };
|
||||
}
|
||||
}
|
||||
|
||||
@ -344,6 +344,14 @@ export function createPhysicsLitePlugin(opts) {
|
||||
init(ctx) {
|
||||
void ctx;
|
||||
},
|
||||
// 运动/几何原语挂实例(#2 阶段一B:AI 直觉 physics.integrateProjectile(...)/clampSpeed(...) 等直接可用,无需从模块 import;引模块级纯函数/类)。
|
||||
integrateProjectile,
|
||||
springStep,
|
||||
criticalDamping,
|
||||
moveWithConstraint,
|
||||
clampSpeed,
|
||||
applyFriction,
|
||||
Projectile,
|
||||
/** 释放:无资源可释放;幂等占位。 */
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
|
||||
@ -43,18 +43,18 @@ function rng(seed) {
|
||||
* ========================================================================== */
|
||||
|
||||
test('integrateProjectile:无重力时为匀速直线(位置=p0+v0·t 精确)', () => {
|
||||
const s0 = { px: 0, py: 0, vx: 3, vy: -2 };
|
||||
const s0 = { x: 0, y: 0, vx: 3, vy: -2 };
|
||||
const s = integrateProjectile(s0, 1.0, { gravity: { x: 0, y: 0 }, fixedStep: 1 / 240 });
|
||||
// 无重力 → 匀速,位置应为 v0·t(半隐式欧拉对匀速精确)。
|
||||
assert.ok(Math.abs(s.px - 3) < 1e-9, 'px 应为 3');
|
||||
assert.ok(Math.abs(s.py - (-2)) < 1e-9, 'py 应为 -2');
|
||||
assert.ok(Math.abs(s.x - 3) < 1e-9, 'px 应为 3');
|
||||
assert.ok(Math.abs(s.y - (-2)) < 1e-9, 'py 应为 -2');
|
||||
// 速度不变。
|
||||
assert.ok(Math.abs(s.vx - 3) < 1e-12 && Math.abs(s.vy - (-2)) < 1e-12, '无重力速度不变');
|
||||
});
|
||||
|
||||
test('integrateProjectile:重力下逼近解析抛物线(小固定步长,容差受控)', () => {
|
||||
const g = { x: 0, y: 10 };
|
||||
const s0 = { px: 0, py: 0, vx: 5, vy: 0 };
|
||||
const s0 = { x: 0, y: 0, vx: 5, vy: 0 };
|
||||
const t = 2.0;
|
||||
const fixedStep = 1 / 1000; // 足够小,半隐式离散误差受控
|
||||
const s = integrateProjectile(s0, t, { gravity: g, fixedStep });
|
||||
@ -65,21 +65,21 @@ test('integrateProjectile:重力下逼近解析抛物线(小固定步长,
|
||||
// 速度解析:vy = g·t。
|
||||
const vyExact = g.y * t;
|
||||
|
||||
assert.ok(Math.abs(s.px - pxExact) < 1e-6, `px 应≈${pxExact},实得 ${s.px}`);
|
||||
assert.ok(Math.abs(s.x - pxExact) < 1e-6, `px 应≈${pxExact},实得 ${s.x}`);
|
||||
// 半隐式欧拉对位置有 O(½·g·h·t) 量级误差;小步长下放宽容差到解析值的 1%。
|
||||
assert.ok(Math.abs(s.py - pyExact) < pyExact * 0.01, `py 应≈${pyExact}(1% 容差),实得 ${s.py}`);
|
||||
assert.ok(Math.abs(s.y - pyExact) < pyExact * 0.01, `py 应≈${pyExact}(1% 容差),实得 ${s.y}`);
|
||||
assert.ok(Math.abs(s.vy - vyExact) < 1e-6, `vy 应≈${vyExact},实得 ${s.vy}`);
|
||||
});
|
||||
|
||||
test('integrateProjectile:时间步容差——固定步长越小越逼近解析(收敛)', () => {
|
||||
const g = { x: 0, y: 10 };
|
||||
const s0 = { px: 0, py: 0, vx: 0, vy: 0 };
|
||||
const s0 = { x: 0, y: 0, vx: 0, vy: 0 };
|
||||
const t = 1.0;
|
||||
const pyExact = 0.5 * g.y * t * t; // = 5
|
||||
// 三档固定步长,误差应单调减小(半隐式欧拉位置误差 ∝ 步长)。
|
||||
const errCoarse = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 50 }).py - pyExact);
|
||||
const errMid = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 200 }).py - pyExact);
|
||||
const errFine = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 800 }).py - pyExact);
|
||||
const errCoarse = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 50 }).y - pyExact);
|
||||
const errMid = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 200 }).y - pyExact);
|
||||
const errFine = Math.abs(integrateProjectile(s0, t, { gravity: g, fixedStep: 1 / 800 }).y - pyExact);
|
||||
assert.ok(errMid < errCoarse, `步长减小误差应减小:mid(${errMid}) < coarse(${errCoarse})`);
|
||||
assert.ok(errFine < errMid, `步长再减小误差应再减小:fine(${errFine}) < mid(${errMid})`);
|
||||
});
|
||||
@ -88,14 +88,14 @@ test('integrateProjectile:dt 切分一致性——同 fixedStep 下一步 vs
|
||||
const g = { x: 1, y: 10 };
|
||||
const opts = { gravity: g, fixedStep: 1 / 600 };
|
||||
// 一次推进 0.6s。
|
||||
const once = integrateProjectile({ px: 0, py: 0, vx: 2, vy: 0 }, 0.6, opts);
|
||||
const once = integrateProjectile({ x: 0, y: 0, vx: 2, vy: 0 }, 0.6, opts);
|
||||
// 分 6 次各推进 0.1s(用有状态 Projectile 累计)。
|
||||
const proj = new Projectile({ px: 0, py: 0, vx: 2, vy: 0 }, opts);
|
||||
const proj = new Projectile({ x: 0, y: 0, vx: 2, vy: 0 }, opts);
|
||||
for (let i = 0; i < 6; i++) proj.step(0.1);
|
||||
const many = proj.state;
|
||||
// fixedStep 整除 0.1 与 0.6,整步序列一致 → 结果应高度一致(残余皆为 0)。
|
||||
assert.ok(Math.abs(once.px - many.px) < 1e-6, `px 切分一致:${once.px} vs ${many.px}`);
|
||||
assert.ok(Math.abs(once.py - many.py) < 1e-6, `py 切分一致:${once.py} vs ${many.py}`);
|
||||
assert.ok(Math.abs(once.x - many.x) < 1e-6, `px 切分一致:${once.x} vs ${many.x}`);
|
||||
assert.ok(Math.abs(once.y - many.y) < 1e-6, `py 切分一致:${once.y} vs ${many.y}`);
|
||||
assert.ok(Math.abs(once.vy - many.vy) < 1e-9, 'vy 切分一致');
|
||||
});
|
||||
|
||||
@ -197,7 +197,7 @@ test('fuzz:integrateProjectile 极端参数全程无 NaN/Infinity', () => {
|
||||
const rand = rng(0x1CEB00DA);
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const state = {
|
||||
px: rand() * 2000 - 1000, py: rand() * 2000 - 1000,
|
||||
x: rand() * 2000 - 1000, y: rand() * 2000 - 1000,
|
||||
vx: rand() * 2000 - 1000, vy: rand() * 2000 - 1000,
|
||||
};
|
||||
const opts = {
|
||||
@ -207,7 +207,7 @@ test('fuzz:integrateProjectile 极端参数全程无 NaN/Infinity', () => {
|
||||
const dt = rand() * 2;
|
||||
const s = integrateProjectile(state, dt, opts);
|
||||
assert.ok(
|
||||
Number.isFinite(s.px) && Number.isFinite(s.py) && Number.isFinite(s.vx) && Number.isFinite(s.vy),
|
||||
Number.isFinite(s.x) && Number.isFinite(s.y) && Number.isFinite(s.vx) && Number.isFinite(s.vy),
|
||||
`integrateProjectile 第 ${i} 出现非有限值`
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user