缺失功能路线图 E6-a 先行小项。受击后 0.5s 无敌窗口(时间戳门控, 杜绝多弹同帧连扣血);顺带复活当前死代码的 PLAYER_DAMAGED emit 与 hurt 音效;spend_hp_cost 自伤不受影响;时长入 balance.json + 设计器。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.3 KiB
5.3 KiB
玩家无敌帧(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_cd0.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_managerdash 边沿同法,无需每帧 tick 驱动)。take_damage(amount)改为:var now := Time.get_ticks_msec()- 若
now < _invuln_until_msec→ 直接 return(无敌中,忽略本次外部伤害)。 - 否则:扣血(保留原难度减伤
player_dmg_taken_mult());设_invuln_until_msec = now + int(SettingsManager.player_iframe_sec() * 1000.0);EventBus.emit(PLAYER_DAMAGED, {"damage": applied})(复活 hurt 音效 / 未来 HUD 钩子)。 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_damageemitPLAYER_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_DAMAGEDemit 自动复活(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. 验收标准
- 多颗敌弹同帧命中玩家 → HP 只扣一次(MCP:批量
spawn_bullet于玩家位置,断言 HP 降幅 = 单次)。 - 受击后
player_iframe_sec秒内任何外部伤害被忽略;窗口结束后可再次受击。 - 无敌期间玩家节点闪烁;
hurt音效在实际受击时播放。 spend_hp_cost(卖血)不受 i-frame 影响,仍正常扣血。- balance.json 改
player_iframe_sec→ 窗口时长实测随之变化;设计器该栏读写往返FAILS=0。 - 全程 GDScript
validate_script通过;纯数据驱动,无硬编码时长副本。
5. 非目标(YAGNI)
- 不做「击退 / 受击硬直」。
- 不做「按伤害来源区分不同无敌时长」(统一一个窗口)。
- 不改 Boss 冲刺
hit_cd(保留,无害)。 - 不做无敌期间的碰撞体尺寸/闪避概率等额外机制。