Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,3 @@ This file tracks all major tracks for the project. Each track has its own detail

---

- [ ] **Track: Implement Meyer's Square training workflow with visual-only interaction and 'Ghost of Meyer' guidance.**
*Link: [./tracks/meyer_square_20260324/](./tracks/meyer_square_20260324/)*

---

5 changes: 0 additions & 5 deletions conductor/tracks/meyer_square_20260324/index.md

This file was deleted.

8 changes: 0 additions & 8 deletions conductor/tracks/meyer_square_20260324/metadata.json

This file was deleted.

36 changes: 0 additions & 36 deletions conductor/tracks/meyer_square_20260324/plan.md

This file was deleted.

41 changes: 0 additions & 41 deletions conductor/tracks/meyer_square_20260324/spec.md

This file was deleted.

6 changes: 6 additions & 0 deletions src/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ pub struct CurriculumPageText;
/// Marker for the curriculum document image display node.
#[derive(Component)]
pub struct CurriculumDocumentImage;

/// Marker for the UI button that switches the training workflow.
///
/// Used to identify the button that toggles between Circular and Meyer's Square workflows.
#[derive(Component)]
pub struct WorkflowModeButton;
43 changes: 43 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,49 @@ pub const CIRCLE_RADIUS: f32 = 300.0;
/// standard HEMA target diagrams (e.g., Meyer's Square).
pub const LABELS: [u8; 8] = [7, 1, 5, 3, 8, 4, 6, 2];

/// The four concentric sequences of Meyer's Square.
///
/// Organized from outer to inner, with each sequence containing a 4-strike order.
/// Coordinates are normalized from -1.0 to 1.0, with (0,0) at the center.
pub const MEYER_SEQUENCES: [crate::resources::MeyerSequence; 4] = [
// Outer Sequence
crate::resources::MeyerSequence {
nodes: [
crate::resources::MeyerNode { x: -1.0, y: 1.0, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: 1.0, y: -1.0, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: -1.0, y: -1.0, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: 1.0, y: 1.0, technique: crate::resources::TechniqueType::Cut },
],
},
// Outer-Mid Sequence
crate::resources::MeyerSequence {
nodes: [
crate::resources::MeyerNode { x: -0.66, y: 0.66, technique: crate::resources::TechniqueType::Thrust },
crate::resources::MeyerNode { x: 0.66, y: -0.66, technique: crate::resources::TechniqueType::Thrust },
crate::resources::MeyerNode { x: -0.66, y: -0.66, technique: crate::resources::TechniqueType::Thrust },
crate::resources::MeyerNode { x: 0.66, y: 0.66, technique: crate::resources::TechniqueType::Thrust },
],
},
// Inner-Mid Sequence
crate::resources::MeyerSequence {
nodes: [
crate::resources::MeyerNode { x: -0.33, y: 0.33, technique: crate::resources::TechniqueType::Parry },
crate::resources::MeyerNode { x: 0.33, y: -0.33, technique: crate::resources::TechniqueType::Parry },
crate::resources::MeyerNode { x: -0.33, y: -0.33, technique: crate::resources::TechniqueType::Parry },
crate::resources::MeyerNode { x: 0.33, y: 0.33, technique: crate::resources::TechniqueType::Parry },
],
},
// Inner Sequence
crate::resources::MeyerSequence {
nodes: [
crate::resources::MeyerNode { x: -0.1, y: 0.1, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: 0.1, y: -0.1, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: -0.1, y: -0.1, technique: crate::resources::TechniqueType::Cut },
crate::resources::MeyerNode { x: 0.1, y: 0.1, technique: crate::resources::TechniqueType::Cut },
],
},
];

// --- Kinetic Brutalism Color Palette ---

/// Background: The primary void.
Expand Down
46 changes: 45 additions & 1 deletion src/logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ impl Plugin for TrainingPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (
update_sequence_logic,
update_meyer_sequence_logic,
handle_session_controls,
sync_rhythm_timer,
sync_arrow_target,
Expand Down Expand Up @@ -56,8 +57,9 @@ fn update_sequence_logic(
mut current_number: ResMut<CurrentNumber>,
mut sequence_state: ResMut<SequenceState>,
mut rhythm_state: ResMut<RhythmState>,
active_workflow: Res<ActiveWorkflow>,
) {
if !sequence_state.running {
if !sequence_state.running || active_workflow.0 != TrainingWorkflow::Circular {
return;
}

Expand Down Expand Up @@ -109,6 +111,48 @@ fn update_sequence_logic(
}
}

/// System to update the Meyer's Square training sequence logic.
///
/// Cycles through the 16 nodes in their defined sequences.
fn update_meyer_sequence_logic(
time: Res<Time>,
sequence_state: Res<SequenceState>,
active_workflow: Res<ActiveWorkflow>,
mut meyer_state: ResMut<MeyerTrainingResource>,
rhythm_state: Res<RhythmState>,
) {
if !sequence_state.running || active_workflow.0 != TrainingWorkflow::MeyerSquare {
return;
}

// Use rhythm_state.duration for the transition speed.
// The transition_timer goes from 0.0 to 1.0 over the duration.
// time.delta() can be 0 or small in minimal tests without explicit time stepping.
// Instead we use `delta_secs()` or `delta().as_secs_f32()` which gets updated by `time.advance_by`.
let delta = time.delta_secs();

// Safety check against zero duration to avoid division by zero or infinite loop
let dur = rhythm_state.duration.max(0.1);

meyer_state.transition_timer += delta / dur;

// Loop until we consume the timer, since a large delta might skip nodes.
// In normal execution delta is small, but tests could jump large amounts.
while meyer_state.transition_timer >= 1.0 {
// Adjust timer for remaining overflow before advancing so while condition re-checks properly
meyer_state.transition_timer -= 1.0;

// Advance node
meyer_state.current_node += 1;

// If we hit the end of a 4-strike sequence, advance to next sequence
if meyer_state.current_node >= 4 {
meyer_state.current_node = 0;
meyer_state.current_sequence = (meyer_state.current_sequence + 1) % 4;
}
}
}

/// Keeps UI elements synchronized with the internal [`RhythmState`].
///
/// Updates the numeric display and the slider widget whenever the rhythm
Expand Down
51 changes: 51 additions & 0 deletions src/logic_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ mod tests {
.insert_resource(HighlightTimer(Timer::from_seconds(1.0, TimerMode::Repeating)))
.insert_resource(CurrentNumber(0))
.insert_resource(SequenceState { running: true, ..default() })
.insert_resource(ActiveWorkflow::default())
.insert_resource(MeyerTrainingResource::default())
.insert_resource(RhythmState { duration: 1.0, mode: RhythmMode::Constant, accelerate_counter: 0 })
.insert_resource(ArrowTarget::default())
.insert_resource(ArrowAnimationState::default());
Expand Down Expand Up @@ -280,4 +282,53 @@ mod tests {
assert!(animation_state.progress > 0.0);
}
}

/// Verifies that the Meyer sequence logic correctly advances nodes and sequences.
#[test]
fn test_meyer_sequence_advances_nodes_and_sequences() {
let mut app = setup_app();

// Setup Meyer's Square workflow and unpause
{
let mut active_workflow = app.world_mut().get_resource_mut::<ActiveWorkflow>().unwrap();
active_workflow.0 = TrainingWorkflow::MeyerSquare;
let mut seq_state = app.world_mut().get_resource_mut::<SequenceState>().unwrap();
seq_state.running = true;
}

app.update(); // Initialize

// Setup initial delta
{
// Instead of dealing with Bevy's time complexities in tests,
// directly manipulate the internal transition timer.
let mut meyer_state = app.world_mut().get_resource_mut::<MeyerTrainingResource>().unwrap();
meyer_state.transition_timer = 1.0;
}

app.update();

// Node should have advanced from 0 to 1
{
let meyer_state = app.world().get_resource::<MeyerTrainingResource>().unwrap();
assert_eq!(meyer_state.current_node, 1);
assert_eq!(meyer_state.current_sequence, 0);
}

// Advance time by 3 more full durations to complete the sequence
for _ in 0..3 {
{
let mut meyer_state = app.world_mut().get_resource_mut::<MeyerTrainingResource>().unwrap();
meyer_state.transition_timer = 1.0;
}
app.update();
}

// Should now be on node 0 of sequence 1
{
let meyer_state = app.world().get_resource::<MeyerTrainingResource>().unwrap();
assert_eq!(meyer_state.current_node, 0);
assert_eq!(meyer_state.current_sequence, 1);
}
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ fn initialize_app(app: &mut App) {
mode: SequenceMode::Random,
current_ordered_value: 0,
})
.insert_resource(ActiveWorkflow::default())
.insert_resource(MeyerTrainingResource::default())
.insert_resource(RhythmState {
duration: 1.0,
mode: RhythmMode::Constant,
Expand Down
54 changes: 54 additions & 0 deletions src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,60 @@ pub struct HighlightTimer(pub Timer);
#[derive(Resource)]
pub struct CurrentNumber(pub u8);

/// Technique required for a strike in Meyer's Square.
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum TechniqueType {
/// A flowing cut or slash.
#[default]
Cut,
/// A direct thrust.
Thrust,
/// A defensive parry or shield.
Parry,
}

/// Represents a single target node in the Meyer's Square grid.
#[derive(Debug, Clone, Copy)]
pub struct MeyerNode {
/// Normalized X coordinate (-1.0 to 1.0).
pub x: f32,
/// Normalized Y coordinate (-1.0 to 1.0).
pub y: f32,
/// The technique to execute at this node.
pub technique: TechniqueType,
}

/// Represents a 4-strike sequence in the Meyer's Square.
#[derive(Debug, Clone)]
pub struct MeyerSequence {
pub nodes: [MeyerNode; 4],
}

/// Resource to manage the state of the Meyer's Square training flow.
#[derive(Resource, Default, Debug)]
pub struct MeyerTrainingResource {
/// The index of the current sequence (0 to 3).
pub current_sequence: usize,
/// The index of the current node within the sequence (0 to 3).
pub current_node: usize,
/// Progress of the transition to the next node (0.0 to 1.0).
pub transition_timer: f32,
}

/// Workflow mode for the training session.
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum TrainingWorkflow {
/// The original circular target training.
#[default]
Circular,
/// The Meyer's Square grid training.
MeyerSquare,
}

/// Stores the current active workflow mode.
#[derive(Resource, Default, Debug)]
pub struct ActiveWorkflow(pub TrainingWorkflow);

/// Strategy for generating the sequence of target numbers.
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum SequenceMode {
Expand Down
2 changes: 2 additions & 0 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ impl Plugin for UiPlugin {
curriculum_page_button_system,
sync_curriculum_ui_labels,
mode_toggle_system,
workflow_toggle_system,
rhythm_mode_toggle_system,
style_slider_system,
update_rhythm_from_slider,
update_circle_layout,
render_glowing_arrow,
render_meyer_square,
sync_target_visuals,
toggle_curriculum_visibility,
curriculum_keyboard_navigation,
Expand Down
Loading