8.1 KiB
Rhythm Action Architecture Refactor Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Refactor the project toward the architecture in docs/架构方案.md: global beat clock, four-slot combo window, ActionData resources, ActionResolver priority rules, Player execution components, and CombatManager formulas.
Architecture: Keep EventBus for decoupled broadcast, but add RhythmManager and CombatManager as first-class autoload services. Move domain logic out of Player into beat-aware components and data-driven resource files while preserving current gameplay tests.
Tech Stack: Godot 4.6 GDScript, .tres Resources, headless SceneTree tests.
Task 1: Architecture Target Regression
Files:
-
Create:
tests/test_rhythm_action_architecture.gd -
Step 1: Write the failing test
Create one test that asserts the six requested architecture artifacts exist and expose their intended behavior:
extends SceneTree
var failures: Array[String] = []
func _init() -> void:
_run.call_deferred()
func _run() -> void:
_check_autoloads()
_check_action_data()
_check_combo_window()
_check_action_resolver()
_check_player_components()
_check_combat_manager()
_finish()
- Step 2: Run test to verify it fails
Run:
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s res://tests/test_rhythm_action_architecture.gd
Expected: FAIL because RhythmManager, CombatManager, ActionData, ActionResolver, ComboWindow, MotionExecutor, and BurstComponent do not exist yet.
Task 2: RhythmManager Autoload
Files:
-
Create:
autoload/rhythm_manager.gd -
Modify:
project.godot -
Modify:
scenes/rhythm/rhythm_conductor.gd -
Modify:
tests/test_rhythm_conductor.gd -
Step 1: Implement
RhythmManager
Create an Autoload node with beat_ticked, judgement_made, and action_judged signals. It owns BPM, beat length, beat index, judgement scale, beat offset, fallback clock timing, and judge_action(action_name).
- Step 2: Register autoload
Add:
[autoload]
RhythmManager="*res://autoload/rhythm_manager.gd"
- Step 3: Preserve
RhythmConductorcompatibility
Keep scenes/rhythm/rhythm_conductor.gd loadable for old tests by delegating equivalent judging math to the same model or leaving it as a scene adapter.
- Step 4: Run rhythm tests
Run:
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s res://tests/test_rhythm_conductor.gd
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s res://tests/test_rhythm_action_architecture.gd
Expected: rhythm conductor still passes; architecture test advances past autoload checks.
Task 3: Four-Slot ComboWindow
Files:
-
Create:
scenes/components/combo_window.gd -
Modify:
scenes/characters/player.tscn -
Modify:
scenes/characters/player.gd -
Modify:
tests/test_combo_window.gd -
Step 1: Implement
ComboWindow
ComboWindow extends Node, stores four slots, records StringName inputs, keeps &"Ø" only as an explicit Miss placeholder, clears on miss/full/action clear, and emits combo_updated/combo_cleared. It must not auto-append &"Ø" just because a beat passed without input.
- Step 2: Replace Player node
Rename or replace ComboTracker with ComboWindow in player.tscn. Keep compatibility methods get_slots(), get_pattern(), get_contiguous_pattern(), queue_clear(), flush_pending_clear() so existing tests can continue running while behavior becomes component-based.
- Step 3: Run combo tests
Run:
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s res://tests/test_combo_window.gd
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s res://tests/test_player_combo_input.gd
Expected: existing combo behavior remains green, and the new architecture test confirms ComboWindow does not auto-append &"Ø" just because a beat passed without input.
Task 4: ActionData and ActionResolver
Files:
-
Create:
resources/action_data.gd -
Create:
resources/actions/*.tres -
Create:
scenes/combat/action_resolver.gd -
Modify:
resources/skill_data.gd -
Modify:
scenes/combat/input_resolver.gd -
Modify:
scenes/characters/player.gd -
Modify:
tests/test_combo_window.gd -
Modify:
tests/test_architecture_refactor.gd -
Step 1: Add full
ActionData
Create ActionData extends Resource with fields from docs/架构方案.md: id, display_name, input_pattern, required_state, base_cost, damage_mult, move_mult_x, move_mult_y, action_beats, hit_type, range, target_type, armor_level, clear_window, can_chain, special, plus compatibility fields animation, energy_cost, energy_reward, spawns_projectile, projectile_scene, and displacement.
- Step 2: Convert resources
Create resources/actions equivalents for the current resources/skills files. After all tests and scripts use ActionData, remove the legacy resources/skills data.
- Step 3: Implement
ActionResolver
Load resources/actions/*.tres, expose resolve(window, state_machine := null, context := {}), resolve_pattern(pattern, state_machine := null, context := {}), reload(), and clear_cache(). Include an ordered rule pass for Space contexts before falling back to pattern matching.
- Step 4: Remove
InputResolverafter migration
Move tests and runtime code to ActionResolver directly, then remove the legacy InputResolver adapter.
Task 5: MotionExecutor and BurstComponent
Files:
-
Create:
scenes/components/motion_executor.gd -
Create:
scenes/components/burst_component.gd -
Modify:
scenes/characters/player.tscn -
Modify:
scenes/characters/player.gd -
Step 1: Add
MotionExecutor
Expose execute(action, direction, beat_time) and tick(delta) to own action duration, lunge timing, and velocity output. Player calls it instead of hand-writing lunge windows.
- Step 2: Add
BurstComponent
Expose ready/active/cooldown state, beat-based duration, cost multiplier, damage multiplier, and judgement scale. It listens to RhythmManager.beat_ticked for duration and cooldown.
- Step 3: Move Player logic
Replace direct charge/burst timing in Player with BurstComponent, and replace direct action displacement with MotionExecutor.
Task 6: CombatManager
Files:
-
Create:
autoload/combat_manager.gd -
Modify:
project.godot -
Modify:
scenes/components/damage_emitter.gd -
Modify:
scenes/characters/player.gd -
Step 1: Implement formulas
CombatManager exposes resolve_damage(base_attack, action, judgement, buffs := null, burst := null), resolve_cost(action, burst := null), and resolve_move(action, judgement, burst := null).
- Step 2: Wire damage and cost
Player uses CombatManager.resolve_cost before action execution. DamageEmitter can accept an action context and asks CombatManager.resolve_damage rather than using scattered multipliers.
Task 7: Verification and Gap Audit
Files:
-
Modify: tests as needed to cover exact behavior.
-
Step 1: Run full suite
Run:
for test in tests/*.gd; do
/Applications/Godot.app/Contents/MacOS/Godot --headless --path /Users/wxm/code/project/Fighting_Rthythm_game -s "res://$test" || exit 1
done
Expected: all tests exit 0.
- Step 2: Search for architecture residue
Run:
rg -n "Timer|charge_duration|attack_lunge|InputResolver|SkillData|ComboTracker|PlayerProjectile\\.new\\(|get_first_node_in_group|has_method|\\.call\\(" autoload resources scenes tests
Expected: any remaining hits are compatibility adapters, tests, or UI-only visual timing, not core gameplay architecture.