From 8196e07c2c477eddeaa3b96c4433459bbcfea51b Mon Sep 17 00:00:00 2001 From: lili Date: Mon, 29 Jun 2026 10:18:21 -0700 Subject: [PATCH] =?UTF-8?q?feat(cheap-gen):=20=E9=98=B6=E6=AE=B5=E4=B8=80B?= =?UTF-8?q?=20#8+#9=20audioMusic=20=E9=9F=B3=E6=95=88=E6=94=BE=E5=BC=80?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=90=8D=20+=20play=20=E5=BC=BA=E5=BA=A6?= =?UTF-8?q?=E5=AE=B9=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 附A #8:playSfx 原限 blip/thud/chime 三预设,AI 直觉用 'click'/'hit'/'win'/'coin' 等语义名会静默失败。 加 SFX_ALIASES 语义名→预设映射(click/jump/coin→blip、hit/fail/land→thud、win/success/levelup→chime), 未识别名兜底 blip(不静默)——AI 用任意音名都出声。 附A #9:play(intensity) 入参是情绪强度 0..1、非曲目名,AI 误传字符串 → NaN 强度。play/loop 对非数容错 为默认强度;api.d.ts/skill 文档钉死"入参是强度非曲目名,曲子先 loadSong"。 全链路同改:impl(SFX_ALIASES + playSfx 兜底 + play/loop 容错)+ api.d.ts(playSfx 放宽 string + 文档) + skill line118。验证:audio-music 16/16 + 确定性(playSfx 任意名不抛、play 非数不抛)。prompt 不提 audioMusic 故仅 skill。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .agents/skills/littlejs-game-dev.md | 2 +- game-runtime/src/plugins/audio-music/api.d.ts | 9 ++++--- game-runtime/src/plugins/audio-music/impl.js | 27 ++++++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) 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 {