Skip to content

saya-ch/utau

Repository files navigation

Voxglass

A 2D pixel art action-exploration game about restoring lost voices in a flooded underground archive.

🇨🇳 简体中文版 README 可用。

Status

Work-in-progress vertical slice. Current milestone: playable 60-second room demonstrating "enter -> Pulse -> repair -> collect -> exit" core loop.

Tech

  • Engine: Godot 4.6.3 (verified — config/features=4.4 retained for backward compat, parses clean on 4.6.3 per REVIEW_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_boss for single InkWarden in archive_03 / archive_boss_dual for the two-Warden room archive_04 / archive_dawn for victory / hub return / archive_storm tier-3 boss phase-2 escalation — InkWarden half-health transition auto-switches / whisper_hollow for late-game Hub — switched automatically once 2 archive rooms are cleared, see ## BGM Palette below / silence_void for GAME_OVER_FAILURE + finale phase 1, see ## Game States) — all generated at runtime via AudioStreamWAV synthesis in src/scripts/audio_manager_enhanced.gd + the 9-preset data table in src/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, modulate shifts to Color(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 in Settings → Saves for the classic "respawn at last Save Lantern" experience.
  • Two-stage archive lighting (M12 polish, T076): when a room's voice_bell is 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": true in their data/rooms/archive_*.json files.
  • Local Godot binary: godot/Godot_v4.6.3-stable_linux.x86_64 (see godot/README.md and the Headless Godot Binary Setup section below for the first-time extraction + --import recipe)

Project Structure

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

Controls

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)

Screenshots

6 营销截图位于 docs/screenshots/(1920x1080 PNG,480x270 内部 4x 整数倍缩放):

  1. 01_title_screen.png — 标题屏(VOXGLASS + 4 按钮)
  2. 02_hub_room.png — Hub 安全区 + 4 扇门 + 墨守者剪影
  3. 03_archive_01_pulse.png — 第一档案房 + Saya + SilenceMote + Pulse 圆环
  4. 04_archive_03_boss.png — 第三档案房 + InkWarden Boss
  5. 05_archive_04_double_boss.png — 第四档案房「共鸣祭坛」+ 双 InkWarden
  6. 06_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

Save System

Three save slots are persisted to user://saves/slot_N.json. Each slot captures:

  • current_room + current_scene (room id + .tscn path, so Continue reloads the right scene)
  • health / resonance / shards and the rooms_completed set
  • unlocked abilities (bind / cut) and the player's last checkpoint_position
  • run_time_seconds (in-game timer)
  • All achievements unlocked so far (also written-through to user://achievements.json on 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.

Game States

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_voidarchive_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.

BGM Boss Override (orthogonal to state)

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.

BGM Palette

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

Tonal map philosophy

  • Major keys (title_intro D / hub_warm F / archive_dawn G) read as "the world is intact / hopeful / bright". The Hub starts on F major (brightest), and archive_dawn is 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_exploration A / archive_boss A / archive_boss_dual A / archive_storm E / whisper_hollow D) read as "the archive is decayed / melancholy / dangerous". Three of the four share A minor so the boss-fight crossfade is harmonic; archive_storm breaks to E minor for harmonic contrast (chaos, not just more intensity) and whisper_hollow breaks 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_void is 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" before archive_dawn resolves 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 by intro_cutscene.gd._play_sequence(). It lives under the upcoming title_intro BGM so the cutscene never plays over a hard silence.

Audio Controls

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.

Development

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.

Headless Godot Binary Setup

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.7d41c59c4

First-run import cache is mandatory — the .godot/imported/*.ctex cache is git-ignored, so the very first Godot run must regenerate it, otherwise every PNG fails to load and cascades into 8+ spurious SCRIPT ERROR lines:

./Godot_v4.6.3-stable_linux.x86_64 --headless --import --path /workspace

For deeper troubleshooting see godot/README.md.

Development Roadmap

We iterate hourly against a publicly visible backlog. The current backlog lives in ROADMAP.md with task IDs T001TNNN and timestamps marking completion.

Milestones

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

Recent completed work

  • #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_COLOR Coral Pulse #E86D5A + VERB_HIT_BIND_COLOR Muted Violet #65506A + VERB_HIT_CUT_COLOR Amber Voice #F2B66E + VERB_HIT_ECHO_COLOR Glass Cyan #69C7CE,严格对应 STYLE_GUIDE 限制色板,1:1 集中定义让"调色板刷新只动 4 行")+ src/scripts/player.gd 5 处 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.gd 5 处字面值断言(T170a.5/6 Bind + T170b.3/4/5 Echo 3 处 + T170c.3 Pulse + X.1-X.4 跨任务 4 处)→ 常量引用断言 + tools/test_t171_t170d_smoke.gd T170d.3 1 处 + tools/test_t098_t100_smoke.gd 2 处放宽 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.sh rule 7 自动验证 README.md + README.zh-CN.md 最新 #N = 90 == ITERATION_COUNT.txt 90 → PASS(F002 #85 引入的 hook 持续生效),本轮 L002 无需手动补内容。40/40 100% PASS(与 #90 比 0 增减,0 回归;T172 触发 3 测试套件共 8 处断言更新,回归后全 PASS)+ check_smoke_consistency.sh 7/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.sh rule ⑥ + 新建后先跑 --importgit 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.uidrm + --import 重新生成 uid://bh4oc6o1wkpl6 / uid://clcrt5damt18k)+ ASSET_REGISTRY 72 条 + 40 smoke test 套件 40/40 100% PASS(L001 修复后重测 0 回归)+ check_smoke_consistency.sh 7/7 规则 PASS(rule 7 README 同步 hook 由 #85 F002 引入已工作);严重 0 / 一般 0 / 轻微 1(L001 已修) / 信息 1(F004 audio 闭环建议下个 5 轮间隔 #91-#95 集中做)5 verb windup 闭环最终态 —— Pulse class_name PulseWindupVFX Glass Cyan #69C7CE 1.0×→0.92× 收缩 ring(T166 #85)+ Bind class_name BindWindupVFX Muted Violet #65506A 1.0×→0.85× 螺旋内收(T167 #86)+ Echo class_name EchoWindupVFX Glass Cyan #69C7CE + Pale #B7E7DD + Amber #F2B66E 0.5×→1.0× 球外撑(T168 #86)+ Cut class_name CutWindupVFX Amber #F2B66E 0.0×→1.0× streak 横扫(T169 #87)+ Wave class_name WaveWindupVFX Pale #B7E7DD 3 环 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.gd start_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.gd Pale 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 = 10 above 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_wavepreload("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_hitScreenShake.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 #B7E7DD Pale 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 命中反馈 —— _readybind_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.050.15s 不会重叠(fire shake 衰减完 = hit shake 才开始),形成"推→中"两步触觉。新冒烟测试 tools/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 Node2D 4 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 verb start_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_arc 12 段 / 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 高光 rim draw_arc 1.5px alpha 0→0.55 / Layer 3 中央暖点 draw_circle(Vector2.ZERO, 2px) alpha 0→0.45,三色皆来自 EchoVFX palette 维持 verb 调色一致(fill #69C7CE Glass Cyan / rim #B7E7DD Pale Resonance / core #F2B66E Amber Voice),alpha ramp-in t / 0.5比 Pulse 的 0.4 更快——Echo windup 仅 0.08s 短窗口,前 0.04s 必须 readable);echo_ability.gd 同模式集成 windup_vfx + _exit_tree 钩子。F006 player.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 falseelse 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() helperaudio_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_singleton false-positive 漏报),新增私有 _has_screen_shake_autoload() helper(tree.root.has_node("ScreenShake") 探测 + Engine.get_main_loop null 守卫);新文件 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.gd windup_time: float = 0.080.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_tree scene-change / _process._max_lifetime 超时自清);player.gd 新增私有 helper func _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.gd 23 项新断言 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 色常量 (#65506A Muted Violet / #B7E7DD Pale Resonance / #F2B66E Amber 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.gd flash_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.gd F002.7 / F002.8 硬编码 #81 → 动态 ITERATION_COUNT.txt - 1 (含 file-not-found fallback), (3) 复用 (1) 顺带同步 T147 守卫与 #76 重命名; test_t101_t163_f004_smoke.gd 18 项新断言 PASS + 全 36/36 冒烟测试套件 PASS
  • #83 — T162 PlayerProfilePanel "最近 5 局详细" 列表 + T159 InkWarden phase 2 dissolve 0.25s 出 + 0.30s 入 tweenpause_menu.tscnProfileTrend20 之后新增 ProfileRecentTitle("✦ 最近 5 局 ✦" Amber Voice 9pt center)+ ProfileRecentList VBoxContainer;pause_menu.gd 新增 @onready var _profile_recent_list + 3 常量(_PROFILE_RECENT_RUNS_MAX=5 视觉密度上限 / _COLOR_RECENT_RUN_NORMAL Pale Resonance 沿用 trend 调色板 / _COLOR_RECENT_RUN_LATEST Amber 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.gd 21 项断言 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.md 4 文档同步重写为 方法 B-1 unzip -FF 强容错(沙箱 / Python 3.14+ 推荐)+ 方法 B-2 Python zipfile 兜底(仅 Python ≤ 3.13 有效),实测复现 Python 3.14.4 BadZipFile: Bad magic number for file headerpause_menu.tscn 新增 NewAchvBanner Label(top center Amber Voice 10pt "✦ 新成就!✦")+ pause_menu.gd 3 常量(_BANNER_DURATION=0.8 / _BANNER_FADE=0.4 / _BANNER_RECENT_UNLOCK_WINDOW=5.0)+ 双轨触发(menu 可见直接 animate + 不可见记 _last_seen_unlock_ts 5s 窗口内 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 / autosave SaveSystem.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.godot autoload 段注册 + player.gd _ready/_exit_tree register/unregister + is_action_globally_blocked() 改 thin delegate + resonance_wave_ability.gd is_globally_blocking() 头部加 D001 refactor 注释;test_d001_t160_t161_f003_smoke.gd 21 项断言 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.sh README 同步检查 hook 规则 ⑦echo_ability.gd 新增 signal echo_multi_reflect(count: int) + const MULTI_REFLECT_THRESHOLD = 4 + 在 _reflect_projectile 末尾首次达到 4 emit 一次(同 cast 后续反弹不再 emit 防 spam);player.gd._readyhas_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.gd 28 项断言 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.sh 6/6 规则 PASS;严重 0 / 一般 1(G001 README Recent work 补 #76-#79 4 轮已修)/ 轻微 0 / 信息 1
  • #79 — T152 0 数灰阶 + T153 槽位 jingle + T151 "最近" badgepause_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.gd 19 项 PASS
  • #78 — T144 wave_focus 谐波 + T148 wave_combo chime tail + T154 灯反向闪audio_manager_enhanced.gd _wave_hit_streams: Dictionary 4 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.gd flash_coral_pulse() 0.15s Coral Pulse 反向闪 + silenced_web.gd on_cut_triggered 迭代 save_lantern group 触发;test_t144_t148_t154_smoke.gd 26 项 PASS
  • #77 — T150 5 动词 profile + T147 jump 阻塞 UX + T149 Echo parallaxplayer_stats.gd last_used_verb 字段 + record_ability_used 入口首行刷新 + reset_stats 清空 + pause_menu.tscn ProfileLastVerb Label + pause_menu.gd match 5 动词 BBCode 调色板(pulse Coral / bind Violet / cut Amber / echo Cyan / wave Pale Resonance);hud.gd show_jump_blocked() + player.gd _handle_jump 双层守卫(is_action_just_pressed 触发);echo_vfx.gd PARALLAX 三常量(rotation 0.5 / radius 1.08 / alpha 0.55)+ PI/8 偏移 + 0.25 rad/s 副层旋转;test_t150_t147_t149_smoke.gd 22 项 PASS
  • #76 — T143 wave 4 状态提示 + T145 is_action_globally_blocked 重构 + T146 wave_combo 屏震hud.gd 4 verb 专属方法(charging / winding_up / active / blocked)+ player.gd _handle_wave 4 分支路由按生命周期排(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.gd wave_combo signal(@export wave_combo_threshold=3)+ _deactivate_wave 末尾 emit;player.gd _on_wave_combo shake(4.0, 0.4) + flash_color(Electric Violet #8C5BFF, 0.18s, 0.30);test_t143_t145_t146_smoke.gd 25 项 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.json quintuple_voice 入列;T142 resonance_wave_ability.gd._try_fire() 加 5 帧 verb-action-only 窗口(拒绝 is_on_floor_only=trueis_dashing 期间触发的"动画中波");T141 resonance_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.gd 14 项断言 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 A072 quintuple_voice 5-verb 一次完成成就落地;新增 tools/test_t139_quintuple_voice_smoke.gd + tools/test_t140_wave_verb_prompts_smoke.gd PASS
  • #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.gd 8 层视觉组 + HUD WaveRow 第五冷却条(Electric Violet 主题色);T137 save_load_menu.gd quick 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.gd PASS
  • #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.gd PASS
  • #70 — Review #70 (D001-D003 严重问题修复): D001 _autosave_timer 改 Timer 节点 (单 timer / pause_mode=PROCESS) + SceneTree 改 _autosave async;D002 get_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.gd PASS
  • #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 A067 personal_best 成就登记;test_t129_save_health_smoke.gd + test_t130_personal_best_smoke.gd PASS
  • #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.gd PASS
  • #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.gd PASS
  • #65 — Review #65 (D001-D004 修复轮):D001 paused SignalListener 重复(player.gd 重复监听)已重构为单连接 + 幂等检查;D002 pause_menu.gd._build_achievement_grid 16x16 texture 引用 orphan 修复(增加 GroupReferenceHolder 跟踪);D003 t134_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.gd PASS
  • #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_PRESETS dict 抽取;T118 whisper_hollow BGM 主题 (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.gd PASS
  • #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 A066 finale_theme 登记;test_t117_finale_smoke.gd PASS
  • #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.gd die() tween 链加 0.4s 灰调 wash 后的"墓志铭"字幕(font_size 8 → 6 fade-in);T116 ink_warden.gd phase_2_silhouette_remain() (死亡后 2.5s 残影淡出) + silhouette_alpha tween (0.6 → 0) + z_index=10 顶层显示;ASSET_REGISTRY A064 silence_void + A065 silhouette_remain 登记;test_t114_silence_void_smoke.gd + test_t115_death_inscription_smoke.gd + test_t116_silhouette_smoke.gd PASS
  • #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 问 / 故障排查 / 决策记录);T111 pause_menu.gd._build_achievement_grid 给每个 16x16 TextureRect 加 mouse_filter=STOP + mouse_entered/exited connect + _on_slot_hover_in/out 0.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 Python zipfile 完整命令 + first-run --import 强提醒 + godot/README.md 交叉链接),Tech 节 "Local Godot binary" / "Death & respawn" 行同步更新
  • #47 — Screen shake polish + decorative props:T089 src/autoload/screen_shake.gd autoload(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: ArchivistShadowWardenShadow node rename in hub_room.tscn to 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.md Python zipfile fallback 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.png JPEG 伪装清理
  • #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

What to read next

  • ROADMAP.md — full task list, current candidate pool, and "已完成" timestamps
  • CHANGELOG.md — per-iteration changelog with what shipped and what was learned
  • REVIEW_LOG.md — every 5th-iteration audit (code quality, gameplay, assets, docs, drift)
  • STYLE_GUIDE.md — visual constitution; all new art must inherit from this
  • ASSET_REGISTRY.md — material ledger; all new assets must be appended here

Room Editor (JSON)

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) · 🇨🇳 简体中文版

About

修复被寂静吞噬的声音,在沉没的档案馆里找回失落的歌声。

Resources

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors