From 383791db14d28c8f04b22912142da985ea1c5a57 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Fri, 24 Jul 2026 09:08:04 +0800 Subject: [PATCH] =?UTF-8?q?docs(spec):=20E6-a=20=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E6=97=A0=E6=95=8C=E5=B8=A7=E8=AE=BE=E8=AE=A1=E2=80=94=E2=80=94?= =?UTF-8?q?take=5Fdamage=20=E5=8D=95=E7=82=B9=E9=97=A8=E6=8E=A7=20+=20?= =?UTF-8?q?=E5=A4=8D=E6=B4=BB=20hurt=20=E5=8F=8D=E9=A6=88=20+=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=A9=B1=E5=8A=A8=E6=97=B6=E9=95=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缺失功能路线图 E6-a 先行小项。受击后 0.5s 无敌窗口(时间戳门控, 杜绝多弹同帧连扣血);顺带复活当前死代码的 PLAYER_DAMAGED emit 与 hurt 音效;spend_hp_cost 自伤不受影响;时长入 balance.json + 设计器。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../specs/2026-07-24-player-iframes-design.md | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs_dev/specs/2026-07-24-player-iframes-design.md diff --git a/docs_dev/specs/2026-07-24-player-iframes-design.md b/docs_dev/specs/2026-07-24-player-iframes-design.md new file mode 100644 index 0000000..88320be --- /dev/null +++ b/docs_dev/specs/2026-07-24-player-iframes-design.md @@ -0,0 +1,81 @@ +# 玩家无敌帧(i-frames)设计 + +> **日期**:2026-07-24 +> **Epic**:E6-a(缺失功能路线图 `docs_dev/plans/2026-07-23-missing-features-roadmap.md`) +> **优先级**:P1 先行小项 · 工作量 S +> **目标**:玩家受击后获得短暂无敌窗口,杜绝多弹同帧连扣血,提升 Boss 弹幕公平性;顺带复活当前失效的受击反馈(hurt 音效 / HUD)。 + +--- + +## 1. 背景与现状(代码复核 2026-07-24) + +- 玩家外部伤害**唯一入口**为 `PlayerStats.take_damage(amount)`(`player_stats.gd:65`)。调用方 3 处: + - `enemy_bullet_manager.gd:33` —— 敌弹命中。 + - `boss_manager.gd:212` —— Boss 冲刺接触(该 action 自带 `hit_cd` 0.4s)。 + - `boss_manager.gd:235` —— Boss 近战 windup 命中。 +- **无任何无敌窗口**:多颗敌弹同一 `_physics_process` 帧命中会连续多次扣血,弹幕海里不公平。 +- **`PLAYER_DAMAGED`(=5) 事件当前无人 emit**(grep 零匹配): + - `audio_manager.gd:36` 订阅它播 `hurt` 音效 → **死代码,从不触发**。 + - `player_stats.gd:33` 自订阅 `PLAYER_DAMAGED → _on_player_damaged → take_damage` → **死代码**(无人 emit)。 +- 自伤入口 `spend_hp_cost`(`player_stats.gd:108`,infinite_spells 卖血)与外部伤害**语义独立**,不应受无敌帧影响。 +- 玩家可视节点为 `combat_s2.gd:_setup_player()` 创建的 `Node2D`(含青色菱形 `Polygon2D` + 光晕),存于 `PlayerManager._player_node`,`modulate` 可用于闪烁。 + +## 2. 设计 + +**单点集中在 `PlayerStats.take_damage()`** —— 3 处调用方无需改动逻辑(仅确认走此入口)。 + +### 2.1 无敌窗口机制 + +- `PlayerStats` 新增 `var _invuln_until_msec: int = 0`(时间戳,`Time.get_ticks_msec()`,与 `player_manager` dash 边沿同法,**无需每帧 tick 驱动**)。 +- `take_damage(amount)` 改为: + 1. `var now := Time.get_ticks_msec()` + 2. 若 `now < _invuln_until_msec` → **直接 return**(无敌中,忽略本次外部伤害)。 + 3. 否则:扣血(保留原难度减伤 `player_dmg_taken_mult()`);设 `_invuln_until_msec = now + int(SettingsManager.player_iframe_sec() * 1000.0)`;`EventBus.emit(PLAYER_DAMAGED, {"damage": applied})`(复活 hurt 音效 / 未来 HUD 钩子)。 + 4. `stats_changed.emit()`;`hp<=0` 时发 `PLAYER_DIED`(同现状)。 +- 新增查询 `func is_invulnerable() -> bool: return Time.get_ticks_msec() < _invuln_until_msec`(供 HUD 闪烁读)。 +- **移除** `player_stats.gd:33` 的 `EventBus.subscribe(PLAYER_DAMAGED, _on_player_damaged)` 与 `_on_player_damaged` 函数 —— 否则 `take_damage` emit `PLAYER_DAMAGED` 会自递归;该订阅本就是死代码。 +- **`spend_hp_cost` 保持不变** —— 不查 / 不设无敌窗口(自伤代价独立)。 +- **`reset_for_run()` 重置** `_invuln_until_msec = 0`。 + +### 2.2 覆盖来源 + +三处外部伤害全部经 `take_damage` → 自动受无敌帧保护,**无需逐点改**。Boss 冲刺原有 `hit_cd` 保留(全局 i-frame 主导,二者叠加无害)。 + +### 2.3 数据驱动 + +- `data/balance.json` 顶层新增 `"player_iframe_sec": 0.5`。 +- `SettingsManager` 新增访问器 `func player_iframe_sec() -> float`(读 `_balance.get("player_iframe_sec", 0.5)`,缺失回退 0.5 并可 push_warning,与既有 `_mult` / infinite 访问器同风格)。 +- 游戏设计器「平衡 Balance」页新增一栏 `SpinBox`(0.0~2.0,step 0.05),复用 `designer_ui.gd` 的 `UI.spin`,中英标签「无敌帧时长(秒) player_iframe_sec」,读写往返一致。 + +### 2.4 视觉反馈(HUD 闪烁) + +- `combat_s2._process(delta)` 每帧末尾:读 `PlayerStats.is_invulnerable()`。 + - 无敌中:`_player_node.modulate.a` 按 8Hz 方波在 `1.0 / 0.35` 间切换(用 `Time.get_ticks_msec()` 求相位,避免累加状态)。 + - 非无敌:`_player_node.modulate.a = 1.0`(确保恢复)。 +- hurt 音效经 §2.1 的 `PLAYER_DAMAGED` emit 自动复活(`audio_manager.gd:36` 现有订阅)。 + +## 3. 影响文件 + +| 文件 | 改动 | +| :-- | :-- | +| `scripts/autoloads/player_stats.gd` | i-frame 时间戳 + `take_damage` 门控 + emit + `is_invulnerable()`;移除死订阅 | +| `scripts/autoloads/settings_manager.gd` | `player_iframe_sec()` 访问器 | +| `data/balance.json` | 新增 `player_iframe_sec: 0.5` | +| `scenes/main/combat_s2.gd` | `_process` 无敌期玩家节点闪烁 | +| `addons/game_designer/*`(平衡页) | 新增 `player_iframe_sec` SpinBox + 读写 | + +## 4. 验收标准 + +1. 多颗敌弹同帧命中玩家 → HP **只扣一次**(MCP:批量 `spawn_bullet` 于玩家位置,断言 HP 降幅 = 单次)。 +2. 受击后 `player_iframe_sec` 秒内任何外部伤害被忽略;窗口结束后可再次受击。 +3. 无敌期间玩家节点闪烁;`hurt` 音效在实际受击时播放。 +4. `spend_hp_cost`(卖血)**不受** i-frame 影响,仍正常扣血。 +5. balance.json 改 `player_iframe_sec` → 窗口时长实测随之变化;设计器该栏读写往返 `FAILS=0`。 +6. 全程 GDScript `validate_script` 通过;纯数据驱动,无硬编码时长副本。 + +## 5. 非目标(YAGNI) + +- 不做「击退 / 受击硬直」。 +- 不做「按伤害来源区分不同无敌时长」(统一一个窗口)。 +- 不改 Boss 冲刺 `hit_cd`(保留,无害)。 +- 不做无敌期间的碰撞体尺寸/闪避概率等额外机制。