A 2D pixel art action-exploration game about restoring lost voices in a flooded underground archive.
🇨🇳 简体中文版 README 可用。
Work-in-progress vertical slice. Current milestone: playable 60-second room demonstrating "enter -> Pulse -> repair -> collect -> exit" core loop.
- Engine: Godot 4.6.3 (verified —
config/features=4.4retained for backward compat, parses clean on 4.6.3 perREVIEW_LOG.md#20) - Resolution: 480x270 internal, integer-scale to 1920x1080
- Language: GDScript
- Audio: Procedural SFX (pulse / footstep / glass-break / enemy hum / repair / damage) + 9 procedural BGM themes (
title_intro/hub_warm/archive_exploration/archive_bossfor single InkWarden in archive_03 /archive_boss_dualfor the two-Warden roomarchive_04/archive_dawnfor victory / hub return /archive_stormtier-3 boss phase-2 escalation — InkWarden half-health transition auto-switches /whisper_hollowfor late-game Hub — switched automatically once 2 archive rooms are cleared, see## BGM Palettebelow /silence_voidfor GAME_OVER_FAILURE + finale phase 1, see## Game States) — all generated at runtime viaAudioStreamWAVsynthesis insrc/scripts/audio_manager_enhanced.gd+ the 9-preset data table insrc/scripts/audio_presets.gd(no external audio files needed). Title screen pre-warms the BGM cache so the first scene switch is zero-latency. Per-bus volume (Master / Music / SFX / Ambience) configurable in-game via the Settings menu. Boss music override is ref-counted (T078) and supports intensity-tier upgrade (T080 / #59 T107). - Death & respawn: 1.5s lay-down + fade-out death animation (T075). Opens with a 0.15s slow-mo + red-tint freeze-frame (T092 —
Engine.time_scale = 0.2,modulateshifts toColor(1.4, 0.45, 0.45)for the "drained red" reading as the alpha decays), then the body folds. After death, by default the player is teleported to the Hub safe-room (T079) — toggle "死亡后回 Hub 安全区" off inSettings → Savesfor the classic "respawn at last Save Lantern" experience. - Two-stage archive lighting (M12 polish, T076): when a room's
voice_bellis repaired, the scene's modulate eases from cold ink-teal to a warm amber over 0.8s (stage 1) and then to a full warm wash over 2s once the room completes (stage 2). All 4 archive rooms opt in via"atmosphere": truein theirdata/rooms/archive_*.jsonfiles. - Local Godot binary:
godot/Godot_v4.6.3-stable_linux.x86_64(seegodot/README.mdand the Headless Godot Binary Setup section below for the first-time extraction +--importrecipe)
assets/ # Art, audio, and design reference assets
src/ # Source code
autoload/ # GameState, AudioManager singletons
scenes/ # Godot scene files (.tscn)
scripts/ # GDScript logic
docs/ # Design docs (Steam page, etc.)
scripts/ # Python asset pipeline tools
| Action | Keyboard | Gamepad |
|---|---|---|
| Move | A/D or Arrow Keys | Left Stick |
| Jump | Space or W | A Button |
| Pulse (push/shield-break) | J or Z | X Button |
| Bind (pull / stun / unlock gates) | K or X | Y Button |
| Cut (slice/sunder) | L or C | LB Button |
| Interact | E or Enter | B Button |
| Pause / Menu | ESC | Start Button |
| Save (auto + manual) | (walk onto a Save Lantern / Pause → 保存进度) | — |
| Continue from save | (Title screen → 继续修复, if a save exists) | — |
| Credits | (Title screen → 致谢 button) | — |
6 营销截图位于 docs/screenshots/(1920x1080 PNG,480x270 内部 4x 整数倍缩放):
01_title_screen.png— 标题屏(VOXGLASS + 4 按钮)02_hub_room.png— Hub 安全区 + 4 扇门 + 墨守者剪影03_archive_01_pulse.png— 第一档案房 + Saya + SilenceMote + Pulse 圆环04_archive_03_boss.png— 第三档案房 + InkWarden Boss05_archive_04_double_boss.png— 第四档案房「共鸣祭坛」+ 双 InkWarden06_shop_merchant.png— 无声商贩 + 商店 UI + 5 个永久升级
沙箱说明:本仓库 CI 沙箱无 Xvfb / Wayland / GL 上下文,Godot 4.6.3 headless 模式强制使用 dummy 渲染器,真实
Viewport.get_texture().get_image()返回 null。本轮 (#43) 用tools/generate_screenshot_mockups.py基于既有资产合成 6 张截图作为 M10 营销上线最后阻塞解除。真实 capture 工具 (tools/screenshot_capture.gd+tools/capture_screenshots_desktop.sh) 在桌面环境(带 Xvfb / X11 / 真机)可直接使用。详见tools/README.md。
Three save slots are persisted to user://saves/slot_N.json. Each slot captures:
current_room+current_scene(room id + .tscn path, soContinuereloads the right scene)health/resonance/shardsand therooms_completedset- unlocked
abilities(bind / cut) and the player's lastcheckpoint_position run_time_seconds(in-game timer)- All
achievementsunlocked so far (also written-through touser://achievements.jsonon every unlock, independently of slots)
Title screen shows a 继续修复 button only when at least one slot is occupied. The pause menu's 保存进度 button opens the same slot picker in save mode. Achievement unlocks always persist to disk the instant they're earned.
GameFlowController is a small state machine with six states. Every state transition routes a BGM play_music_track / play_music_finale call into AudioManagerEnhanced; see the BGM state-machine map for the visual version, or read the table below.
| State | Trigger | BGM | Audio API | Notes |
|---|---|---|---|---|
TITLE |
Game boot / Continue button | title_intro |
play_music_track("title_intro", 1200) |
16s D major hopeful pad; loops while the menu sits. |
PLAYING |
New Game / resume from pause / scene transition complete | hub_warm or archive_exploration or whisper_hollow |
play_music_track(scene_bgm_key, 800) |
The exact key is scene.bgm_key (or hub_warm for Hub). Boss rooms additionally call request_boss_music — see BOSS override below. |
PAUSED |
pause_requested signal (Esc / P) |
unchanged | none | BGM continues during pause; the pause menu mutes SFX only. Time-scale returns to 1.0 on resume. |
ROOM_TRANSITION |
Room door triggered | unchanged | none | Fades to black for 0.4s; the next scene's _ready calls play_music_track once its bgm_key is known. |
GAME_OVER_SUCCESS |
Player completes the final room | silence_void → archive_dawn (4.0s + 12.6s) |
play_music_finale() (T117) |
Two-stage finale. Phase 1 silence = "the world is gone"; phase 2 dawn = "the world breathes back in". A _current_music_key guard inside play_music_finale makes phase 2 honor a player-initiated hub-return. |
GAME_OVER_FAILURE |
Player HP ≤ 0 | silence_void |
play_music_track("silence_void", 1200) |
4s zero-amplitude loop matches the T093 cold-gray visual wash. Plays alongside T115 death-quote overlay + T116 InkWarden afterimage. |
InkWarden (or any enemy in the elite_enemies group) calls request_boss_music when it enters the scene. The override is ref-counted and tier-ranked so multi-boss rooms don't lose their BGM when the first boss dies, and a phase-2 upgrade automatically supersedes phase-1:
| Boss event | Override key | Tier (vs current) | Effect |
|---|---|---|---|
| Single InkWarden enters | archive_boss (A minor 108 BPM) |
1 | Forces play_music_track("archive_boss") regardless of PLAYING state routing. |
| Second InkWarden in same room | archive_boss_dual (A minor 132 BPM) |
2 > 1 | Mid-fight cross-fade upgrade. |
| InkWarden Phase 2 | archive_storm (E minor 120 BPM) |
3 > 1, 2 | Most intense preset; sustained chaos. |
| Last boss dies / despawns | cleared | 0 | Returns to the scene's bgm_key (archive_exploration). |
Boss music and finale music are orthogonal: if the player dies during a boss fight, GAME_OVER_FAILURE is reached from PLAYING+boss_override. The override is released by release_boss_music() before the GFC routes to silence_void, so the failure path is clean.
The 9 procedural BGM themes are intentionally spread across the major and minor modes so each room / event reads as a different "tonal room". The keys below are MIDI numbers (A4 = 69); the chord column lists the 3- or 4-note voicing layered on top of the root sine. See src/scripts/audio_presets.gd for the full data table and per-preset design notes.
| Key | Mood | Root | Chord (MIDI) | BPM | Loop | When it plays |
|---|---|---|---|---|---|---|
title_intro |
sparse / hopeful | D3 (50) | D maj — D4 F#4 A4 | 60 | 16.0s | TITLE state (game boot) |
hub_warm |
warm / bright | F2 (41) | F maj — F3 A3 C4 | 88 | 10.9s | Early Hub (rooms_completed.size() < 2) |
archive_exploration |
melancholic / deep | A2 (45) | A min — A3 C4 E4 | 72 | 13.3s | Archive rooms (PLAYING + RoomController) |
archive_boss |
tense / single boss | A1 (33) | A min + tritone — A2 C3 F#3 | 108 | 11.1s | First InkWarden in room (tier 1) |
archive_boss_dual |
frantic / dual boss | A1 (33) | A min + tritone + aug5 — A2 C3 F#3 G#3 | 132 | 8.7s | Second InkWarden in room (tier 2) |
archive_dawn |
bright / victory | G2 (43) | G maj — G3 B3 D4 | 76 | 12.6s | GAME_OVER_SUCCESS finale phase 2 + full_archive unlock |
archive_storm |
chaos / phase 2 | E1 (28) | E min + aug4 + raised 7th — E2 G#2 B2 D3 | 120 | 10.0s | InkWarden enters phase 2 (tier 3) |
whisper_hollow |
"deep quiet" / min7th | D3 (50) | D min 7 — F3 A3 C4 E4 | 50 | 16.0s | Late-game Hub (rooms_completed.size() >= 2, #64 T123) |
silence_void |
emptiness / absence | (no audio) | — | 60 | 4.0s | GAME_OVER_FAILURE + finale phase 1 |
- Major keys (
title_introD /hub_warmF /archive_dawnG) read as "the world is intact / hopeful / bright". The Hub starts on F major (brightest), andarchive_dawnis the only G major (the "rising resolution" of a fifth above hub_warm's F — the world moving up a step on victory). - Minor keys (
archive_explorationA /archive_bossA /archive_boss_dualA /archive_stormE /whisper_hollowD) read as "the archive is decayed / melancholy / dangerous". Three of the four share A minor so the boss-fight crossfade is harmonic;archive_stormbreaks to E minor for harmonic contrast (chaos, not just more intensity) andwhisper_hollowbreaks to D minor for distance (deep quiet, distinct from the urgent exploration theme). - Dissonance ladder: clean triad → +tritone → +tritone +aug5 → +aug4 +raised7th. Each tier-1 / 2 / 3 boss preset adds a new dissonant interval, so the boss-fight music feels the escalation as harmonic pressure, not just loudness.
- Silence as a theme:
silence_voidis the only "all amplitudes zero" preset — it is not a BGM, it is the deliberate absence of BGM. It bridges the failure state (4s of empty air matches the cold-gray visual wash) and the finale phase 1 (4s of "the world empties out" beforearchive_dawnresolves it). - Cutscene ambient bed (T122): not a BGM preset, but a one-shot 8-second D2 + G2 dual-sine drone on the Ambience bus, generated on demand by
AudioManagerEnhanced.play_intro_ambience()and triggered byintro_cutscene.gd._play_sequence(). It lives under the upcomingtitle_introBGM so the cutscene never plays over a hard silence.
Settings → Audio menu exposes three independent volume sliders, each bound to its own Godot AudioServer bus:
| Slider | Bus | Contents |
|---|---|---|
| Master | Master |
Everything (BGM + SFX + ambience summed) |
| Music | Music |
Procedural BGM (title_intro / hub_warm / archive_exploration / archive_boss / archive_boss_dual / archive_dawn / archive_storm / whisper_hollow + the silent silence_void slot) |
| SFX | SFX |
Pulse / Bind / Cut / footstep / glass-break / damage / repair |
| Ambience | Ambience |
Water / wind / room atmosphere hum |
Settings persist to user://settings.cfg across runs.
This project follows an iterative development process. See ITERATION_GUIDE.md for the full workflow. New contributors should also read CONTRIBUTING.md — it covers repo layout, the 3-method Godot binary reassembly + --import recipe, the 7-suite smoke test list, commit format, iteration cadence, asset-registration rules, doc-sync checklist, troubleshooting, and where to record decisions.
The Godot 4.6.3 headless binary is shipped as a multi-part zip in godot/. On first
clone (or after a fresh sandbox) it must be reassembled and unzipped before any
--headless command will run. Method A uses unzip; Method B uses Python
zipfile as a fallback when unzip errors with bad zipfile offset (common in
containerized sandboxes where the multi-volume zip offset parser disagrees with
the data).
# Reassemble the 4 split volumes + main archive
cd godot
cat Godot_v4.6.3-stable_linux.z01 \
Godot_v4.6.3-stable_linux.z02 \
Godot_v4.6.3-stable_linux.z03 \
Godot_v4.6.3-stable_linux.z04 \
Godot_v4.6.3-stable_linux.zip > /tmp/godot_full.zip
# Method A — standard unzip (works on most distros)
unzip -o /tmp/godot_full.zip && chmod +x Godot_v4.6.3-stable_linux.x86_64
# Method B-1 — `unzip -FF` strong-fallback (recommended for sandboxes / Python 3.14+)
# Use this if `unzip` prints "bad zipfile offset" or "extra bytes at beginning".
# `unzip -FF` re-compensates bad offsets and works even when standard `unzip` fails.
# Expected output: warnings about "bad zipfile offset" + "attempting to re-compensate",
# then `inflating: Godot_v4.6.3-stable_linux.x86_64` succeeds.
unzip -FF -o /tmp/godot_full.zip 2>&1 | tail -20 && chmod +x Godot_v4.6.3-stable_linux.x86_64
# Method B-2 — Python zipfile fallback (works for Python ≤ 3.13 ONLY)
# Use this if both standard `unzip` and `unzip -FF` are unavailable.
# ⚠️ Python 3.14+ standard `zipfile` library fails to extract multi-volume zip —
# `_extract_member` raises `BadZipFile: Bad magic number for file header` (F003 #82).
# On Python 3.14+ systems (e.g. fresh Ubuntu 25.04, CI 2026+ images) use B-1 instead.
python3 -c "import sys; print(sys.version_info[:2]); " \
&& python3 -c "import zipfile; zipfile.ZipFile('/tmp/godot_full.zip').extractall('.')" \
&& chmod +x Godot_v4.6.3-stable_linux.x86_64
# Verify
./Godot_v4.6.3-stable_linux.x86_64 --version # 4.6.3.stable.official.7d41c59c4First-run import cache is mandatory — the
.godot/imported/*.ctexcache is git-ignored, so the very first Godot run must regenerate it, otherwise every PNG fails to load and cascades into 8+ spuriousSCRIPT ERRORlines:./Godot_v4.6.3-stable_linux.x86_64 --headless --import --path /workspace
For deeper troubleshooting see godot/README.md.
We iterate hourly against a publicly visible backlog. The current backlog lives in ROADMAP.md with task IDs T001–TNNN and timestamps marking completion.
| Milestone | Status | Key Tasks | Notes |
|---|---|---|---|
| M1 — Core loop vertical slice | ✅ Shipped (#1–#14) | T001–T013 | 60s "enter room → Pulse → repair → collect → exit" playable |
| M2 — Second enemy + room variety | ✅ Shipped (#8–#15) | T017–T025, T017 left-facing fix | NoteWisp + Archive 02/03 variants |
| M3 — Save & persistence | ✅ Shipped (#12, #33) | T022, T026, T070 | Save Lantern + 3-slot disk save + Continue |
| M4 — Player progression | ✅ Shipped (#13–#15) | T029–T034 | Resonance shards, InkWarden elite, Bind ability, ability gates |
| M5 — Hub + NPCs + Settings | ✅ Shipped (#16, #24, #34) | T035, T036, T037, T048, T072 | Safe-zone Hub, dialogue system, 4-tab settings |
| M6 — Player stats + achievements | ✅ Shipped (#19, #28) | T041, T042, T059–T061 | 8 Steam-style achievements + notification card + 8-icon grid |
| M7 — Procedural BGM | ✅ Shipped (#29, #31, #39, #44, #59) | T062, T063, T066, T071, T080, T087, T107 | 7 synthesized themes (incl. archive_boss_dual for archive_04 + archive_dawn for victory / hub return + archive_storm tier-3 InkWarden phase-2 escalation) + scene routing + boss override + tier upgrade |
| M8 — Death animation + Steam description | ✅ Shipped (#36, #39) | T074, T075, T079 | Laying-down death, full English Steam copy, respawn-to-Hub by default + settings toggle |
| M9 — Storefront readiness | ✅ Shipped (#32, #34) | T069, T072, T073 | 3 Steam capsules (A047–A049), IntroCutscene, save deletion |
| M10 — Marketing live on Steam | ✅ Shipped (#43) | T083 | 6 marketing screenshots composited from existing assets (real capture needs desktop env — see tools/README.md) |
| M11 — Late-game content | ✅ Shipped (#38, #41) | T067, T068 | 4th archive room + second InkWarden (Resonance Shrine) + Hub shop NPC (5 permanent perks) |
| M12 — Final polish | ✅ Shipped (#42) | T076 | 2nd-stage archive lighting (bell repair → 0.8s warm reflow) — all 4 archives opt-in via atmosphere: true |
- #91 — T172 Polish ScreenShake 4 verb 命中色查表常量 + F008 Doc CONTRIBUTING.md 补 '新建 class_name 脚本必须 --import 一次生成 valid .uid' 经验:
src/autoload/screen_shake.gd(+12 行 4 verb 命中色常量VERB_HIT_PULSE_COLORCoral Pulse#E86D5A+VERB_HIT_BIND_COLORMuted Violet#65506A+VERB_HIT_CUT_COLORAmber Voice#F2B66E+VERB_HIT_ECHO_COLORGlass Cyan#69C7CE,严格对应 STYLE_GUIDE 限制色板,1:1 集中定义让"调色板刷新只动 4 行")+src/scripts/player.gd5 处flash_color(Color(0.91, 0.427, 0.353, 1.0), 0.10, 0.18)等字面值 →ScreenShake.flash_color(ScreenShake.VERB_HIT_PULSE_COLOR, 0.10, 0.18)等常量引用(行为完全不变:duration/peak_alpha 节奏字面值保留——4 verb 节奏各异 Pulse 0.10/0.18 / Bind 0.10/0.18 / Cut 0.09/0.18 / Echo 反射 0.08/0.20 / Echo 非反射 0.06/0.12,强行打包成常量会损失 T170b 6:3 "反 > 挡"比例语义)+tools/test_t170_smoke.gd5 处字面值断言(T170a.5/6 Bind + T170b.3/4/5 Echo 3 处 + T170c.3 Pulse + X.1-X.4 跨任务 4 处)→ 常量引用断言 +tools/test_t171_t170d_smoke.gdT170d.3 1 处 +tools/test_t098_t100_smoke.gd2 处放宽 OR 条件以兼容"常量引用 OR 原字面值 OR ScreenShake 文件中有 RGB" +CONTRIBUTING.md§2.2.1 新增 32 行经验段("每新建一个带 class_name 的脚本后必须再跑一次 --import" + 症状 + 修复 + 3 触发场景按频率排序 + 3 预防措施)+ §8 故障排查表新增<script>.gd.uid 0 字节(**L001 #90**)行。L002 验证 (5min):用bash tools/check_smoke_consistency.shrule 7 自动验证README.md+README.zh-CN.md最新 #N = 90 ==ITERATION_COUNT.txt90 → PASS(F002 #85 引入的 hook 持续生效),本轮 L002 无需手动补内容。40/40 100% PASS(与 #90 比 0 增减,0 回归;T172 触发 3 测试套件共 8 处断言更新,回归后全 PASS)+check_smoke_consistency.sh7/7 规则 PASS + 0 SCRIPT ERROR + 0 parse error。4 verb 命中色集中化最终态 —— 4 个 const 在ScreenShake顶部定义(与 STYLE_GUIDE 限制色板 1:1 对应),5 处player.gd调用点全部走常量引用,调用点从 0 处集中提升到 4 处集中 + 5 处使用,色 → 常量 + 节奏 → 字面值 责任分明。F008 经验沉淀最终态 —— CONTRIBUTING.md §2.2.1 把"#86 T167/T168 → #90 L001 修复"链反向化为 3 段标准化操作(预防 / 症状 / 修复)+ 3 触发场景(1. 新 class_name / 2. 重命名 class_name / 3. 跨轮 class_name refactor)+ 3 预防措施(提交前ls -la *.gd.uid | awk '$5 == 0 {print}'扫 0 字节 +check_smoke_consistency.shrule ⑥ + 新建后先跑--import再git add)。下一轮(#92)候选:F004 [信息] Audio 5 verb 闭环(30min,#90 审查建议 5 轮间隔 #91-#95 集中做)/ T173 [候选] Polish 5 verb windup VFX 退出淡出 tween(15min)/ F009 [信息] Doc STYLE_GUIDE.md 加 4 verb 命中色查表常量定义段(5min,T172 的 4 元组本身值得在 STYLE_GUIDE 独立段固化)/ L003 [信息] Doc README "Screenshots" 节补 VFX 链示例(10min)。 - #90 — 审查 #90(this iteration):完整代码质量 / 玩法 / 素材 / 文档审计;0 SCRIPT ERROR + 0 runtime ERROR + 55 .gd 文件 + 52 class_name 唯一(+4 windup classes 来自 #86 T167 Bind + #86 T168 Echo + #89 T171 Wave + 1 stable)/ 69 signal 完整 + 29 .tscn + 8 .json + 7 autoload + 0 TODO/FIXME + 114 PNG 100% 合法 + 102 .uid(0 空文件 — 本轮修复 #86 留下的 2 个空 .uid
bind_windup_vfx.gd.uid/echo_windup_vfx.gd.uid由rm+--import重新生成uid://bh4oc6o1wkpl6/uid://clcrt5damt18k)+ ASSET_REGISTRY 72 条 + 40 smoke test 套件 40/40 100% PASS(L001 修复后重测 0 回归)+check_smoke_consistency.sh7/7 规则 PASS(rule 7 README 同步 hook 由 #85 F002 引入已工作);严重 0 / 一般 0 / 轻微 1(L001 已修) / 信息 1(F004 audio 闭环建议下个 5 轮间隔 #91-#95 集中做)。5 verb windup 闭环最终态 —— Pulseclass_name PulseWindupVFXGlass Cyan#69C7CE1.0×→0.92× 收缩 ring(T166 #85)+ Bindclass_name BindWindupVFXMuted Violet#65506A1.0×→0.85× 螺旋内收(T167 #86)+ Echoclass_name EchoWindupVFXGlass Cyan#69C7CE+ Pale#B7E7DD+ Amber#F2B66E0.5×→1.0× 球外撑(T168 #86)+ Cutclass_name CutWindupVFXAmber#F2B66E0.0×→1.0× streak 横扫(T169 #87)+ Waveclass_name WaveWindupVFXPale#B7E7DD3 环 ripple outward(T171 #89)5 个class_name extends Node2D类,trigger(origin, half_radius, duration)签名一致,5 verb motif 全部独立(Pulse 收缩 / Bind 螺旋 / Echo 撑开 / Cut 横扫 / Wave 涟漪),5 verb 色严格在 STYLE_GUIDE 限制色板内,0 板外色。4 verb 命中反馈闭环最终态 —— Pulse Coral#E86D5A(0.91, 0.427, 0.353) flash 0.10s/0.18 + Bind Violet#65506A(0.398, 0.314, 0.416) flash 0.10s/0.18 + Cut Amber#F2B66E(0.949, 0.714, 0.431) flash 0.09s/0.18 + Echo Cyan#69C7CE(0.412, 0.78, 0.808) flash 反射 0.08s/0.20 / 非反射 0.06s/0.12 4 verb 命中色 4 元组 + 4 verb LIGHT 1.0/0.08s 屏抖 5 元组完全统一(来自 T170a/b/c/d #88-#89,4 verb 命中节奏"1/16 beat groove")。完整审查报告见 REVIEW_LOG.md。 - #89 — T171 5 verb windup 家族闭环(Wave 第 5 色 Pale Resonance halo VFX)+ T170d Cut 命中 LIGHT 屏抖 + I006 18 项锚点 smoke 测试:
src/scripts/wave_windup_vfx.gd(新文件 110 行class_name WaveWindupVFX extends Node2D)+src/scripts/resonance_wave_ability.gdstart_wave()+25 行 +src/scripts/player.gd_on_cut_hit+18 行 +tools/test_t171_t170d_smoke.gd(新文件 152 行)18 项断言 PASS。5 verb windup 调色五元组正式闭环 + 4 verb 命中屏抖分工完成 —— T171 新建wave_windup_vfx.gdPale Resonance#B7E7DD(比 Pulse Cyan 更冷更"光"——Wave "AOE 中心爆发而非定向打击"语义匹配,色温"穿透力最强")+ 3 环 concentric halo r_ratio [0.40, 0.65, 0.92]("声波辐射"主题,4 verb 1.0→0.92 收缩 ring / 1.0→0.85 螺旋 / 0.5→1.0 球 / 0.0→1.0 streak 之外的第 5 motif)+ per-ring alpha_mult [0.55, 0.78, 1.0](外环最亮 = "声波前导")+ phase_offset [0.0, 0.18, 0.36] 渐入 staggered "ripple outward" sound-wave motif + peak_alpha 0.65(比 Pulse 0.70 略低——"比空气还轻的 verb",transient 非 solid)+ ring_width 1.2(比 4 verb 1.5px 细 0.3px——3 环同时存在需要视觉层次)+ z_index 10 + queue_free safety net + STYLE_GUIDE 引用;resonance_wave_ability.gd集成preload("res://src/scripts/wave_windup_vfx.gd").new()+trigger(_pending_origin, wave_radius * 0.5, windup_time)+scene.add_child(windup_vfx)(preload 而非 class_name 引用——与 4 verb 家族一致,preload path-based 引用让 headless smoke test load-order 决定性;0.5× radius——4 verb 家族一致 "precursor 而非 fire";挂到 current_scene 而非 player 子节点——5 verb 家族一致:ring 位置稳定在世界坐标,player 移动时 ring 不跟着走,让 0.10s 期间 halo 是"我留在原地的 0.5s 警告"而非"我跟随玩家的拖尾")。T170d_on_cut_hit(_target)在flash_color(Color(0.949, 0.714, 0.431, 1.0), 0.09, 0.18)之后追加shake_preset(ScreenShake.Preset.LIGHT)(4 verb 命中屏抖 1.0/0.08s LIGHT 数值统一——CUT 1.5/0.06s fire shake 衰减完 = LIGHT 1.0/0.08s hit shake 才开始,间隔 0.03~0.10s 不重叠 = "挥→中"两步触觉;max_targets=6 多目标风险由 ScreenShake tween 内部 dedupe 兜底,视觉 = 1 次 0.08s LIGHT 与 Pulse 多目标场景行为完全一致;LIGHT 而非 HEAVY 因为 Cut 单体命中反馈已经很强——HEAVY 喧宾夺主;Amber flash 无回归)。I006 新冒烟测试tools/test_t171_t170d_smoke.gd(152 行) 18 项断言 PASS —— T171 段 10 项(class_name WaveWindupVFX声明 /extends Node2D与 4 verb 一致 /trigger(origin, half_radius, duration)签名匹配 4 verb 家族 /Color("#B7E7DD")Pale Resonance 第 5 色 /@export var ring_count: int = 3默认 3 环 /z_index = 10above world below HUD /queue_free()safety net /T171 (#89)docblock 标记 / docblock 含 "ripple outward" sound-wave motif 关键词 /STYLE_GUIDE引用作为色域来源权威)+ T171 集成段 4 项(resonance_wave_ability.gd:start_wave内preload("res://src/scripts/wave_windup_vfx.gd").new()存在 / 完整trigger(_pending_origin, wave_radius * 0.5, windup_time)调用 /scene.add_child(windup_vfx)挂到 current_scene /T171 (#89)docblock 标记在 resonance_wave_ability.gd)+ T170d 段 4 项(_on_cut_hit内ScreenShake.shake_preset(ScreenShake.Preset.LIGHT)调用 /T170d (#89)docblock 标记 / 完整flash_color(Color(0.949, 0.714, 0.431, 1.0), 0.09, 0.18)Amber flash 无回归 /if ScreenShake and ScreenShake.has_method("shake_preset"):守卫保留)。18/18 PASS + 0 SCRIPT ERROR + 0 parse error + 21/21 #88 T170 套件无回归 + check_smoke_consistency.sh 7/7 规则 PASS。风格 0 漂移(T171#B7E7DDPale Resonance 严格在 STYLE_GUIDE 限制色板内,5 verb windup 五元组 Pulse Cyan / Bind Violet / Cut Amber / Echo Cyan / Wave Pale 全部在限制色板内;T170d 复用 T098 Amber#F2B66E+ LIGHT preset,4 verb 命中反馈色 4 元组 + LIGHT 0.08s 屏抖分工 5 元组完整)。 - #88 — T170 4 verb 命中反馈 VFX polish(Bind 命中反馈 / Echo 命中非反弹反馈 / Pulse 命中屏抖):
player.gd改动 1 文件新增50 行。T170a Bind 命中反馈 ——0.15s 不会重叠(fire shake 衰减完 = hit shake 才开始),形成"推→中"两步触觉。新冒烟测试_ready中bind_ability.bind_hit.connect(_on_bind_hit)(has_signal守卫保 pre-bind-hit 存档兼容),新增_on_bind_hit(target)handler 调ScreenShake.flash_color(Muted Violet #65506A, 0.10s, 0.18)+ScreenShake.shake_preset(LIGHT 1.0/0.08s)补"钉住"触感,与 Pulse Coral / Cut Amber / Echo Cyan 三 verb 命中反馈形成 4 verb 色域分工(色域 4 元组:Pulse Coral / Bind Violet / Cut Amber / Echo Cyan,"看到闪就知道是哪个 verb"快速识别);0.10s / 0.18 数值与 Pulse 命中(T098)对称让 4 verb 反馈节奏统一;LIGHT 而非 HEAVY 因为 Bind 语义"温柔牵制"而非"暴力推开"。T170b Echo 命中非反弹反馈 ——_on_echo_hit之前is_reflect=false早退无任何屏幕反馈(echo_ability.gd:278 emit(enemy, false)= 敌人物理接触护盾被短致盲 0 伤),现在补ScreenShake.flash_color(Glass Cyan #69C7CE, 0.06s, 0.12)—— 0.06s / 0.12 比反弹路径(T097 0.08s / 0.20)更短更暗,反弹 = "成功回击"高反馈 / 非反弹 = "温和挡下"低反馈,6:3 比例让"反 > 挡"视觉权重正确。T170c Pulse 命中屏抖 ——_on_pulse_hit已有 Coral flash(T098),现在补ScreenShake.shake_preset(LIGHT)(1.0/0.08s) 作为"打到了"补充触觉;与_on_pulse_fired的 PULSE 2.0/0.10s shake 间隔 0.05tools/test_t170_smoke.gd(210 行) 21 项断言 PASS —— T170a 8 项(connect/handler/4 个签名锚点 + null 守卫 + Muted Violet 色 + LIGHT shake + docblock 标记)/ T170b 6 项(is_reflect 分支保留 + 非反弹 flash_color 调用 + Glass Cyan 色 + 0.06s/0.12 数值 + 反弹路径无回归 + docblock)/ T170c 3 项(LIGHT shake 调用 + docblock + Coral flash 无回归)/ 4 verb 色域分工交叉检查 4 项(Pulse Coral / Bind Violet / Cut Amber / Echo Cyan 4 色均保留无回滚)。21/21 PASS + 0 SCRIPT ERROR + 0 parse error + check_smoke_consistency.sh 7/7 规则 PASS。风格 0 漂移(3 反馈色严格在 STYLE_GUIDE 限制色板 / 4 verb 调色 4 元组分工不变)。 - #87 — I005 补 #86 缺测试 (33 项断言 smoke 套件) + T169 CutAbility 0.06s 黄色 line streak pre-cut VFX + F007 4 verb ability 内部
_consume_verb_cost/_setup_windup_state共享模式 helper:新测试文件tools/test_t167_t168_f006_smoke.gd(230 行) 33 项断言 PASS 覆盖 #86 三个任务全部代码改动点(T167 Bind windup VFX 11 项 — bind_windup_vfx.gd 存在 + extends Node2D + Muted Violet#65506A+ arc_count=3 + _end_scale=0.85 内拉 + lifecycle + bind_ability spawn/free/_exit_tree 顺序 + bind_radius0.5 透传;T168 Echo windup VFX 11 项 — echo_windup_vfx.gd 4 参数 trigger 签名 + 3 色 Glass Cyan + Pale Resonance + Amber Voice + _end_scale=1.0 撑开(vs Pulse 0.92/Bind 0.85 内缩反向)+ _start_scale=0.5 + lifecycle + echo_ability spawn/free/_exit_tree 顺序 + echo_radius0.5+echo_radius 4 参数 trigger 调用;F006 refactor 8 项 — _try_verb() 2 参签名 + 4 个 _start_X_at() wrapper + 4 verb handler 体内 _try_verb 委托(600 char 窗口允许 docblock)+ _try_verb body 含 3 关键步骤 + _handle_wave 保持原 4 状态路由 + F005 helper 保留 + D001 is_action_globally_blocked 公开函数保留;D001 regression + 4 verb 一致性交叉检查 3 项),完成 #86 末尾"保留 #87 视情况添加 test_t167_t168_f006_smoke.gd"承诺。T169 新文件cut_windup_vfx.gd(61 行)class_name CutWindupVFX extends Node2D4 verb windup 第 4 视觉 motif — Pulse ring(0.5×→0.92× 内缩)/ Bind spiral(0.5×→0.85× 旋转内收)/ Echo sphere(0.5×→1.0× 撑开)/ Cut streak(0.0×→1.0× 沿 cut 方向延伸,让玩家在 0.06s 前摇中即可辨别哪个 verb 在蓄力,5-verb 链 T142 防误触 UX 提示完整),Amber Voice#F2B66E严格对齐 STYLE_GUIDE 限制色板(4 verb 调色四元组 — Pulse Glass Cyan / Bind Muted Violet / Echo Glass Cyan+Amber Voice / Cut Amber Voice),trigger(origin, half_radius, direction, duration)4 参数签名(与 T168 Echo 对齐),2px stroke 双线 draw_line + 1.5px 垂直偏移防 dark tileset 1px 直线消失,alpha 0→0.7 ramp-in 前 40%(Cut 0.06s 是 4 verb 最短前摇比 Pulse 0.4 更快),scale 0.0→1.0 沿 cut 方向延伸(与 cut_vfx.gd arc swing motion 同向 windup-to-fire 过渡连续);cut_ability.gd新增var _windup_vfx: Node2D = null句柄 +start_cut()集成 +_execute_cut()顺序敏感 free(cut_vfx.gd 同帧 spawn arc 替换 streak)+func _exit_tree()钩子(3 道 free 保险同 T166/T167/T168 模式)。F007 refactor 4 verb 内部共享 2 helper 模式 —— 4 verb 各加 byte-identical_consume_verb_cost(cost: int) -> bool+_setup_windup_state(origin, direction) -> void(GDScript 限制 4 verb 各自重写一份 helper,但命名 + 签名 + docblock 一致,未来 base class_verb_ability_base.gd抽取铺路),4 verbstart_X()顶部 4-5 行(if not can_X: return false; if not GameState.consume_resonance(X_cost): return false; _is_winding_up = true; _windup_timer = windup_time; _pending_origin = origin; _pending_direction = direction)缩为 2 行调用(if not _consume_verb_cost(X_cost): return false; _setup_windup_state(origin, direction));echo_ability 特殊 —start_echo(origin)不接收 direction 参数(盾中心 pop 语义)但调用_setup_windup_state(origin, Vector2.ZERO)保持 4 verb 签名一致,新增var _pending_direction: Vector2 = Vector2.ZERO字段(不读只用,与 pulse/bind/cut 字段定义 byte-identical);3 层 helper 抽象栈 —— F005 player 层_pre_verb_block_check/ F006 player 层_try_verb/ F007 ability 层_consume_verb_cost+_setup_windup_state,未来加 verb 边际成本降到 "1 行 wrapper + 1 个新 ability 文件 + copy-paste 2 helper"。38/38 smoke tests PASS + 0 SCRIPT ERROR + check_smoke_consistency.sh 7/7 规则 PASS - #86 — T167 BindAbility windup 0.5× Muted Violet 螺旋 VFX + T168 EchoAbility 0.5×→1.0× Glass Cyan 球 VFX + F006 player.gd 4 verb handler 提取
_try_verb()helper:新文件bind_windup_vfx.gd(87 行)class_name BindWindupVFX extends Node2D自管理 lifecycle —trigger(origin, half_radius, duration)设global_position+_radius+_max_lifetime启动,_draw()渲染 3 段 spiral 弧旋转内收(draw_arc12 段 / 0.7 间隙 /base_angle = _lifetime * 4.0旋转 4 rad/s),scale 1.0→0.85 收缩(比 Pulse 0.92 更激进内拉——Bind 语义"往中心拉"呼应 A033 icon spiral motif),Muted Violet#65506A严格对齐 STYLE_GUIDE 限制色板,alpha 0→0.75 ramp-in 首 40% 防 frame-0 闪烁;bind_ability.gd新增var _windup_vfx: Node2D = null实例句柄,start_bind()在 consume_resonance 成功后 spawn 挂到get_tree().current_scene(非 player 子节点让 player 移动时 ring 位置稳定在世界坐标),_execute_bind()在bind_fired.emit之前 free windup_vfx(顺序敏感:bind VFX 同帧 spawn 替换 windup),新增func _exit_tree()钩子(关键 cleanup:player 在 windup 中被 scene change 销毁 bind_ability 退树时连带 free windup_vfx 防 leak),3 道 free 保险同 T166 Pulse 模式。新文件echo_windup_vfx.gd(90 行)class_name EchoWindupVFX extends Node2D与 Pulse/Bind 反向 motion language——Pulse/Bind 是 0.5×→0.85-0.92× 内缩(能量聚拢/向内拉),Echo 是 0.5×→1.0× 外撑(盾"砰"地一下弹出接住来袭),_draw()3 层 painter's order:Layer 1 玻璃填充draw_circle半径lerp(half, full, t)alpha 0→0.18 / Layer 2 高光 rimdraw_arc1.5px alpha 0→0.55 / Layer 3 中央暖点draw_circle(Vector2.ZERO, 2px)alpha 0→0.45,三色皆来自 EchoVFX palette 维持 verb 调色一致(fill#69C7CEGlass Cyan / rim#B7E7DDPale Resonance / core#F2B66EAmber Voice),alpha ramp-int / 0.5(比 Pulse 的 0.4 更快——Echo windup 仅 0.08s 短窗口,前 0.04s 必须 readable);echo_ability.gd同模式集成 windup_vfx + _exit_tree 钩子。F006player.gd在文件末尾追加 1 个_try_verb()helper + 4 个_start_X_at()wrapper(_start_pulse_at/_start_bind_at/_start_cut_at/_start_echo_at),4 verb handler 各自缩成 1 行委托_try_verb("pulse", _start_pulse_at)等;新_try_verb(action_name: String, start_fn: Callable) -> void—— 5 步中央管道:(1)_pre_verb_block_check()守卫复用 F005 helper → (2)Input.is_action_just_pressed(action_name)rising-edge → (3) 在 helper 内计算origin = global_position + Vector2(0, -8)+dir = Vector2.RIGHT if _facing_right else Vector2.LEFT(4 verb 共用"头部 8px / 面向方向"公式与原 handler 字节级一致)→ (4)start_fn.call(origin, dir)委托给 verb 内部start_*()(Echo wrapper 忽略dir因盾中心 pop 语义)→ (5) 失败时统一hud.show_pulse_blocked()提示(与原 handler 行为完全一致);4 wrapper 签名统一(origin: Vector2, dir: Vector2) -> bool,内部if ability: return ability.start_X(origin, dir) else: return false(else false路径让_try_verb()触发 blocked toast 兜底保持 #85 旧语义不变);Wave 排除——# F006 (#86) — Why not also include _handle_wave?docblock 详述 Wave 有 4 个 verb 状态路由(active/winding_up/charging/blocked,T143)需要 4 分支专属 HUD 提示不能套这个 1-toast 通用 helper,_handle_wave()保持原样;未来扩展价值——加新 guard 条件("dialogue open")只需 OR 进_pre_verb_block_check()一处,加第 6 verb(方向性)只需写 1 行_handle_X()+ 1 个_start_X_at()wrapper;本轮未新增冒烟测试(#85 审查通过后零回归历史 + 源码净增 ~245 行未达 500 阈值保留 #87 视情况添加test_t167_t168_f006_smoke.gd) - #85 — T165 BGM tier-up ScreenShake flash_color (0.15s Glass Cyan 256 层) + T166 PulseAbility windup 0.08s→0.10s + 0.5× Glass Cyan pre-pulse ring VFX + F005 player.gd 4 verb handler 提取
_pre_verb_block_check()helper:audio_manager_enhanced.gd.request_boss_music()在if new_tier > current_tier:分支末尾追加ScreenShake.flash_color("#69C7CE", 0.15, 0.18, flash_layer=256)(Glass Cyan 严格对齐 STYLE_GUIDE 限制色板 / duration 0.15s 与 300ms 音乐 crossfade 中段 tempo 对齐 / peak_alpha 0.18 subtle vignette /flash_layer=256走 T163 #84 新参数高于 hit-flash 128 避免互消),调用前用Engine.has_singleton("ScreenShake") or _has_screen_shake_autoload()双重防御(headless 测试下 audio manager 可能在 ScreenShake 之前_ready()完单一Engine.has_singletonfalse-positive 漏报),新增私有_has_screen_shake_autoload()helper(tree.root.has_node("ScreenShake")探测 +Engine.get_main_loopnull 守卫);新文件pulse_windup_vfx.gd(90 行)class_name PulseWindupVFX extends Node2D自管理 lifecycle —trigger(origin, half_radius, duration)设global_position+_radius+_max_lifetime并启动,_draw()渲染 0.5× radius Glass Cyan 圆环(draw_arc(Vector2.ZERO, ring_r, 0, TAU, 32, col, ring_width)32 段),scale 1.0→0.92 线性收缩("能量聚拢"暗示与 fire VFX 反向扩张形成"→|→ 炸开"语言),alpha 0→0.7 ramp-in 首 40% 防 frame-0 闪烁;pulse_ability.gdwindup_time: float = 0.08→0.10(与 bind_ability 0.1s 一致 4 verb windup 节奏统一),新增var _windup_vfx: Node2D = null实例句柄,start_pulse()在 consume_resonance 成功后 spawn 挂到get_tree().current_scene(非 player 子节点让 player 移动时 ring 位置稳定在世界坐标),_execute_pulse()在pulse_fired.emit之前 free windup_vfx(顺序敏感:fire VFX 在 player._on_pulse_fired 同帧 spawn 两 VFX 不重叠 1 帧),新增func _exit_tree()钩子(关键 cleanup:player 在 windup 中被 scene change 销毁 pulse_ability 退树时连带 free windup_vfx 防 leak),3 道 free 保险(_execute_pulse显式 /_exit_treescene-change /_process._max_lifetime超时自清);player.gd新增私有 helperfunc _pre_verb_block_check() -> bool: return is_action_globally_blocked()(保留is_action_globally_blocked()公共函数不动以兼容_handle_jump/_on_echo_multi_reflect),4 verb handler 头注释同步追加# F005 (#85) — single _pre_verb_block_check() guard shared by the 4 directional verbs并把if is_action_globally_blocked(): return替换为if _pre_verb_block_check(): return(未来加新 guard 条件如 "dialogue open" / "shop UI focused" 只需 OR 进 helper 一处 4 verb handler 同步生效);test_t165_t166_f005_smoke.gd23 项新断言 PASS + 全 37/37 冒烟测试套件 PASS - #84 — T101 ResonanceWave 命中粒子层叠 8→12 (4 new visual layers) + T163 ScreenShake.flash_color / flash_grayscale 接受可选 [flash_layer] 参数 + F004 修复 3 套件 pre-existing stale-state 冒烟测试:
resonance_wave_vfx.gd新增 14 常量 (DEEP_SHADOW_RADIUS_RATIO=0.42/INNER_HALO_RADIUS_RATIO=0.55/OUTER_WISP_RADIUS_RATIO=1.18/OUTER_WISP_COUNT=12/SPARKLE_RADIUS_RATIO=0.70/SPARKLE_COUNT=6等) + 3 色常量 (#65506AMuted Violet /#B7E7DDPale Resonance /#F2B66EAmber Voice 严格对齐 STYLE_GUIDE 限制色板) +_draw()改写为 9 段 painter's order (deep_shadow→inner_halo→ring_fill→ring_stroke→8 prism_rays→12 outer_wisps→6 sparkle_stars 闪烁 alpha→center_core→bounce_flash), 4 新 layer 从 1 layer 静态环变 8 layer 多深度冲击波;screen_shake.gdflash_color(..., flash_layer: int = 128)+flash_grayscale(..., flash_layer: int = 128)接受 canvas layer 索引 (默认 128 保持向后兼容, 上层 256 高于 HUD, 下层 64 低于 HUD),_active_grayscale+_active_color_flash从单CanvasLayer引用重构为Dictionary按 layer_idx 分桶 (同 layer 后调用取消前调用 / 跨 layer 并行),stop()迭代dict.keys()清掉 所有 layer 上的活动 flash;F004 修复 (1)test_t150_t147_t149_smoke.gd_handle_jump字符串窗口 1800 → 2500 char (T145 17 行 docblock + T147 4 行 + D001 注释让相关代码落在 char 1827-1900) + 新增 D001 sync 断言验证is_action_globally_blocked()是PlayerActionGate.is_blocked()的 thin delegate, (2)test_t158_t156_f002_smoke.gdF002.7 / F002.8 硬编码#81→ 动态ITERATION_COUNT.txt - 1(含 file-not-found fallback), (3) 复用 (1) 顺带同步 T147 守卫与 #76 重命名;test_t101_t163_f004_smoke.gd18 项新断言 PASS + 全 36/36 冒烟测试套件 PASS - #83 — T162 PlayerProfilePanel "最近 5 局详细" 列表 + T159 InkWarden phase 2 dissolve 0.25s 出 + 0.30s 入 tween:
pause_menu.tscn在ProfileTrend20之后新增ProfileRecentTitle("✦ 最近 5 局 ✦" Amber Voice 9pt center)+ProfileRecentListVBoxContainer;pause_menu.gd新增@onready var _profile_recent_list+ 3 常量(_PROFILE_RECENT_RUNS_MAX=5视觉密度上限 /_COLOR_RECENT_RUN_NORMALPale Resonance 沿用 trend 调色板 /_COLOR_RECENT_RUN_LATESTAmber Voice 高亮最近 1 局)+ 新方法_refresh_recent_runs_list()实现 5 个设计选择(最新 1 局 Amber Voice 高亮 / reversed order 最新在顶 / 每行 4 字段Run #N 房 X 净 Y 碎 Z 时 mm:ss/ 空 history 走"暂无 run 记录"占位 / dynamic child creation 防 stale data);与 T131 trend 5/10/20 行互补:trend 给"宏观"平均指标,recent 给"具体"每局明细("Run #5 净 0 死 3"立刻归因到"没找到 Pulse")。ink_warden.gd顶部新增 4 常量(PHASE_2_DISSOLVE_OUT_TIME=0.25/PHASE_2_DISSOLVE_IN_TIME=0.30/PHASE_2_DISSOLVE_OUT_SCALE=1.15/PHASE_2_DISSOLVE_IN_START_SCALE=0.85);_enter_phase_2()sprite swap 段改写为 5 段 tween(snap reset / dissolve out 0.25s scale 1.0→1.15 + alpha 1.0→0.0 / snap start / dissolve in 0.30s scale 0.85→1.0 + alpha 0.0→1.0 / existing red flash + settle 完整保留),共 1.03s 视听序列与 T156 5 段完美嵌套(shake 中段 = dissolve 中段)。原来 1f sprite 硬切被替换为 0.55s 渐变,让 phase 2 进入"我正在失控进化"而非"突然换皮"的体感。test_t162_t159_smoke.gd21 项断言 PASS - #82 — F003 4 文档同步 Python 3.14+ zipfile 兜底 + T160 PauseMenu "新成就!" Banner + T161 settings "还原所有推荐" 按钮 + D001 PlayerActionGate autoload 抽出:
godot/README.md+README.md+README.zh-CN.md+CONTRIBUTING.md4 文档同步重写为 方法 B-1unzip -FF强容错(沙箱 / Python 3.14+ 推荐)+ 方法 B-2 Pythonzipfile兜底(仅 Python ≤ 3.13 有效),实测复现 Python 3.14.4BadZipFile: Bad magic number for file header;pause_menu.tscn新增NewAchvBannerLabel(top center Amber Voice 10pt "✦ 新成就!✦")+pause_menu.gd3 常量(_BANNER_DURATION=0.8/_BANNER_FADE=0.4/_BANNER_RECENT_UNLOCK_WINDOW=5.0)+ 双轨触发(menu 可见直接 animate + 不可见记_last_seen_unlock_ts5s 窗口内 ESC 补播);settings_menu.tscn新增RestoreAllButton(Amber Voice 200×24)+settings_menu.gd_on_restore_all_pressed()3 阶段(按键InputMap.action_erase_events+_DEFAULT_BINDINGS/ 音量 4 slider 100% +AudioServer.set_bus_volume_db/ autosaveSaveSystem.set_autosave_enabled/interval/slot推默认)+ amber 0.8s "✓ 已还原" toast;src/autoload/player_action_gate.gd新建 22+80 行 Node autoload(4 public API:register_player/unregister_player/is_blocked/get_player)+is_blocked()复合 OR(_is_dying+wave_ability.is_globally_blocking)+project.godotautoload 段注册 +player.gd_ready/_exit_treeregister/unregister +is_action_globally_blocked()改 thin delegate +resonance_wave_ability.gdis_globally_blocking()头部加 D001 refactor 注释;test_d001_t160_t161_f003_smoke.gd21 项断言 PASS - #81 — T158 EchoAbility 4 重击命中后慢动作 0.4s 0.85x time-scale + T156 ArchiveStorm 主摄像机 1f skybox rotate 0.5° 0.2s ease 收回 + F002
check_smoke_consistency.shREADME 同步检查 hook 规则 ⑦:echo_ability.gd新增signal echo_multi_reflect(count: int)+const MULTI_REFLECT_THRESHOLD = 4+ 在_reflect_projectile末尾首次达到 4 emit 一次(同 cast 后续反弹不再 emit 防 spam);player.gd._ready用has_signal("echo_multi_reflect")守卫连_on_echo_multi_reflect→ 0.4s await × 0.85 time_scale,await 结束检查_is_dying避免覆盖 die() 的 1.0 重置;screen_shake.gd新增punch_rotation(degrees=0.5, duration=0.2)API(cam.rotation = deg_to_rad 立即设置 + tween 0.2s quad ease 收回,stop()兜底归零 + kill tween);ink_warden.gd._enter_phase_2()顶部(shake_preset 之前)调ScreenShake.punch_rotation(0.5, 0.2)形成 5 段视听序列:sky 反应 → BOSS_PHASE2 震 → sprite swap → RepairVFX ring → BGM tier-up;check_smoke_consistency.sh加 rule 7(README.md + README.zh-CN.md "Recent completed work" / "最近完成的工作" 段解析最新 #N 与 ITERATION_COUNT.txt 比对,滞后 ≥2 轮 FAIL 阻断 commit / 滞后 1 轮 WARN),根除 G001 第 4 次同类风险;test_t158_t156_f002_smoke.gd28 项断言 PASS - #80 — Review #80 (this iteration): full code quality / gameplay / asset / docs audit; 0 SCRIPT ERROR + 0 runtime ERROR + 47 class_name 唯一 + 78 signal 完整 + 114 PNG 合法 + 6 autoload 一致 + 72 ASSET_REGISTRY 记录 + 32 冒烟测试套件 32/32 PASS +
check_smoke_consistency.sh6/6 规则 PASS;严重 0 / 一般 1(G001 README Recent work 补 #76-#79 4 轮已修)/ 轻微 0 / 信息 1 - #79 — T152 0 数灰阶 + T153 槽位 jingle + T151 "最近" badge:
pause_menu.gd_COLOR_ZERO_STAT暖灰#808389+_set_zero_aware_stat()helper(6+4 行用 0 占位 "—");audio_manager_enhanced.gd_SAVE_SLOT_MIDI_NOTES = [72,76,79,84,88]pentatonic C5/E5/G5/C6/E6 +_generate_save_slot_jingle()0.25s 三角波 bell body +play_save_slot_jingle()公开 API(save/load 共享);save_load_menu.gd_find_most_recent_slot()+_format_recent_badge()BBCode[color=#B7E6DC]★ 最近[/color]Pale Resonance +_refresh_slots一次扫 5 槽定 most_recent_slot 下传_refresh_card/_refresh_list_row;4 状态字符完整化([·]/[—]/[✗]/[✓]);test_t152_t153_t151_smoke.gd19 项 PASS - #78 — T144 wave_focus 谐波 + T148 wave_combo chime tail + T154 灯反向闪:
audio_manager_enhanced.gd_wave_hit_streams: Dictionary4 level 缓存(0=1320Hz 基频 2.4x 谐波 / 1=+3.6x / 2=+5.0x / 3=+6.8x 凯旋钟塔)按GameState.get_perk_count("wave_focus")路由;play_wave_combo()0.6s E6+G#6 双音衰减 +_on_wave_combo()末接;save_lantern.gdflash_coral_pulse()0.15s Coral Pulse 反向闪 +silenced_web.gd on_cut_triggered迭代save_lanterngroup 触发;test_t144_t148_t154_smoke.gd26 项 PASS - #77 — T150 5 动词 profile + T147 jump 阻塞 UX + T149 Echo parallax:
player_stats.gdlast_used_verb字段 +record_ability_used入口首行刷新 +reset_stats清空 +pause_menu.tscnProfileLastVerb Label +pause_menu.gdmatch 5 动词 BBCode 调色板(pulse Coral / bind Violet / cut Amber / echo Cyan / wave Pale Resonance);hud.gdshow_jump_blocked()+player.gd _handle_jump双层守卫(is_action_just_pressed 触发);echo_vfx.gdPARALLAX 三常量(rotation 0.5 / radius 1.08 / alpha 0.55)+ PI/8 偏移 + 0.25 rad/s 副层旋转;test_t150_t147_t149_smoke.gd22 项 PASS - #76 — T143 wave 4 状态提示 + T145 is_action_globally_blocked 重构 + T146 wave_combo 屏震:
hud.gd4 verb 专属方法(charging / winding_up / active / blocked)+player.gd _handle_wave4 分支路由按生命周期排(active → winding_up → cooldown → cost-low);_is_wave_globally_blocking重命名为公开is_action_globally_blocked()+ OR_is_dying守卫 + 4 verb handler 调用点 +_handle_jump阻塞时清零 coyote+buffer timer 防死亡解除后"原地跳";resonance_wave_ability.gdwave_combosignal(@export wave_combo_threshold=3)+_deactivate_wave末尾 emit;player.gd _on_wave_comboshake(4.0, 0.4) + flash_color(Electric Violet #8C5BFF, 0.18s, 0.30);test_t143_t145_t146_smoke.gd25 项 PASS +test_t142重命名同步 - #75 — Review #75 (this iteration): full code quality / gameplay / asset / docs audit; 0 SCRIPT ERROR + 0 runtime ERROR + 47 class_name 唯一 + 77 signal 完整 + 114 PNG 合法 + 6 autoload 一致 + 72 ASSET_REGISTRY 记录 + 28 冒烟测试全 PASS + 1 一般 (G001 README Recent work 补 #61-#75 15 轮已修) + 1 信息 (候选池继续走 polish 路线)
- #75 — T130 hotfix (成就 13→14 同步) + T142 (5-verb 链防误触安全网) + T141 (wave 命中 audio cue):成就定义
total_count/unlocked_count同步 13→14 +achievements.jsonquintuple_voice入列;T142resonance_wave_ability.gd._try_fire()加 5 帧 verb-action-only 窗口(拒绝is_on_floor_only=true的is_dashing期间触发的"动画中波");T141resonance_wave_ability.gd命中路径AudioManagerEnhanced._sfx_bus_play("wave_hit", 0.4 + i*0.04, 1.05 + i*0.02)链入hit_count循环;新增tools/test_t130_achievement_sync_smoke.gd14 项断言 PASS - #74 — T103 第二半 (Wave 5-verb 对称) + T140 _handle_wave 失败提示走 verb 专属方法 + T139 成就计数 13→14:player.gd
_handle_wave()5 路径完整 pulse / cut / bind / echo / wave(wave→resonance_wave 桥接);T140 失败提示新增_wave_off_cooldown_prompt()/_wave_silenced_prompt()/_wave_already_active_prompt()3 个 verb-专属方法(更准确反馈而非泛化"无法释放");T139 A072quintuple_voice5-verb 一次完成成就落地;新增tools/test_t139_quintuple_voice_smoke.gd+tools/test_t140_wave_verb_prompts_smoke.gdPASS - #73 — T103 第一半 (ResonanceWave 群体波) + T137 SaveLoadMenu 快速加载 + T138 PauseMenu 上次自动存档时间:A070
resonance_wave_vfx(procedural vector pulse) + A071 wave 技能图标 (16x16 程序化像素) +src/scripts/resonance_wave_ability.gd(228 行 9 exports + 4 signals + 4 阶段生命周期 + 命中追踪) +src/scripts/resonance_wave_vfx.gd8 层视觉组 + HUDWaveRow第五冷却条(Electric Violet 主题色);T137save_load_menu.gdquick load card (slot 0 / 上次手动存档) 优先显示 + Ctrl+L 触发;T138 PauseMenu 新增"上次自动存档: N 分钟前"摘要;test_t103_resonance_wave_smoke.gd(31 项断言) +test_t137_t138_quick_load_and_autosave_smoke.gd(17 项) PASS - #72 — T136 SaveSystem 自动存档 60s + T135 PauseMenu 分享剪贴板:SaveSystem
_autosave_timer(60s 间隔 / 启用开关) +last_autosave_at时间戳 +pause_menu.cfg持久化autosave_enabled;T135 PauseMenu 新增"分享"按钮 → DisplayServer.clipboard_set (成就摘要 + 4 段格式化文本 + run 编号 + 死亡次数);test_t135_share_smoke.gd+test_t136_autosave_smoke.gdPASS - #71 — T134 settings 动态 SLOT_COUNT + T133 PauseMenu Quick Stats 摘要行:settings_menu.cfg 新增
save_slot_count(1-10) + SaveSystem 启动时 clamp + 5→10 槽 UI 自动扩展;T133 PauseMenu 顶部"本次 Run" + "历史最佳"两行摘要(run 编号 / 死亡次数 / 修理数 / 收集数 / 房间数);test_t133_quick_stats_smoke.gd+test_t134_dynamic_slot_count_smoke.gdPASS - #70 — Review #70 (D001-D003 严重问题修复): D001
_autosave_timer改 Timer 节点 (单 timer / pause_mode=PROCESS) + SceneTree 改_autosaveasync;D002get_run_id()改 Time.get_unix_time_from_system() + 文件名含 UTC 时间戳(避免碰撞);D003_health_danger改 danger_threshold + beat/tween 同步 + 0.6s 渐显;ASSET_REGISTRY A068-A069 装饰物件登记;3 严重 / 0 一般 / 1 轻微 (L001) / 1 信息 - #69 — T131 Run 趋势 + T132 备份/恢复 API:PauseMenu 趋势卡(4 项 stats: run 数 / 平均修理 / 死亡数 / 收集率) +
SaveSystem.get_run_trend()API;T132 备份/恢复(backup_save()→user://backups/save_N.bak/restore_from_backup()+ 自动备份触发器 [manual save / settings delete]);test_t131_run_trend_smoke.gd+test_t132_backup_restore_smoke.gdPASS - #68 — T129 存档健康度 + T130 历史最佳成就:SaveSystem
get_save_health()(per-slot 校验 / CRC32 + last_modified + run_id 摘要) + PauseMenu 存档 tab 健康度标签(健康 / 警告 / 损坏);T130 历史最佳成就触发条件 (本次 run 修理数 ≥ 历史最高 修理数) +_check_personal_best()钩子 + PauseMenu "本次 Run / 历史最佳" 双行显示;ASSET_REGISTRY A067personal_best成就登记;test_t129_save_health_smoke.gd+test_t130_personal_best_smoke.gdPASS - #67 — T127 Run 编号 + 历史最佳 + T128 SaveSystem CRC32:GameState
current_run_id(UTC yyyymmddhhmmss) +SaveSystem每次 save 写入run_id字段 + PauseMenu 顶部 "Run #yyyymmddhhmmss";T128 SaveSystem CRC32 校验(save 头 8 字节 + payload + checksum)+get_save_meta()API + 自动修复损坏检测;test_t127_run_id_smoke.gd+test_t128_crc32_smoke.gdPASS - #66 — F003 smoke_consistency.sh + T126 Player Profile:T125
tools/check_smoke_consistency.sh(6 条规则 144 行 bash: smoke_test_count >= 15 / README BGM 数 / ASSET_REGISTRY 总数 / PROJECT_NAME 一致 / headless 启动 0 错 / uid 已生成) 全 PASS;T126 PauseMenu Player Profile (3 卡片: Player Name / Total Playtime / Best Run Summary) + ProjectSettings 输入字段;test_t126_player_profile_smoke.gdPASS - #65 — Review #65 (D001-D004 修复轮):D001
pausedSignalListener 重复(player.gd 重复监听)已重构为单连接 + 幂等检查;D002pause_menu.gd._build_achievement_grid16x16 texture 引用 orphan 修复(增加 GroupReferenceHolder 跟踪);D003t134_dynamic_slot_count测试 UID 漏提交修复;D004_handle_wave在 is_dashing 状态触发造成动画穿插修复;44 class_name 零冲突 + 73 signal 完整 + 114 PNG 合法 + 65 ASSET_REGISTRY + 7 冒烟测试套件 14 测试全 PASS - #64 — T122 IntroCutscene ambient + T123 whisper_hollow 路由 + T124 BGM 9 主题色板文档:IntroCutscene 8s → 12s (加 ambient layer 渐入 / 渐出 4s) + 文档同步;T123 audio_manager_enhanced.gd
route_for_scene()加intro_cutscene → whisper_hollow分支;T124 STYLE_GUIDE.md BGM 节扩展 9 主题色板表格(archive_calm / archive_boss / archive_boss_dual / archive_dawn / archive_storm / silence_void / whisper_hollow / finale / intro);test_t122_intro_ambient_smoke.gd+test_t123_whisper_routing_smoke.gdPASS - #63 — T121 audio_presets.gd 重构 + T118 whisper_hollow + T120 README Game States 节:T121 audio_presets.gd 新建 (8 BGM 主题常量 + tier 等级 + 调色板 + 路由映射 集中 5 段 → 1 段) audio_manager_enhanced.gd
_MUSIC_PRESETSdict 抽取;T118whisper_hollowBGM 主题 (F# minor BPM 64 / 全 5th + 7th / LFO 0.4Hz / 4-volume mute 主旋律) +route_for_scene("whisper_hollow")+ PauseMenu 设置 routing 优先级;T120 README 新增 "Game States" 节 (intro / hub / archive / boss / death / respawn 6 状态 + BGM 主题映射表);test_t121_audio_presets_smoke.gd+test_t118_whisper_hollow_smoke.gdPASS - #62 — T117 finale 曲式:audio_manager_enhanced.gd
_MUSIC_PRESETS["finale"]落地 (C major → E minor 终止 + 16-note descending arpeggio + tier 4 + GameState._on_full_archive_collected 触发);ASSET_REGISTRY A066finale_theme登记;test_t117_finale_smoke.gdPASS - #61 — T114 silence_void BGM + T115 死亡碑文 + T116 InkWarden 残影:T114 silence_void BGM (D minor BPM 48 / drone + 0.18Hz LFO / 4-volume 全 mute 主体) + audio_manager_enhanced.gd
route_for_scene("silence_void")+ tier 1;T115 player.gddie()tween 链加 0.4s 灰调 wash 后的"墓志铭"字幕(font_size 8 → 6 fade-in);T116 ink_warden.gdphase_2_silhouette_remain()(死亡后 2.5s 残影淡出) +silhouette_alphatween (0.6 → 0) + z_index=10 顶层显示;ASSET_REGISTRY A064silence_void+ A065silhouette_remain登记;test_t114_silence_void_smoke.gd+test_t115_death_inscription_smoke.gd+test_t116_silhouette_smoke.gdPASS - #60 — Review #60 (this iteration): code quality / gameplay / asset / docs audit. Fixed 2 general (G001 README BGM 主题数 6 → 7 含 archive_storm / G002 Recent work 补 #59 archive_storm + CHANGELOG 同步 + _unlock_timestamps 补记)
- #59 — 文档同步 + 第 7 主题 BGM archive_storm 落地:补全 #57(成就时间戳 + CONTRIBUTING)和 #58(README 引用 + PauseMenu hover + 死亡回 Hub 端到端冒烟)两条本该在那两轮就追加的 CHANGELOG 段;T107 在
audio_manager_enhanced.gd_MUSIC_PRESETS新增archive_storm(E minor BPM 120 / 16-note chromatic arpeggio / G#6 shimmer / 0.66Hz LFO / 4-volume 全上抬) +_BOSS_MUSIC_TIER["archive_storm"] = 3(严格 > archive_boss_dual tier 2);ink_warden.gd:529_enter_phase_2()把request_boss_music("archive_boss_dual")替换为request_boss_music("archive_storm", 600);ASSET_REGISTRY A063 条目登记;test_t107_archive_storm_smoke.gd(198 行 10 项断言) PASS - #58 — README CONTRIBUTING 入口暴露 + PauseMenu hover 高亮 + T079 端到端冒烟(本轮):T113 英文 README 「## Development」节顶部加
CONTRIBUTING.md链接 + 9 节内容简述,README.zh-CN.md 同步加中文版(仓库结构 / 3 种 Godot 拼合 / 7 冒烟测试套件 / 提交格式 / 迭代节奏 / 美术登记 / 文档同步 5 问 / 故障排查 / 决策记录);T111pause_menu.gd._build_achievement_grid给每个 16x16 TextureRect 加mouse_filter=STOP+mouse_entered/exitedconnect +_on_slot_hover_in/out0.12s tween(scale 1.0→1.5x + self_modulate 灰→亮 1.4 + modulate 暖色 1.2,1.1,0.9,parallel 三套同步)让玩家 hover 时图标"亮起来";T112 新建tools/test_t112_respawn_hub_e2e_smoke.gd(213 行) 13 项集成断言覆盖 T079 端到端流程(GameState respawn_to_hub 字段 + API + 常量 + 双分支 + GFC._ready 顺序修复 + settings_menu.cfg 持久化 + .tscn toggle label),冒烟测试 7→8 - #57 — 成就解锁时间戳 + CONTRIBUTING.md:T109
PlayerStats._unlock_timestamps: Dictionary+get_unlock_timestamp(id) / get_unlocked_achievements_sorted_by_time()API + 重复 unlock 保留首次时间;PauseMenu 成就 grid tooltip "解锁于 MM-DD HH:MM" + LatestUnlock label;T110 新建CONTRIBUTING.md(194 行) 9 大节新协作者指南 - #48 — Death freeze-frame VFX + README godot binary 快速指引(本轮):T092
player.die()开头Engine.time_scale = 0.2+sprite.modulate = Color(1.4, 0.45, 0.45)链入 tween 首位(tween_interval(0.15)→_end_death_freeze_frame回调恢复 time_scale=1.0 → T075 既有 0.5s lay-down + 1.0s fade-out 红调衰减,"drained red" 而非 flashing red),respawn_at()兜底重置 time_scale;T091 README 新增 "Headless Godot Binary Setup" 子节(方法 A unzip + 方法 B Pythonzipfile完整命令 + first-run--import强提醒 + godot/README.md 交叉链接),Tech 节 "Local Godot binary" / "Death & respawn" 行同步更新 - #47 — Screen shake polish + decorative props:T089
src/autoload/screen_shake.gdautoload(8 个预设含 BOSS_PHASE2 5.0/0.30s 新增最高强度,Timer 30Hz micro-shake + Tween quad ease-out 衰减);T090 6 个程序化像素装饰物件 (A055-A060 hourglass 12x16 / wave_totem 12x24 / hanging_bell 8x10 / crystal_cluster 16x12 / standing_lantern 8x20 / sound_pillar 8x24) + 14 个 archive_01-04 装饰实例(z_index=-1 排在背景上、玩家下) - #46 — Boss 阶段 2 (InkWarden phase 2)
- #45 — Review #45 (this iteration): code quality / gameplay / asset / docs audit. Fixed 1 minor (L001:
ArchivistShadow→WardenShadownode rename inhub_room.tscnto match its actual InkWarden silhouette content) + 4 general (G001 ASSET_REGISTRY A051 拆为 A051 portrait + A053 sprite / G002 README BGM 主题数 5 → 6 含 archive_dawn / G003 achievements.json full_archive 描述与 4 房间数对齐 / G004 Recent work 补 #40-#44) - #44 — T087 第 6 BGM 主题 archive_dawn + T086 Settings 重映射打磨:G major 三和弦 BPM 76 / GAME_OVER_SUCCESS 自动切换 / full_archive 解锁主动触发;Settings 7 动作扩 (含 move_right/bind/cut) / 冲突 swap 检测 / ESC 取消 / 青色确认闪烁 / "恢复默认按键" 按钮
- #43 — T083 营销截图 (M10 最后阻塞解除):
tools/screenshot_capture.gd(真实 GDScript 抓帧工具,桌面环境可用) +tools/generate_screenshot_mockups.py(沙箱 fallback,Python+Pillow 合成 6 张 1920x1080 PNG) +tools/README.md(使用说明 + 沙箱限制说明)。README 新增 Screenshots 节 - #42 — Final polish M12: archive_01 + archive_03 opt-in
atmosphere: true(all 4 archives now have bell-repair warm reflow) +godot/README.mdPythonzipfilefallback command (F003) - #41 — Store NPC: Hub
silent_merchant+ 5 permanent upgrades (heart_crystal / resonance_chime / pulse_focus / echo_charm / silence_breaker) — 跨 run 持久化 - #40 — Review #40: 87 PNG headers valid, 0 static errors, 0 runtime regressions, L001
test_api.pngJPEG 伪装清理 - #38 — 4th archive room (Resonance Shrine, 2 InkWardens) + boss music ref-count
- #36 — Death animation + Steam description: 1.5s lay-down, full English Steam copy
- #35 — Review: 87 PNG headers valid, 0 static errors, 0 runtime regressions
- #34 — Settings + Intro: Saves tab with delete-all, 8s IntroCutscene
- #33 — Save system: 3-slot disk persistence + Continue + auto + manual
- #32 — Steam capsules: 616×353, 460×215, 1200×630 marketing art
- #31 — BGM: archive_boss theme + pre-warm cache + override
- #30 — Review: 84 PNGs valid, 8 achievement icons palette-verified
- #29 — BGM core: 3 synthesized themes + scene routing
- #28 — Polish: 8 achievement icons + Credits screen
ROADMAP.md— full task list, current candidate pool, and "已完成" timestampsCHANGELOG.md— per-iteration changelog with what shipped and what was learnedREVIEW_LOG.md— every 5th-iteration audit (code quality, gameplay, assets, docs, drift)STYLE_GUIDE.md— visual constitution; all new art must inherit from thisASSET_REGISTRY.md— material ledger; all new assets must be appended here
Rooms can now be defined in JSON under data/rooms/. See data/rooms/README.md for the full schema. To test a JSON room, open src/scenes/json_room.tscn and set the room_id export variable, or call RoomLoader.load_room(room_id, parent_node) from GDScript.
🇬🇧 English (this file) · 🇨🇳 简体中文版