附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>
261 lines
13 KiB
TypeScript
261 lines
13 KiB
TypeScript
/**
|
||
* gamefeel/api.d.ts — 手感原语插件公开类型门面(手写,对齐 core-protocol-v0)
|
||
* owner:T1b-α lane-math | 消费方:所有 lane / agent 造游戏时读本件了解手感原语能力面
|
||
*
|
||
* 【本件定位】
|
||
* 「游戏手感原语」引擎能力的类型契约:输入缓冲 / coyote time / 缓动标准库 / 连击计时窗。
|
||
* 缓动为**纯函数**;输入缓冲/coyote/连击为**时间驱动状态机**(时间一律走受控时间源,禁 Date.now)。
|
||
*
|
||
* 【受控面用法(具名消费者)】
|
||
* 输入缓冲经受控面 `getInput`(订阅归一化输入事件)+ `time`(事件/查询时间戳)工作;
|
||
* coyote/连击窗经受控 `time` 推进。绝不直接 addEventListener / 读 Date.now / performance.now。
|
||
*
|
||
* 【受控面铁律(继承自 core)】
|
||
* 只经 PluginContext 受控面拿引擎能力,绝不直透 littlejsengine 裸对象 / 原生 DOM 事件。
|
||
*/
|
||
|
||
import type { Plugin, PluginContext, InputEventType, InputSource, TimeSource, EngineMath } from '../../core/api.d.ts';
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
* 缓动标准库(纯函数,端点恒等 f(0)=0、f(1)=1)
|
||
* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
/**
|
||
* 缓动函数:入参 t∈[0,1](外部应自行归一化进度),返回缓动后的进度。
|
||
* 约定:所有缓动 **f(0)=0、f(1)=1**(端点恒等);越界 t 由各实现钳制到 [0,1]。
|
||
*/
|
||
export type EasingFn = (t: number) => number;
|
||
|
||
/**
|
||
* 缓动标准库命名空间。每族含 in / out / inOut 三向(linear 仅一项)。
|
||
* 全部为纯函数、确定性、无副作用、不触受控面。
|
||
*/
|
||
export interface EasingLib {
|
||
/** 线性(恒等钳制版):f(t)=clamp(t,0,1)。 */
|
||
linear: EasingFn;
|
||
|
||
/** 二次缓入 t²。 */
|
||
quadIn: EasingFn;
|
||
/** 二次缓出。 */
|
||
quadOut: EasingFn;
|
||
/** 二次缓入缓出。 */
|
||
quadInOut: EasingFn;
|
||
|
||
/** 三次缓入 t³。 */
|
||
cubicIn: EasingFn;
|
||
/** 三次缓出。 */
|
||
cubicOut: EasingFn;
|
||
/** 三次缓入缓出。 */
|
||
cubicInOut: EasingFn;
|
||
|
||
/** 弹性缓入(末段回弹超调后收敛;端点仍恒等)。 */
|
||
elasticIn: EasingFn;
|
||
/** 弹性缓出。 */
|
||
elasticOut: EasingFn;
|
||
/** 弹性缓入缓出。 */
|
||
elasticInOut: EasingFn;
|
||
|
||
/** 回弹(back)缓入:先回拉再冲过(中段越界 [0,1],端点恒等)。 */
|
||
backIn: EasingFn;
|
||
/** 回弹缓出。 */
|
||
backOut: EasingFn;
|
||
/** 回弹缓入缓出。 */
|
||
backInOut: EasingFn;
|
||
}
|
||
|
||
/**
|
||
* 缓动模块级单例(向后兼容导出,= 内置 builtinEasing fallback 实现)。
|
||
* 【2026-06-13 easing 改判】模块级无 ctx 拿不到引擎 → 此导出恒为内置纯函数 fallback(行为/签名不变);
|
||
* **门面包装引擎 Ease 经插件实例 `GamefeelPlugin.easing` getter 暴露**(init 经 ctx.getEngine().math 门面优先)。
|
||
*/
|
||
export declare const easing: EasingLib;
|
||
|
||
/**
|
||
* 缓动门面工厂(impl 内部 export-for-test):造一份「门面优先、null-engine 退内置 fallback」的缓动 family。
|
||
* 门面分支 clamp01(t) 后调引擎裸曲线(保越界钳语义);engMath 缺位/缺某曲线 → 该路退内置(backInOut/
|
||
* elasticInOut 因引擎门面刻意不暴而永久退内置,是补层非 fallback)。供单测验门面转发/null 退内置。
|
||
* @param engMath 引擎 math 门面句柄(ctx.getEngine()?.math),缺位时整体退内置 fallback
|
||
*/
|
||
export declare function makeEasingFacade(engMath: EngineMath | null | undefined): EasingLib;
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
* 输入缓冲(可配窗口 ms,经受控面 getInput + time)
|
||
* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
/** InputBuffer 构造选项。 */
|
||
export interface InputBufferOptions {
|
||
/**
|
||
* 缓冲窗口(毫秒):一次输入在被记录后的 windowMs 内仍可被 `consume` 命中("提前按"宽容)。
|
||
* 缺省 120ms。必须 > 0。
|
||
*/
|
||
windowMs?: number;
|
||
/**
|
||
* 关注的输入事件类型集合(默认全 5 类:pointerdown/pointermove/pointerup/keydown/keyup)。
|
||
* 只缓冲这些类型的事件。
|
||
*/
|
||
types?: InputEventType[];
|
||
}
|
||
|
||
/**
|
||
* 输入缓冲:把「最近一次(按类型)的输入」记下时间戳,供逻辑帧在窗口内"补判"。
|
||
* 典型用途 = 容忍玩家略早于可执行时机的按键(窗口内仍算有效)。**无玩法语义**,只是时间窗内的输入记账。
|
||
*
|
||
* 工作方式:绑定受控 InputSource(订阅)+ 受控 TimeSource(取时间)。每条关注事件记录其 tMs;
|
||
* `consume(type, nowMs)` 若存在「nowMs - 记录时间 ≤ windowMs」的未消费记录则命中并清除(一次性消费)。
|
||
*
|
||
* **时间源铁律**:一切时间走受控 TimeSource(事件 tMs 来自受控时间源;查询 nowMs 调用方传或用 time.nowMs)。
|
||
*/
|
||
export declare class InputBuffer {
|
||
/**
|
||
* @param input 受控输入订阅面(来自 ctx.getInput())
|
||
* @param time 受控时间源(来自 ctx.time)
|
||
* @param opts 窗口/类型
|
||
*/
|
||
constructor(input: InputSource, time: TimeSource, opts?: InputBufferOptions);
|
||
/**
|
||
* 查询并消费:若该 type 有「在窗口内」的未消费输入记录,返回 true 并清除该记录(一次性)。
|
||
* @param type 输入类型
|
||
* @param nowMs 当前时刻(毫秒);缺省用受控 time.nowMs()
|
||
* @returns 是否命中缓冲
|
||
*/
|
||
consume(type: InputEventType, nowMs?: number): boolean;
|
||
/**
|
||
* 只查不消费:该 type 是否有窗口内的有效缓冲(不清除)。
|
||
* @param type @param nowMs 缺省用受控 time.nowMs()
|
||
*/
|
||
peek(type: InputEventType, nowMs?: number): boolean;
|
||
/** 清空全部缓冲记录。 */
|
||
clear(): void;
|
||
/** 注销输入订阅(释放,幂等)。插件 dispose 时调;注册器另会兜底回收。 */
|
||
dispose(): void;
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
* coyote time 计时器
|
||
* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
/** CoyoteTimer 构造选项。 */
|
||
export interface CoyoteTimerOptions {
|
||
/**
|
||
* coyote 宽限窗口(毫秒):自「条件变为不满足(如离地)」起,该窗口内 `isAvailable` 仍返回 true。
|
||
* 缺省 100ms。必须 > 0。
|
||
*/
|
||
windowMs?: number;
|
||
}
|
||
|
||
/**
|
||
* coyote time 计时器:把「某条件刚结束后的一小段宽限期」记账。
|
||
* 典型用途 = 离开平台边缘后仍宽限片刻可执行某动作。**无玩法语义**,只是「条件失效后的时间宽限」记账。
|
||
*
|
||
* 工作方式:`setGrounded(true/false, nowMs)` 标记条件状态变化(true→false 时记下失效时刻);
|
||
* `isAvailable(nowMs)`:条件为 true 时恒可用;条件刚 false 但「nowMs - 失效时刻 ≤ windowMs」仍可用。
|
||
* `consume()` 用掉本次宽限(防一次宽限多次触发)。时间走受控 TimeSource。
|
||
*/
|
||
export declare class CoyoteTimer {
|
||
/**
|
||
* @param time 受控时间源
|
||
* @param opts 窗口
|
||
*/
|
||
constructor(time: TimeSource, opts?: CoyoteTimerOptions);
|
||
/**
|
||
* 标记条件状态(如"是否在地面/可执行区")。true→false 跳变时记下失效时刻(宽限期起点)。
|
||
* @param grounded 条件是否满足
|
||
* @param nowMs 当前时刻;缺省用受控 time.nowMs()
|
||
*/
|
||
setGrounded(grounded: boolean, nowMs?: number): void;
|
||
/**
|
||
* 当前是否处于可用窗口(条件满足,或条件刚失效但在宽限窗口内且未被消费)。
|
||
* @param nowMs 缺省用受控 time.nowMs()
|
||
*/
|
||
isAvailable(nowMs?: number): boolean;
|
||
/** 消费本次宽限(用掉后宽限期内 isAvailable 返回 false,直到条件再次满足复位)。 */
|
||
consume(): void;
|
||
/** 复位(清宽限与消费标记)。 */
|
||
reset(): void;
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
* 连击计时窗
|
||
* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
/** ComboWindow 构造选项。 */
|
||
export interface ComboWindowOptions {
|
||
/**
|
||
* 连击窗口(毫秒):相邻两次 `hit` 间隔 ≤ windowMs 则连击数 +1,否则重置为 1。缺省 500ms。必须 > 0。
|
||
*/
|
||
windowMs?: number;
|
||
/** 连击上限(达到后不再增长,停在上限)。缺省 Infinity(不封顶)。 */
|
||
maxCount?: number;
|
||
}
|
||
|
||
/**
|
||
* 连击计时窗:按「相邻命中间隔是否在窗口内」累计连击数。**无玩法语义**,只是时间窗内的计数。
|
||
* 工作方式:`hit(nowMs)` 若距上次命中 ≤ windowMs 则 count+1,否则重置为 1;`getCount(nowMs)` 读当前连击
|
||
* (若已超窗则视为断连返回 0)。时间走受控 TimeSource。
|
||
*/
|
||
export declare class ComboWindow {
|
||
/**
|
||
* @param time 受控时间源
|
||
* @param opts 窗口/上限
|
||
*/
|
||
constructor(time: TimeSource, opts?: ComboWindowOptions);
|
||
/**
|
||
* 记一次命中:在窗口内则连击 +1(封顶 maxCount),否则重置为 1。
|
||
* @param nowMs 当前时刻;缺省用受控 time.nowMs()
|
||
* @returns 命中后的当前连击数
|
||
*/
|
||
hit(nowMs?: number): number;
|
||
/**
|
||
* 读当前连击数:若距上次命中已超窗口(断连)则返回 0,否则返回当前连击数。
|
||
* @param nowMs 缺省用受控 time.nowMs()
|
||
*/
|
||
getCount(nowMs?: number): number;
|
||
/** 复位连击(清零)。 */
|
||
reset(): void;
|
||
}
|
||
|
||
/* ──────────────────────────────────────────────────────────────────────────
|
||
* 插件工厂(把输入缓冲接上受控面;缓动经 plugin.easing getter 走 getEngine().math.easing 门面)
|
||
* ────────────────────────────────────────────────────────────────────────── */
|
||
|
||
/** gamefeel 插件可选配置。 */
|
||
export interface GamefeelPluginOptions {
|
||
/** 内置输入缓冲的窗口(毫秒,缺省 120)。 */
|
||
inputBufferMs?: number;
|
||
/** 内置输入缓冲关注的类型(缺省全 5 类)。 */
|
||
inputTypes?: InputEventType[];
|
||
}
|
||
|
||
/**
|
||
* gamefeel 插件实例 = core Plugin + 一个 init 时接上受控面的内置 InputBuffer 句柄。
|
||
* 缓动/coyote/连击为模块级导出(import 即用);插件额外托管一个「已接好 getInput+time」的 InputBuffer,
|
||
* 演示「受控面如何被正确接线」,dispose 时注销订阅。
|
||
*/
|
||
export interface GamefeelPlugin extends Plugin {
|
||
/**
|
||
* 插件内置的输入缓冲(init 后接上受控面;init 前为 null)。
|
||
* dispose 时其订阅被注销(注册器另会兜底回收)。
|
||
*/
|
||
readonly inputBuffer: InputBuffer | null;
|
||
/**
|
||
* 缓动 family(2026-06-13 easing 改判·additive 新增):门面优先——init 经 ctx.getEngine().math.easing
|
||
* 门面包装引擎 Ease(clamp01 包裹保越界钳语义),null-engine 退内置纯函数 fallback。签名 (t)=>number 不变。
|
||
* 与模块级 `easing`(恒内置 fallback)区别:此 getter 在引擎可用时走引擎 Ease(11 条),backInOut/elasticInOut
|
||
* 永久走内置(引擎门面刻意不暴此二者)。
|
||
*/
|
||
readonly easing: EasingLib;
|
||
/** #4:用插件 init 时捕获的受控时间源造一个 CoyoteTimer(免自己 new + 找 time)。init 后可用。 */
|
||
createCoyoteTimer(opts?: CoyoteTimerOptions): CoyoteTimer;
|
||
/** #4:用受控时间源造一个 ComboWindow(免自己 new + 找 time)。init 后可用。 */
|
||
createComboWindow(opts?: ComboWindowOptions): ComboWindow;
|
||
}
|
||
|
||
/**
|
||
* 创建 gamefeel 插件(可注册进 PluginRegistry)。
|
||
* init 时经 ctx.getInput()+ctx.time 接一个内置 InputBuffer(演示受控面接线);dispose 注销订阅。
|
||
* @param opts 可选配置
|
||
*/
|
||
export declare function createGamefeelPlugin(opts?: GamefeelPluginOptions): GamefeelPlugin;
|
||
|
||
export type { Plugin, PluginContext } from '../../core/api.d.ts';
|