feat(cheap-gen): 阶段一B #4 gamefeel 加 CoyoteTimer/ComboWindow 工厂方法(免 new+传 time)

附A #4:gamefeel 的 CoyoteTimer/ComboWindow 是 class、须 new 且要传受控 TimeSource——AI 拿不到
受控 time、做不出来。加工厂方法 gamefeel.createCoyoteTimer(opts)/createComboWindow(opts):
用 init 时捕获的受控时间源自动注入,AI 直接调、免 new + 免找 time。全链路同改:
- gamefeel/impl.js:init 捕获 ctx.time,加两个工厂方法(new CoyoteTimer/ComboWindow(time,opts))
- gamefeel/api.d.ts:GamefeelPlugin 接口加两个工厂声明
- skill line115:列工厂方法(免 new + 自动注入受控 time);内置 inputBuffer 改 gamefeel.inputBuffer
验证:gamefeel 18/18 + 确定性(init 后工厂可调、返带 isAvailable/hit 的实例)。prompt 不提 gamefeel 故不同改。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-29 10:14:09 -07:00
parent b29a379f9c
commit 5cb73a6d82
3 changed files with 24 additions and 1 deletions

View File

@ -112,7 +112,7 @@ export function createGame({ plugins, bundle, viewport }) {
|---|---|---|
| **collision** | 碰撞查询(纯数学) | `circleVsCircle/circleVsAabb/aabbVsAabb/satVsPolygon`→`{hit,depth,nx,ny}`;`raycast*`;`SpatialHash`。**⚠️ Aabb=中心+半宽半高 `{x,y,hw,hh}`(非 {x,y,w,h});Circle `{x,y,r}`** |
| **physics-lite** | 街机运动原语(非刚体) | `Projectile{step,reset}`/`integrateProjectile(state,dt,opts)`;`springStep`/`criticalDamping`;`moveWithConstraint`;`clampSpeed`/`applyFriction`。**⚠️ ProjectileState=`{px,py,vx,vy}`(位置是 px/py 非 x/y);重力经 opts.gravity={x,y}`** |
| **gamefeel** | 手感 | `easing`(13 曲线 in/out/inOut);`InputBuffer{consume,peek}`;`CoyoteTimer{setGrounded,isAvailable,consume}`;`ComboWindow{hit,getCount}` |
| **gamefeel** | 手感 | `easing`(13 曲线 in/out/inOut);内置 `gamefeel.inputBuffer{consume,peek}`;**工厂(免 new + 自动注入受控 time,#4)**:`gamefeel.createCoyoteTimer(opts)→{setGrounded,isAvailable,consume}`、`gamefeel.createComboWindow(opts)→{hit,getCount}` |
| **particles-juice** | 粒子 + 打击感 | `spawnEmitter(preset\|cfg,x,y)`(或 `burst(x,y)` 直发 burst 预设,#6/#10 母语别名);`hitStop(dur)`/`getTimeScale`;`shakeScreen`/`getShakeOffset`;`flashScreen`/`pulseScale`;`PRESETS`(burst/trail/drift)。**⚠️ `render(g)` 必须每帧手调(非自动);`step(dt)` 默认 autoStep 自驱(经 onFrame),要手调 step 须工厂传 `autoStep:false` 防双步进** |
| **palette-post** | 调色 + 后处理 | `rgbToHsl/hslToRgb/applyPaletteMap/shiftHsl`;`setPost/renderPost`(vignette/dither/scanline)。**色板由你注入,插件不内置成品色** |
| **audio-music** | 程序化音频 | `loadSong/play/stop/loop`;`setIntensity`/`setIntensityLayering`;`playSfx('blip'\|'thud'\|'chime')` |

View File

@ -244,6 +244,10 @@ export interface GamefeelPlugin extends Plugin {
* 永久走内置(引擎门面刻意不暴此二者)。
*/
readonly easing: EasingLib;
/** #4:用插件 init 时捕获的受控时间源造一个 CoyoteTimer(免自己 new + 找 time)。init 后可用。 */
createCoyoteTimer(opts?: CoyoteTimerOptions): CoyoteTimer;
/** #4:用受控时间源造一个 ComboWindow(免自己 new + 找 time)。init 后可用。 */
createComboWindow(opts?: ComboWindowOptions): ComboWindow;
}
/**

View File

@ -456,6 +456,8 @@ export function createGamefeelPlugin(opts) {
/** @type {EngineMath|null} */
let engMath = null;
let disposed = false;
/** @type {TimeSource|null} init 时捕获的受控时间源,供 #4 工厂方法 createCoyoteTimer/createComboWindow 注入。 */
let time = null;
/** @type {GamefeelPlugin} */
const plugin = {
@ -471,6 +473,22 @@ export function createGamefeelPlugin(opts) {
get easing() {
return makeEasingFacade(engMath);
},
/**
* #4 阶段一B:用受控时间源造一个 CoyoteTimer(AI 直觉 gamefeel.createCoyoteTimer(opts),免自己 new + 找 time)。
* @param {CoyoteTimerOptions} [opts]
* @returns {CoyoteTimer}
*/
createCoyoteTimer(opts) {
return new CoyoteTimer(time, opts);
},
/**
* #4 阶段一B:用受控时间源造一个 ComboWindow(AI 直觉 gamefeel.createComboWindow(opts))。
* @param {ComboWindowOptions} [opts]
* @returns {ComboWindow}
*/
createComboWindow(opts) {
return new ComboWindow(time, opts);
},
/**
* 初始化:经受控面 getInput()+time 接一个内置 InputBuffer(演示受控面正确接线),
* 并缓存引擎 math 门面句柄(供 easing getter 门面优先取引擎 Ease)。
@ -479,6 +497,7 @@ export function createGamefeelPlugin(opts) {
init(ctx) {
// 经受控面拿输入订阅与时间源(禁直接 addEventListener / Date.now)。
const input = ctx.getInput();
time = ctx.time; // #4:缓存受控时间源,供 createCoyoteTimer/createComboWindow 工厂注入。
inputBuffer = new InputBuffer(input, ctx.time, {
windowMs: options.inputBufferMs,
types: options.inputTypes,