diff --git a/.agents/skills/littlejs-game-dev.md b/.agents/skills/littlejs-game-dev.md index aaf0bbea..145195a0 100644 --- a/.agents/skills/littlejs-game-dev.md +++ b/.agents/skills/littlejs-game-dev.md @@ -115,7 +115,7 @@ export function createGame({ plugins, bundle, viewport }) { | **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')` | +| **audio-music** | 程序化音频 | `loadSong`(载曲)`/play(intensity?)`(播已载曲;**入参是情绪强度 0..1、不是曲目名**,#9)`/stop/loop`;`setIntensity`;**`playSfx(name)`(#8 放开)**:预设 `blip/thud/chime` 或语义名 `click/hit/win/coin/jump/fail` 等(自动映射、未知兜底 blip) | | **save-progress** | KV 持久化 | `get(key,default)/set/remove/clear`;`createMemoryAdapter`/`createLocalStorageAdapter` | | **runtime-probe** | 启动取证(六锚) | `mark(anchor)/getRecords/toJSONL/verify/verifyChain` | diff --git a/game-runtime/src/plugins/audio-music/api.d.ts b/game-runtime/src/plugins/audio-music/api.d.ts index 082b5d53..2aebb935 100644 --- a/game-runtime/src/plugins/audio-music/api.d.ts +++ b/game-runtime/src/plugins/audio-music/api.d.ts @@ -158,7 +158,7 @@ export interface AudioMusicPlugin extends Plugin { * - 无已载入曲谱 → no-op; * - `getAudioContext()===null` → 静默 no-op(告警一次); * - 否则经受控音频上下文渲染并播放。 - * @param intensity 可选初始情绪强度(0..1),用于按情绪分层决定起始声部增益(缺省 1=全声部)。 + * @param intensity 可选初始情绪强度(0..1)——**#9:这是强度数值、不是曲目名**(曲子先 loadSong 载入,play 只负责播);非数容错为默认强度。缺省 1=全声部。 */ play(intensity?: number): void; /** 停止当前播放(停 song 与所有 loop)。无上下文/未播放时安全 no-op。 */ @@ -188,10 +188,11 @@ export interface AudioMusicPlugin extends Plugin { */ resolveVoices(intensity: number): VoiceState[]; /** - * 播放一个内置 ZzFX 音效预设(blip/thud/chime)。无上下文时静默 no-op(告警一次)。 - * @param name 预设名。 + * 播放一个音效(#8)。name 可是预设名 blip/thud/chime,也可是常用语义名(click/hit/win/coin/jump/fail 等,自动映射到预设); + * 未识别的名兜底播 blip(不静默)。无音频上下文时 no-op。 + * @param name 预设名或语义名。 */ - playSfx(name: SfxPresetName): void; + playSfx(name: SfxPresetName | string): void; /** 取内置预设的参数包(纯数据,恒可用;便于 node 层 schema 校验与诊断)。 */ getPreset(name: SfxPresetName): SfxPreset | undefined; /** 取全部内置预设名(≥3)。 */ diff --git a/game-runtime/src/plugins/audio-music/impl.js b/game-runtime/src/plugins/audio-music/impl.js index 282fb65f..eb573099 100644 --- a/game-runtime/src/plugins/audio-music/impl.js +++ b/game-runtime/src/plugins/audio-music/impl.js @@ -78,6 +78,19 @@ const SFX_PRESETS = Object.freeze({ /** 预设名列表(≥3)。 */ const SFX_PRESET_NAMES = /** @type {SfxPresetName[]} */ (Object.keys(SFX_PRESETS)); +/** + * #8 阶段一B:常用语义音效名 → 三个中性预设的映射(放开 playSfx,让 AI 直觉名 'click'/'hit'/'win' 等可用)。 + * 三类音色:blip=短促清脆点击 / thud=低频闷响 / chime=明亮带余韵。未列入的名兜底 blip(不静默)。 + */ +const SFX_ALIASES = Object.freeze({ + // → blip(清脆点击/取得) + click: 'blip', tap: 'blip', select: 'blip', beep: 'blip', pop: 'blip', coin: 'blip', point: 'blip', score: 'blip', shoot: 'blip', jump: 'blip', + // → thud(撞击/失败/受伤) + hit: 'thud', land: 'thud', bump: 'thud', hurt: 'thud', damage: 'thud', crash: 'thud', explosion: 'thud', fail: 'thud', lose: 'thud', gameover: 'thud', + // → chime(成功/奖励/升级) + win: 'chime', success: 'chime', ding: 'chime', levelup: 'chime', star: 'chime', powerup: 'chime', bonus: 'chime', unlock: 'chime', pickup: 'chime', win1: 'chime', +}); + /* ============================================================================ * 二、曲谱(Song)数据结构校验 —— 纯逻辑,恒可用 * ========================================================================== */ @@ -453,7 +466,8 @@ export function createAudioMusicPlugin(opts) { }, play(startIntensity) { - startPlayback(false, startIntensity); + // #9:入参是「情绪强度 0..1」、不是曲目名(曲子先 loadSong 载入,play 只播);非数容错为 undefined(用默认强度),防误传字符串致 NaN 强度。 + startPlayback(false, (typeof startIntensity === 'number' && Number.isFinite(startIntensity)) ? startIntensity : undefined); }, stop() { @@ -461,7 +475,8 @@ export function createAudioMusicPlugin(opts) { }, loop(startIntensity) { - startPlayback(true, startIntensity); + // #9:同 play——入参是情绪强度 0..1、非曲目名;非数容错为 undefined。 + startPlayback(true, (typeof startIntensity === 'number' && Number.isFinite(startIntensity)) ? startIntensity : undefined); }, setIntensityLayering(l) { @@ -479,11 +494,9 @@ export function createAudioMusicPlugin(opts) { }, playSfx(name) { - const preset = SFX_PRESETS[name]; - if (!preset) { - logWarn('[audio-music] 未知音效预设:' + String(name)); - return; - } + // #8:语义名映射 + 兜底——直接是预设名就用;命中语义别名映射到预设;否则兜底 blip(不静默,AI 用任意名都出声)。 + const resolved = SFX_PRESETS[name] ? name : (SFX_ALIASES[name] || 'blip'); + const preset = SFX_PRESETS[resolved]; const ac = audioOrSilent(); if (ac == null) return; // 静默降级(无音频上下文)。 try {