Skip to content

Conversation

@cleder
Copy link
Owner

@cleder cleder commented Feb 2, 2026

PR Type

Enhancement, Tests, Bug fix, Documentation


Description

  • Implements direction brick impulse system (bricks 43-48, 52) with observer pattern for instantaneous velocity modifications

  • Adds DirectionBrickEffect event signal and apply_direction_brick_effects observer system to apply impulses via ExternalImpulse component

  • Fixes paddle shrink logic to only trigger when ALL balls are lost, not on individual ball loss in multi-ball scenarios

  • Adds comprehensive test suite covering direction brick impulses, paddle shrink edge cases, and multi-ball interactions

  • Updates level loader to include bricks 50 and 54 in completion counting logic

  • Provides extensive specification documentation with TDD requirements, acceptance criteria, and implementation planning

  • Creates GIMP texture assets for ball indicators (1-3 balls) with emissive variants and direction brick material profiles

  • Adds test level 099 featuring all direction brick types and updates pre-commit hooks and typos configuration


Diagram Walkthrough

flowchart LR
  A["Ball Collision<br/>mark_brick_on_ball_collision"] -->|"Compute impulse<br/>for brick type 43-48, 52"| B["Queue DirectionBrickEffect<br/>Event"]
  B -->|"Observer pattern"| C["apply_direction_brick_effects<br/>Observer System"]
  C -->|"Apply velocity"| D["ExternalImpulse<br/>Component"]
  D -->|"Physics update"| E["Ball velocity<br/>modified"]
  F["Ball Lost"] -->|"Check remaining<br/>ball count"| G["apply_paddle_shrink<br/>System"]
  G -->|"Only if ALL balls gone"| H["PaddleGrowing +<br/>InputLocked"]
Loading

File Walkthrough

Relevant files
Tests
4 files
direction_bricks.rs
Direction brick impulse unit and integration tests             

tests/direction_bricks.rs

  • Comprehensive test suite for direction brick impulse system (bricks
    43-48, 52)
  • Tests verify impulse application to ExternalImpulse component without
    full physics simulation
  • Covers cardinal directions (43-46), diagonal directions (47-48), and
    random direction (52) bricks
  • Includes multi-frame persistence tests and RNG distribution validation
+372/-0 
paddle_shrink_multiball.rs
Paddle shrink regression tests for multi-ball scenarios   

tests/paddle_shrink_multiball.rs

  • Tests paddle shrink behavior when multiple balls are in play
  • Verifies paddle does NOT shrink when other balls still exist
  • Validates shrink animation only triggers when last ball is lost
  • Tests interaction between respawn system and paddle visual effects
+213/-0 
paddle_shrink_regression.rs
Paddle shrink edge case and regression testing                     

tests/paddle_shrink_regression.rs

  • Regression test for paddle shrink animation with multiple ball losses
  • Verifies shrink only applies once when losing multiple balls on last
    life
  • Tests that paddle shrink component configuration is correct
  • Validates shrink animation timing and scale targets
+161/-0 
level_099.ron
Direction Brick Test Level 099                                                     

assets/levels/level_099.ron

  • New test level featuring all 7 direction brick types (43-48, 52) in a
    grid layout
  • Includes gravity configuration and presentation metadata with
    descriptive notes
  • Provides minimal ball and paddle placement for testing direction brick
    mechanics
+34/-0   
Enhancement
6 files
brick_effects.rs
Direction brick observer system implementation                     

src/systems/brick_effects.rs

  • New observer system for direction brick impulse effects (bricks 43-48,
    52)
  • Implements apply_direction_brick_effects observer function triggered
    by DirectionBrickEffect events
  • Applies velocity impulses via ExternalImpulse component with proper
    tracing instrumentation
  • Defines brick type constants and impulse magnitude constants for all
    direction brick types
+175/-0 
signals.rs
Direction brick effect event signal definition                     

src/signals.rs

  • Adds DirectionBrickEffect event type for immediate velocity impulses
    via observer pattern
  • Documents impulse semantics for cardinal (43-46), diagonal (47-48),
    and random (52) bricks
  • Specifies per-brick impulse vectors and coordinate system constraints
  • Clarifies distinction between observer events (immediate) and message
    types (buffered)
+56/-2   
lib.rs
Direction brick collision detection and observer registration

src/lib.rs

  • Registers apply_direction_brick_effects observer in core observer
    setup
  • Adds direction brick impulse computation logic to
    mark_brick_on_ball_collision system
  • Implements impulse vector calculation for all 7 brick types (43-48,
    52) with RNG for randomizer
  • Uses queue_silenced for entity despawn commands to avoid command
    conflicts
+47/-2   
mod.rs
Systems module updates for brick effects                                 

src/systems/mod.rs

  • Adds new brick_effects module for direction brick observer system
  • Exports direction brick constants and impulse magnitude values for
    public use
  • Maintains module organization and visibility hierarchy
+7/-0     
level_loader.rs
Level loader brick type completion counting                           

src/level_loader.rs

  • Updates level completion counting logic to include bricks 50 and 54 as
    destructible
  • Adds special case handling for these brick types in
    CountsTowardsCompletion filter
  • Maintains backward compatibility with existing brick type handling
+5/-2     
1-balls-emissive.xcf
GIMP Emissive Texture Asset for Ball Indicator                     

assets/textures/gimp/1-balls-emissive.xcf

  • GIMP project file for emissive texture with numbered ball indicator
  • Contains text layer with "1" in Orbitron Heavy font (red color
    #ff4660)
  • Includes grid settings and layer composition for visual asset
[link]   
Bug fix
2 files
paddle_shrink.rs
Paddle shrink test fixes for lives state validation           

tests/paddle_shrink.rs

  • Updates existing paddle shrink tests to set lives_remaining = 1 before
    triggering ball loss
  • Ensures shrink animation only triggers when it's the last ball (lives
    == 1 condition)
  • Adds clarifying comments about InputLocked component behavior during
    shrink
  • Maintains backward compatibility with existing test structure
+58/-1   
respawn.rs
Paddle shrink logic fix for multi-ball scenarios                 

src/systems/respawn.rs

  • Updates apply_paddle_shrink to only trigger when ALL balls are gone
    (not just one ball lost)
  • Adds balls query to check remaining ball count before applying shrink
    animation
  • Implements per-frame flag to prevent multiple shrink animations in
    same frame
  • Adds InputLocked component alongside PaddleGrowing for consistent
    input locking
+30/-6   
Miscellaneous
2 files
merkaba.rs
Merkaba system visibility adjustment                                         

src/systems/merkaba.rs

  • Makes despawn_balls_and_merkabas_on_life_loss function public for test
    access
+1/-1     
2-balls.xcf
GIMP texture asset file                                                                   

assets/textures/gimp/2-balls.xcf

  • Binary GIMP project file with texture asset data
  • Contains layer definitions and image composition for 2-ball texture
[link]   
Configuration changes
3 files
ball_spawn_bricks.rs
Ball spawn system scheduling refinement                                   

src/systems/ball_spawn_bricks.rs

  • Adds scheduling constraint to ensure ball spawn system runs before
    respawn detection
  • Uses .before(RespawnSystems::Detect) to establish proper system
    ordering
+3/-1     
_typos.toml
Typos configuration dictionary extension                                 

_typos.toml

  • Adds consts to the typos dictionary to prevent false positives on Rust
    consts keyword
  • Maintains existing project name exceptions (rumdl, brkrs)
+2/-1     
manifest.ron
Direction Brick Material Profiles and Type Mappings           

assets/textures/manifest.ron

  • Adds 7 new brick material profiles: brick/balls_1, brick/balls_2,
    brick/balls_3, brick/up, brick/down, brick/left, brick/right
  • Maps brick type IDs 43-48 to direction arrow profiles (down, left,
    right, up, up-right, up-left)
  • Includes emissive texture paths for visual distinction of direction
    bricks
  • Adds object class mappings for brick types 37, 39, 43-48 with
    corresponding profiles
+161/-0 
Documentation
12 files
tasks.md
Direction bricks feature task specification and planning 

specs/026-direction-bricks/tasks.md

  • Comprehensive task breakdown for direction brick feature
    implementation
  • Defines 82 tasks across 10 phases (setup, foundation, 4 user stories,
    persistence, integration, documentation, acceptance)
  • Includes mandatory TDD red-phase tests, acceptance criteria, and Bevy
    0.17 compliance checkpoints
  • Provides parallel execution strategy and file inventory for feature
    development
+492/-0 
bricks.md
Brick documentation implementation status update                 

docs/bricks.md

  • Marks direction brick types (43-48, 52) as implemented with ✅️
    checkmarks
  • Updates documentation to reflect completed feature status
+7/-7     
plan.md
Comprehensive implementation plan for direction bricks feature

specs/026-direction-bricks/plan.md

  • Comprehensive implementation plan for seven new direction brick types
    (43-48, 52)
  • Detailed technical context including Rust/Bevy version, dependencies,
    and physics constraints
  • Constitution compliance verification for Bevy 0.17 mandates (Observers
    pattern, coordinate system, multi-frame persistence testing)
  • Four implementation phases with deliverables: core direction brick
    system, level design, documentation, and testing strategy
  • Risk analysis, success metrics, and integration points with existing
    game systems
+451/-0 
acceptance.md
Acceptance criteria testability checklist for direction bricks

specs/026-direction-bricks/checklists/acceptance.md

  • Testability validation checklist for all acceptance scenarios across
    four user stories
  • Verifies measurability of velocity changes, scoring values, and
    randomization behavior
  • Confirms multi-frame persistence testing requirements and
    tracing/observability scenarios
  • Identifies gaps in specification (floating-point tolerance,
    statistical thresholds, multi-frame scenarios)
  • 44 checklist items covering scenario clarity, completeness, and
    consistency
+90/-0   
requirements.md
Specification quality checklist for direction bricks         

specs/026-direction-bricks/checklists/requirements.md

  • Quality validation checklist confirming specification completeness
  • Verifies all mandatory sections completed and no [NEEDS CLARIFICATION]
    markers remain
  • Confirms requirements are testable, unambiguous, and
    technology-agnostic
  • Validates feature readiness for implementation planning phase
+37/-0   
spec.md
Direction Bricks Feature Specification with TDD Requirements

specs/026-direction-bricks/spec.md

  • Comprehensive feature specification for direction bricks (types 43-48,
    52) with instantaneous velocity impulses
  • Defines 4 user stories with acceptance scenarios covering cardinal
    directions, diagonals, randomization, and scoring
  • Establishes TDD requirements, Bevy 0.17 compliance mandates, and
    multi-frame persistence testing
  • Documents 11 functional requirements, edge cases, and success criteria
    for the feature
+210/-0 
data-model.md
Direction Bricks Data Model and System Design                       

specs/026-direction-bricks/data-model.md

  • Defines DirectionBrickEffect observer event type with payload
    structure
  • Documents velocity modification logic for each brick type (43-48, 52)
    with behavior examples
  • Specifies scoring integration (75/100/125 points) and state
    transitions
  • Includes test fixtures, validation rules, and expected outcomes table
+351/-0 
quickstart.md
Direction Bricks Development Quickstart Guide                       

specs/026-direction-bricks/quickstart.md

  • 5-phase development guide: setup, failing tests (red), observer
    implementation (green), tracing/scoring (refactor), acceptance testing
  • Provides minimal test structure and implementation code snippets for
    each phase
  • Includes troubleshooting section and file structure reference
  • Emphasizes TDD workflow with explicit commit points for red/green
    phases
+374/-0 
observer-event.md
Direction Bricks Observer Event API Contract                         

specs/026-direction-bricks/contracts/observer-event.md

  • Defines DirectionBrickEffect event contract with Rust type signature
    and constraints
  • Documents observer function signature, integration points with scoring
    and physics systems
  • Specifies error handling, performance characteristics, and tracing
    observability contract
  • Includes backward compatibility analysis and success criteria for
    contract compliance
+338/-0 
direction-bricks-coordinates.md
Direction Bricks Coordinate System Documentation                 

docs/direction-bricks-coordinates.md

  • Documents Bevy coordinate system mapping for direction bricks (X/Y/Z
    axes)
  • Provides impulse vector table for all brick types with magnitude and
    direction
  • Includes random brick formula and visual top-down layout diagram
  • Explains gameplay context and integration with physics system
+76/-0   
LICENSE-agpl-3.0.md
License Document Metadata Updates                                               

docs/LICENSE-agpl-3.0.md

  • Adds :orphan: directive to mark document as standalone
  • Inserts inclusion markers for license text embedding in other
    documentation
+4/-0     
contributing.md
Contributing Guide License Section Addition                           

docs/contributing.md

  • Adds License section with Sphinx include directive to embed AGPL-3.0
    license text
  • References markers in LICENSE-agpl-3.0.md for dynamic content
    inclusion
+9/-0     
Dependencies
1 files
.pre-commit-config.yaml
Pre-commit hook version updates                                                   

.pre-commit-config.yaml

  • Updates typos pre-commit hook from v1 to v1.43.0 for bug fixes and
    improvements
  • Updates rumdl pre-commit hook from v0.1.0 to v0.1.10 for enhanced
    functionality
+2/-2     
Assets
4 files
3-balls.xcf
Add GIMP project for 3-balls texture asset                             

assets/textures/gimp/3-balls.xcf

  • Converted empty placeholder file to complete GIMP XCF project file
  • Added text layer with "3" character using Orbitron Heavy font (128px)
  • Configured text color (#46d19c green) and layout properties
  • Included background layer and image grid configuration
[link]   
1-balls.xcf
Add GIMP project for 1-balls texture asset                             

assets/textures/gimp/1-balls.xcf

  • Converted empty placeholder file to complete GIMP XCF project file
  • Added text layer with "1" character using Orbitron Heavy font (128px)
  • Configured text color (#ff4660 red) and layout properties
  • Included background layer and image grid configuration
[link]   
3-balls-emmissve.xcf
Add GIMP project for 3-balls emissive texture variant       

assets/textures/gimp/3-balls-emmissve.xcf

  • Created new GIMP XCF project file for emissive variant of 3-balls
    texture
  • Added text layer with "3" character using Orbitron Heavy font (128px)
  • Configured text color (#46d19c green) with emissive rendering
    properties
  • Included background layer with emissive effects and image grid
    configuration
[link]   
2-balls-emissve.xcf
Add GIMP project for 2-balls emissive texture variant       

assets/textures/gimp/2-balls-emissve.xcf

  • Created new GIMP XCF project file for emissive variant of 2-balls
    texture
  • Added text layer with "2" character using Orbitron Heavy font (128px)
  • Configured text color (#46b737 green) with emissive rendering
    properties
  • Included background layer with emissive effects and image grid
    configuration
[link]   
Additional files
1 files
up-arrow-red-60907c.xcf [link]   


Open with Devin

Summary by CodeRabbit

  • New Features

    • Seven Direction Bricks (IDs 43–48, 52) that apply directional/random velocity impulses.
    • Paddle shrink now triggers only when the last ball is lost.
  • Bug Fixes

    • Safer brick despawn handling to avoid resource/borrow conflicts during collisions.
  • Assets

    • Added Level 99 and new textures/profiles for direction bricks.
  • Documentation

    • Direction-bricks docs, acceptance/checklists, and license inclusion added.
  • Tests

    • Comprehensive direction-brick and paddle-shrink tests added.
  • Chores

    • Updated pre-commit pins and typo dictionary entries.

cleder added 15 commits February 1, 2026 11:34
- Impulses: Instantaneous velocity changes (not per-frame acceleration)
- Observability: Tracing spans via tracing crate
- Event system: Observers + Trigger pattern
- Validation: Physics system owns bounds, no validation by bricks
- RNG: Generate magnitude directly in 5.0-15.0 range
…r velocity effects

- Added observer system for direction brick effects, applying velocity impulses to balls upon destruction.
- Updated specifications and plans to clarify behavior of bricks, including randomized velocity for brick 52.
- Created detailed tasks for implementation, testing, and integration of direction bricks into the existing game framework.
- Ensured compliance with Bevy 0.17 standards, including message-event separation and coordinate system correctness.
Changed direction brick effects from direct velocity modification to using
bevy_rapier3d's ExternalImpulse component for more physically realistic behavior.
- Updated `ball_spawn_bricks.rs` to ensure ball spawning occurs before respawn detection.
- Made `despawn_balls_and_merkabas_on_life_loss` public in `merkaba.rs` for broader access.
- Refined `respawn.rs` to improve system execution order and added checks for paddle shrink conditions.
- Enhanced impulse tests in `direction_bricks.rs` to reflect accurate impulse application for directional bricks.
- Introduced new tests in `paddle_shrink_multiball.rs` and `paddle_shrink_regression.rs` to validate paddle shrink behavior under various ball loss scenarios, ensuring it only triggers when the last ball is lost.
- Ensured that paddle shrink animations do not occur when multiple balls are in play.
Copilot AI review requested due to automatic review settings February 2, 2026 19:58
@cleder cleder linked an issue Feb 2, 2026 that may be closed by this pull request
7 tasks
@semanticdiff-com
Copy link

semanticdiff-com bot commented Feb 2, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  src/systems/ball_spawn_bricks.rs  81% smaller
  src/level_loader.rs  58% smaller
  src/systems/respawn.rs  35% smaller
  src/systems/merkaba.rs  25% smaller
  tests/paddle_shrink.rs  4% smaller
  .pre-commit-config.yaml  0% smaller
  _typos.toml Unsupported file format
  assets/levels/level_099.ron Unsupported file format
  assets/textures/default/1-balls-emissive.png Unsupported file format
  assets/textures/default/2-balls-emissive.png Unsupported file format
  assets/textures/default/3-balls-emissive.png Unsupported file format
  assets/textures/default/emissive-arrow-down.png Unsupported file format
  assets/textures/default/emissive-arrow-left.png Unsupported file format
  assets/textures/default/emissive-arrow-right.png Unsupported file format
  assets/textures/default/emissive-arrow-up.png Unsupported file format
  assets/textures/gimp/1-balls-emissive.xcf Unsupported file format
  assets/textures/gimp/1-balls.xcf Unsupported file format
  assets/textures/gimp/2-balls-emissve.xcf Unsupported file format
  assets/textures/gimp/2-balls.xcf Unsupported file format
  assets/textures/gimp/3-balls-emmissve.xcf Unsupported file format
  assets/textures/gimp/3-balls.xcf Unsupported file format
  assets/textures/gimp/up-arrow-red-60907c.xcf Unsupported file format
  assets/textures/manifest.ron Unsupported file format
  docs/LICENSE-agpl-3.0.md Unsupported file format
  docs/bricks.md Unsupported file format
  docs/contributing.md Unsupported file format
  docs/direction-bricks-coordinates.md Unsupported file format
  specs/001-ball-lives/plan.md Unsupported file format
  specs/001-cheat-mode-safeguards/plan.md Unsupported file format
  specs/001-complete-game/plan.md Unsupported file format
  specs/001-display-current-level/plan.md Unsupported file format
  specs/001-paddle-size-powerups/plan.md Unsupported file format
  specs/001-sphinx-docs/plan.md Unsupported file format
  specs/001-textured-visuals/plan.md Unsupported file format
  specs/003-map-format/plan.md Unsupported file format
  specs/004-pause-system/plan.md Unsupported file format
  specs/005-multi-hit-bricks/plan.md Unsupported file format
  specs/006-audio-system/plan.md Unsupported file format
  specs/007-level-metadata/plan.md Unsupported file format
  specs/008-paddle-shrink-feedback/plan.md Unsupported file format
  specs/009-add-scoring/plan.md Unsupported file format
  specs/010-refactor/CHECKPOINT.md Unsupported file format
  specs/010-refactor/plan.md Unsupported file format
  specs/011-refactor-systems/plan.md Unsupported file format
  specs/012-refactor-entity-spawning/plan.md Unsupported file format
  specs/013-post-refactor-qa/plan.md Unsupported file format
  specs/015-ball-physics-config/checklists/requirements-quality.md Unsupported file format
  specs/015-ball-physics-config/plan.md Unsupported file format
  specs/016-audio-wall-delay-fix/plan.md Unsupported file format
  specs/017-brick-material-textures/plan.md Unsupported file format
  specs/022-paddle-destroyable-brick/plan.md Unsupported file format
  specs/026-direction-bricks/checklists/acceptance.md Unsupported file format
  specs/026-direction-bricks/checklists/requirements.md Unsupported file format
  specs/026-direction-bricks/contracts/observer-event.md Unsupported file format
  specs/026-direction-bricks/data-model.md Unsupported file format
  specs/026-direction-bricks/plan.md Unsupported file format
  specs/026-direction-bricks/quickstart.md Unsupported file format
  specs/026-direction-bricks/spec.md Unsupported file format
  specs/026-direction-bricks/tasks.md Unsupported file format
  src/lib.rs Unsupported file format
  src/signals.rs  0% smaller
  src/systems/brick_effects.rs  0% smaller
  src/systems/mod.rs  0% smaller
  tests/direction_bricks.rs  0% smaller
  tests/paddle_shrink_multiball.rs  0% smaller
  tests/paddle_shrink_regression.rs  0% smaller

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cleder, your pull request is larger than the review limit of 150000 diff characters

@cleder
Copy link
Owner Author

cleder commented Feb 2, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

This PR introduces a Direction Bricks feature that applies instantaneous impulses to balls upon collision with seven new brick types (IDs 43–48, 52). The implementation includes an observer-based event system for handling velocity modifications, comprehensive specifications and documentation, accompanying test coverage, and supporting asset and configuration updates.

Changes

Cohort / File(s) Summary
Configuration & Tools
\.pre-commit-config.yaml, typos.toml, docs/LICENSE-agpl-3.0.md
Bumped pre-commit hook revs; added consts to typos extend words; added include markers in license doc.
Assets & Textures
assets/levels/level_099.ron, assets/textures/manifest.ron
Added level_099 RON and many new brick texture profiles (balls variants and directional profiles) with type-variant mappings for brick IDs 37–39, 43–48, 52.
Signals & Events
src/signals.rs
Added public DirectionBrickEffect event (ball, brick_type, position, velocity_before, impulse) and updated observer-event wording.
Brick Effect System
src/systems/brick_effects.rs, src/systems/mod.rs
New module and re-exports; constants for direction brick IDs and impulse magnitudes; apply_direction_brick_effects observer implemented to apply additive or randomized impulses to balls.
Core Integration
src/lib.rs, src/level_loader.rs
Emit DirectionBrickEffect from brick collision path; register observer; switched some despawn insertions to queue_silenced; treat BRICK_50/BRICK_54 as countable in level completion.
System Adjustments
src/systems/respawn.rs, src/systems/ball_spawn_bricks.rs, src/systems/merkaba.rs
Paddle shrink now guarded to trigger only on last-ball loss with per-frame guard and InputLocked insertion; adjusted ball spawn system scheduling; made a merkaba despawn function public.
Specs & Docs
specs/026-direction-bricks/*, docs/direction-bricks-coordinates.md, docs/bricks.md, docs/contributing.md
Large add of design/spec/acceptance/checklists/quickstart/tasks for Direction Bricks; coordinate & impulse docs; updated bricks table and contributing license include.
Small spec formatting
specs/*/plan.md, specs/*/CHECKPOINT.md, etc.
Fixed excessive leading whitespace across many spec files (formatting-only).
Tests
tests/direction_bricks.rs, tests/paddle_shrink.rs, tests/paddle_shrink_multiball.rs, tests/paddle_shrink_regression.rs
Added comprehensive direction-brick tests; updated/enabled paddle-shrink tests and added multiball/regression suites.

Sequence Diagram(s)

sequenceDiagram
    participant Ball
    participant CollisionHandler as BrickCollision
    participant Signals as DirectionBrickEffect
    participant Observer as ApplyDirectionBrickEffects
    participant Physics

    Ball->>CollisionHandler: Collision with brick (43-48,52)
    CollisionHandler->>CollisionHandler: Compute impulse (or random magnitude/angle for 52)
    CollisionHandler->>Signals: Emit DirectionBrickEffect(ball, brick_type, pos, vel_before, impulse)

    Observer->>Signals: On<DirectionBrickEffect> trigger
    alt Cardinal / Diagonal (43-48)
        Observer->>Physics: Add impulse to ExternalImpulse (X/Y)
    else Random (52)
        Observer->>Observer: Generate random magnitude & angle
        Observer->>Physics: Apply random impulse (XZ plane)
    end

    Physics->>Ball: Integrate, update LinearVelocity
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

Game-play 👾🎲, rust 🦀, enhancement, documentation 📚, visual

Poem

🐰 I hop and I tap where the direction bricks lie,
Seven new nudges send balls flying high.
Left, right, up, down — a random whirl too,
Tests and textures in place, ready for view.
Rabbit cheers — play on, let the ball bounds fly!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title "026 direction bricks" directly refers to the implementation of direction brick features (types 43–48, 52) that comprise the main objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 026-direction-bricks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Looks good to me! 👍

Reviewed everything up to b3ad6f5 in 25 seconds. Click for details.
  • Reviewed 4156 lines of code in 28 files
  • Skipped 14 files when reviewing.
  • Skipped posting 0 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.

Workflow ID: wflow_vupGzWx5LwPGWicv

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @cleder, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a significant new gameplay mechanic by introducing 'direction bricks' that dynamically alter ball velocity. The implementation leverages Bevy's observer pattern for responsive physics interactions and includes robust documentation covering design, implementation, and testing. Alongside this feature, the PR refines existing paddle behavior for multi-ball scenarios and updates development tooling, ensuring a stable and well-documented addition to the game.

Highlights

  • New Gameplay Feature: Direction Bricks: Introduced seven new brick types (43-48, 52) that apply instantaneous velocity impulses to the ball upon destruction. These include cardinal directions (Up, Down, Left, Right), diagonal directions (Up-Left, Up-Right), and a randomizer brick.
  • Observer Pattern for Impulse Application: Implemented the direction brick effects using Bevy's observer pattern (On<DirectionBrickEffect>) to ensure immediate and synchronous application of velocity impulses within the same frame as the brick destruction.
  • Comprehensive Documentation and Specification: Added extensive documentation including feature specifications, implementation plans, data models, quickstart guides, and acceptance criteria checklists for the new direction bricks feature, located under specs/026-direction-bricks/.
  • Paddle Shrink Logic Refinement: Modified the paddle shrinking behavior to only trigger when the last ball is lost, preventing premature shrinking in multi-ball scenarios and improving gameplay consistency.
  • Pre-commit Hook and Typos Configuration Updates: Updated the typos and rumdl-pre-commit hooks to their latest versions and added 'consts' to the _typos.toml file to prevent false positives.
  • New Test Level and Texture Profiles: Added a new test level (level_099.ron) specifically designed to test the direction bricks and introduced new texture profiles in manifest.ron for visual representation of these bricks.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Comment on lines 156 to 157
// Apply the impulse vector computed by collision system
external_impulse.impulse = effect.impulse;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impulse Overwrite Instead of Accumulation

The code sets external_impulse.impulse = effect.impulse;, which overwrites any existing impulse value. According to the documentation (lines 63-64), impulses from multiple direction brick effects should compound additively (except for brick 52). This implementation will only apply the last impulse if multiple triggers occur for the same ball entity in a single frame.

Recommended Solution:
Accumulate the impulse instead:

external_impulse.impulse += effect.impulse;

For brick 52 (random direction), ensure that it replaces the X and Y components as specified, but otherwise, additive accumulation should be the default.

Comment on lines 167 to 172
// This is a non-critical error that can occur if the ball despawned
debug!(
ball = ?effect.ball_entity,
brick_type = effect.brick_type,
"Ball entity not found or missing ExternalImpulse component when applying direction brick effect"
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error Logging Level May Be Insufficient

The error case for missing ball entity or ExternalImpulse component is logged at the debug level. If this error is rare but critical (e.g., due to unexpected despawning or system misordering), it may be missed in production logs.

Recommended Solution:
Consider logging at the warning or error level to ensure visibility:

warn!(
    ball = ?effect.ball_entity,
    brick_type = effect.brick_type,
    "Ball entity not found or missing ExternalImpulse component when applying direction brick effect"
);

This will help diagnose issues that could affect gameplay or system integrity.

Comment on lines 465 to 511
fn apply_paddle_shrink(
mut life_lost_events: MessageReader<LifeLostEvent>,
paddles: Query<(Entity, &Transform), (With<Paddle>, Without<PaddleGrowing>)>,
balls: Query<Entity, With<Ball>>,
respawn_schedule: Res<RespawnSchedule>,
mut commands: Commands,
mut shrink_applied_this_frame: Local<bool>,
) {
// Reset the flag at the start of each frame
*shrink_applied_this_frame = false;

for _event in life_lost_events.read() {
// Only shrink if ALL balls are gone (no more balls in play)
// This runs BEFORE the life decrement in enqueue_respawn_requests
let remaining_balls = balls.iter().count();

if remaining_balls != 0 {
// Other balls still in play, don't shrink
continue;
}

// Only apply the shrink once per frame, even if multiple LifeLostEvents trigger
if *shrink_applied_this_frame {
continue;
}
*shrink_applied_this_frame = true;

for (entity, transform) in paddles.iter() {
let shrink_duration = respawn_schedule.timer.duration();
commands.entity(entity).insert(PaddleGrowing {
timer: Timer::from_seconds(shrink_duration.as_secs_f32(), TimerMode::Once),
target_scale: Vec3::splat(0.01),
start_scale: transform.scale,
});
commands.entity(entity).insert((
PaddleGrowing {
timer: Timer::from_seconds(shrink_duration.as_secs_f32(), TimerMode::Once),
target_scale: Vec3::splat(0.01),
start_scale: transform.scale,
},
InputLocked,
));
info!(
target: "respawn",
event = "paddle_shrink_started",
?entity,
start_scale = ?transform.scale,
duration_secs = shrink_duration.as_secs_f32(),
"Paddle shrink animation triggered by ball loss"
"Paddle shrink animation triggered when last ball is lost"
);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential for Unintended Behavior with Multiple Paddles

The current implementation applies the shrink animation to all paddles without an active PaddleGrowing component when all balls are lost. While this is acceptable for a single-paddle design, it could lead to unintended behavior if the game is extended to support multiple paddles. Consider updating the logic to target only the relevant paddle(s) associated with the lost ball, or clearly enforce single-paddle constraints in the system to prevent future issues.

Recommended Solution:

  • If multi-paddle support is planned, associate each ball with its controlling paddle and only apply the shrink to that paddle.
  • Otherwise, add a runtime assertion or comment to ensure only one paddle exists.

Comment on lines +304 to +312
use rand::Rng;

// Test multiple samples to verify RNG distribution
let mut rng = rand::rng();
for _ in 0..100 {
// Simulate the RNG logic from lib.rs brick 52 case
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
let impulse = Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin());

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-standard RNG API usage in random impulse tests

The random impulse tests (T026, T027) use rand::rng() and rng.random_range, which are not part of the standard rand crate API. This will likely cause compilation errors or incorrect behavior. The standard approach is to use rand::thread_rng() and rng.gen_range(a..b):

let mut rng = rand::thread_rng();
let magnitude = rng.gen_range(5.0..15.0);
let angle: f32 = rng.gen_range(0.0..std::f32::consts::TAU);

Recommended solution:
Replace all instances of rand::rng() and random_range with the standard rand::thread_rng() and gen_range methods to ensure compatibility and correct random value generation.

Comment on lines +56 to +68
fn apply_impulse_to_ball(app: &mut App, ball: Entity, impulse: Vec3) -> Vec3 {
if let Some(mut external_impulse) = app.world_mut().get_mut::<ExternalImpulse>(ball) {
external_impulse.impulse = impulse;
}

// Return the current velocity for assertion purposes
// (In real physics, this would be updated by the physics system)
if let Some(velocity) = app.world().get::<Velocity>(ball) {
velocity.linvel
} else {
Vec3::ZERO
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impulse overwriting in helper function may not match physics system behavior

The apply_impulse_to_ball function overwrites the ExternalImpulse.impulse value each time it is called. In many physics systems, impulses are accumulated rather than replaced. This could lead to misleading test results, especially in tests that expect composability or persistence of impulses.

Recommended solution:
If the production system accumulates impulses, modify the helper to add to the existing impulse:

external_impulse.impulse += impulse;

Otherwise, clarify in the test documentation that impulses are expected to be overwritten, and ensure this matches the actual system behavior.

Comment on lines +124 to +125
advance_time(&mut app, 0.016);
app.update();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test advances time and calls app.update() only once after triggering a life loss. If the system under test requires multiple update cycles to process all events (e.g., respawn logic, component changes), this may result in flaky or incomplete test coverage. Recommendation: Run multiple update cycles after advancing time to ensure all systems have processed their events:

advance_time(&mut app, 0.016);
for _ in 0..2 { app.update(); }

This will make the tests more robust against changes in system scheduling.

Comment on lines +135 to +139
let mut lives = app
.world_mut()
.resource_mut::<brkrs::systems::respawn::LivesState>();
lives.lives_remaining = 1;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly mutating the lives_remaining field in the resource may be brittle if the underlying system changes how lives are managed (e.g., via events or deferred updates). Recommendation: Abstract lives manipulation into a helper function or use the game's public API for changing lives, if available. This will improve maintainability and reduce coupling to internal implementation details.

@codacy-production
Copy link

codacy-production bot commented Feb 2, 2026

Codacy's Analysis Summary

2 new issues (≤ 0 issue)
0 new security issue
50 complexity
15 duplications

Review Pull Request in Codacy →

AI Reviewer available: add the codacy-review label to get contextual insights without leaving GitHub.

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 2, 2026

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Misleading variable name: The variable ball_velocity is computed from transforms.get(...) and always set to
Vec3::ZERO, making the name misleading relative to its actual value and source.

Referred Code
let ball_velocity = transforms
    .get(triggering_ball)
    .ok()
    .map(|_t| Vec3::ZERO)
    .unwrap_or(Vec3::ZERO);

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Audit logging unclear: The PR adds tracing logs for gameplay events but does not demonstrate audit-style logging
fields (e.g., user identity, outcome) for any potentially critical actions, requiring
human verification of whether audit trails are applicable in this project context.

Referred Code
// Create a tracing span for this direction brick effect
let span = debug_span!(
    "apply_direction_brick_effect",
    brick_type = effect.brick_type,
    brick_position = ?effect.brick_position,
    velocity_before = ?effect.velocity_before,
    impulse = ?effect.impulse,
);
let _guard = span.enter();

match query.get_mut(effect.ball_entity) {
    Ok(mut external_impulse) => {
        // Apply the impulse vector computed by collision system
        external_impulse.impulse = effect.impulse;

        debug!(
            ball_entity = ?effect.ball_entity,
            impulse_applied = ?effect.impulse,
            "Direction brick effect applied to ball"
        );
    }


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Logging sensitivity review: The new tracing logs include entity identifiers and world positions/velocities, which
appear non-sensitive, but a human review is needed to confirm the project has no policy
treating these runtime identifiers/telemetry as sensitive.

Referred Code
// Create a tracing span for this direction brick effect
let span = debug_span!(
    "apply_direction_brick_effect",
    brick_type = effect.brick_type,
    brick_position = ?effect.brick_position,
    velocity_before = ?effect.velocity_before,
    impulse = ?effect.impulse,
);
let _guard = span.enter();

match query.get_mut(effect.ball_entity) {
    Ok(mut external_impulse) => {
        // Apply the impulse vector computed by collision system
        external_impulse.impulse = effect.impulse;

        debug!(
            ball_entity = ?effect.ball_entity,
            impulse_applied = ?effect.impulse,
            "Direction brick effect applied to ball"
        );
    }


 ... (clipped 9 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@dosubot dosubot bot added the Levels 🆙 Issue requests new level(s) or PR implements level, or other level related work label Feb 2, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements direction bricks (types 43-48, 52) that apply instantaneous impulses to the ball when destroyed, adding dynamic velocity control for level design. The implementation uses Bevy 0.17's observer pattern for immediate physics effects, scoring integration, and comprehensive test coverage including multi-frame persistence validation.

Changes:

  • Adds 7 new brick types with directional impulse mechanics (cardinal, diagonal, and random directions)
  • Implements observer-based system for immediate velocity modifications via ExternalImpulse component
  • Includes comprehensive test suites validating impulse application, multi-ball scenarios, and paddle shrink behavior refinements

Reviewed changes

Copilot reviewed 29 out of 66 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/direction_bricks.rs New test suite covering all 7 direction brick types with cardinal, diagonal, and random impulse validation
tests/paddle_shrink_*.rs New regression tests ensuring paddle shrink only triggers when last ball is lost
src/systems/brick_effects.rs New observer system applying direction brick impulses to ball ExternalImpulse component
src/systems/respawn.rs Updated paddle shrink logic to check remaining balls before triggering animation
src/signals.rs Added DirectionBrickEffect event type for observer pattern
src/lib.rs Integrated direction brick collision detection and impulse triggering
specs/026-direction-bricks/* Complete feature specification with acceptance criteria and implementation plan
docs/direction-bricks-coordinates.md Documentation of coordinate system and impulse mappings
assets/levels/level_099.ron Test level demonstrating all 7 direction brick types
Comments suppressed due to low confidence (2)

tests/direction_bricks.rs:1

  • The random direction brick implementation in the test and production code applies impulses incorrectly to the Y-axis instead of the Z-axis. According to the coordinate system documentation in docs/direction-bricks-coordinates.md and specs/026-direction-bricks/plan.md, the XZ plane is horizontal (gameplay surface) and Y is vertical. The random brick should modify X and Z components (horizontal plane), not X and Y. The correct formula should be Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin()) to apply random direction in the horizontal XZ plane while preserving vertical Y component.
//! Tests for direction brick effects (bricks 43-48, 52)

src/systems/brick_effects.rs:1

  • The coordinate system documentation contradicts the actual implementation and test code. Lines 43-44 state X is for left/right and Z is for forward/backward, but the code in src/lib.rs lines 885-890 applies impulses to X for down/up (lines 885, 888) and Z for left/right (lines 886-887). This mismatch between documentation and implementation will cause confusion. The documentation should be updated to match the actual brick mappings: X-axis for down/up movement and Z-axis for left/right movement.
use crate::signals::DirectionBrickEffect;

/// - Positive Y: Up (away from gravity)
/// - Negative Y: Gravity pull (down)
///
/// **Z-Axis Invariant**: No direction brick modifies Z-component of velocity.
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 41 states 'No direction brick modifies Z-component of velocity' but this directly contradicts the implementation in src/lib.rs lines 886-887 and 889-890, which modify the Z component for bricks 44, 45, 47, and 48. The comment should either be removed or corrected to state that the Y-component is never modified (as Y is vertical and unused for horizontal gameplay).

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +82
let ball = spawn_ball(
&mut app,
Vec3::new(-3.0, 2.0, 0.0),
Vec3::new(-3.0, 2.0, 0.0),
);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test T006 (brick 43 down impulse) spawns the ball with initial velocity Vec3::new(-3.0, 2.0, 0.0) but brick 43 applies +5.0 X impulse (line 885 in src/lib.rs). The test should verify the X component changes from -3.0 to 2.0, not verify Y component changes. The current assertions on lines 87-91 check the wrong axis - they should validate X-axis changes, not Y-axis.

Copilot uses AI. Check for mistakes.
src/lib.rs Outdated
Comment on lines 877 to 880
let ball_velocity = transforms
.get(triggering_ball)
.ok()
.map(|_t| Vec3::ZERO)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The velocity_before field is hardcoded to Vec3::ZERO instead of reading the actual ball velocity from the Velocity component. The transforms query is used but the transform is discarded (line 880 |_t|), and the result is always Vec3::ZERO. This makes the velocity_before field in DirectionBrickEffect useless for tracing and debugging. Query the ball's Velocity component instead: velocities.get(triggering_ball).map(|v| v.linvel).unwrap_or(Vec3::ZERO).

Suggested change
let ball_velocity = transforms
.get(triggering_ball)
.ok()
.map(|_t| Vec3::ZERO)
let ball_velocity = velocities
.get(triggering_ball)
.map(|v| v.linvel)

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +312
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
let impulse = Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin());
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test T026 validates the random brick RNG logic but uses the wrong axes - it should match the production implementation in src/lib.rs line 898. However, both this test and the production code have the same axis error (Y instead of Z for the sine component). When the production bug is fixed to use Z-axis, this test will also need updating to Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin()) to maintain consistency.

Copilot uses AI. Check for mistakes.
for _event in life_lost_events.read() {
// Only shrink if ALL balls are gone (no more balls in play)
// This runs BEFORE the life decrement in enqueue_respawn_requests
let remaining_balls = balls.iter().count();
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The balls.iter().count() on line 479 iterates through all ball entities every time a LifeLostEvent occurs. For games with multiple balls in play, this could be inefficient if many LifeLostEvents fire in the same frame. Consider using balls.iter().len() if the query supports it, or cache the count. However, since this only runs when a ball is lost (infrequent event), the performance impact is likely negligible.

Suggested change
let remaining_balls = balls.iter().count();
let remaining_balls = balls.iter().len();

Copilot uses AI. Check for mistakes.
Comment on lines +956 to +960
commands.entity(entity).queue_silenced(
|mut entity_commands: bevy::ecs::world::EntityWorldMut| {
entity_commands.insert(MarkedForDespawn);
},
);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of queue_silenced with a closure to insert MarkedForDespawn (lines 956-960 and 1210-1214) is more verbose than the direct commands.entity(entity).insert(MarkedForDespawn) pattern. Unless there's a specific reason to silence the command or defer it beyond normal command application, consider reverting to the simpler direct insertion pattern for consistency with the rest of the codebase.

Suggested change
commands.entity(entity).queue_silenced(
|mut entity_commands: bevy::ecs::world::EntityWorldMut| {
entity_commands.insert(MarkedForDespawn);
},
);
commands.entity(entity).insert(MarkedForDespawn);

Copilot uses AI. Check for mistakes.
),
(
object_class: Brick,
type_id: 39,
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 1386 assigns type_id: 39 to the brick/balls_3 profile, but this duplicates the type_id from lines 1379-1380 where brick/balls_2 also uses type_id: 39. Each brick type should have a unique type_id. Based on the pattern (brick/balls_1 = 37, brick/balls_2 = 39), this should likely be type_id: 40 or another unique value.

Suggested change
type_id: 39,
type_id: 40,

Copilot uses AI. Check for mistakes.
Comment on lines 105 to 107
- "Randomized velocity" for brick 52: Generate random 2D direction in XZ plane (or full 3D if Z movement is allowed), magnitude 5.0-15.0 units/sec
- Direction angle generated in **radians** (0.0..TAU = 0.0..2π), NOT degrees (0.0..360.0)
- Implementation: `angle = rng.gen_range(0.0..std::f32::consts::TAU)` (direct radians); convert to cos/sin for X/Y components
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 108 states 'convert to cos/sin for X/Y components' but this conflicts with line 106 which correctly identifies the XZ plane as the horizontal gameplay surface. The implementation should convert to X/Z components (X = magnitude * cos(angle), Z = magnitude * sin(angle)), not X/Y. This documentation error matches the implementation bug in src/lib.rs line 898.

Copilot uses AI. Check for mistakes.
@qodo-code-review
Copy link

qodo-code-review bot commented Feb 2, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix random impulse generation and plane
Suggestion Impact:The commit updated brick 52's impulse vector to be in the XZ plane (setting Y to 0 and using Z for sin), but it did not change the rand API usage to thread_rng/gen_range as suggested.

code diff:

                             52 => {
-                                // Random direction brick: RNG-based impulse in XY plane
+                                // Random direction brick: RNG-based impulse in XZ plane (horizontal)
                                 // Magnitude: 5.0-15.0 units/sec, Direction: 0-2π radians
                                 use rand::Rng;
                                 let mut rng = rand::rng();
                                 let magnitude = rng.random_range(5.0..15.0);
                                 let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
-                                Vec3::new(magnitude * angle.cos(), magnitude * angle.sin(), 0.0)
+                                Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin())
                             }

Update the random impulse generation for brick 52 to use the correct rand crate
functions (thread_rng and gen_range) and apply the impulse in the XZ plane
instead of the XY plane.

src/lib.rs [891-899]

 52 => {
-    // Random direction brick: RNG-based impulse in XY plane
+    // Random direction brick: RNG-based impulse in XZ plane
     // Magnitude: 5.0-15.0 units/sec, Direction: 0-2π radians
     use rand::Rng;
-    let mut rng = rand::rng();
-    let magnitude = rng.random_range(5.0..15.0);
-    let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
-    Vec3::new(magnitude * angle.cos(), magnitude * angle.sin(), 0.0)
+    let mut rng = rand::thread_rng();
+    let magnitude = rng.gen_range(5.0..15.0);
+    let angle: f32 = rng.gen_range(0.0..std::f32::consts::TAU);
+    Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin())
 }

[Suggestion processed]

Suggestion importance[1-10]: 10

__

Why: This suggestion correctly identifies and fixes two critical bugs: a compilation error due to incorrect rand crate API usage, and a logic error where the impulse for brick 52 is applied to the wrong plane (XY instead of XZ), contradicting the game's physics model and its own tests.

High
Correct random number generation in test

Update the test for brick 52 to use the correct rand crate functions,
thread_rng() and gen_range(), to fix a compilation error and align with the
corrected implementation.

tests/direction_bricks.rs [302-329]

 #[test]
 fn test_brick_52_random_magnitude_range() {
     use rand::Rng;
 
     // Test multiple samples to verify RNG distribution
-    let mut rng = rand::rng();
+    let mut rng = rand::thread_rng();
     for _ in 0..100 {
         // Simulate the RNG logic from lib.rs brick 52 case
-        let magnitude = rng.random_range(5.0..15.0);
-        let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
+        let magnitude = rng.gen_range(5.0..15.0);
+        let angle: f32 = rng.gen_range(0.0..std::f32::consts::TAU);
         let impulse = Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin());
         let magnitude_computed = (impulse.x * impulse.x + impulse.z * impulse.z).sqrt();
 
         assert!(
             magnitude_computed >= 5.0,
             "Magnitude should be >= 5.0, got {:.2}",
             magnitude_computed
         );
         assert!(
             magnitude_computed < 15.0,
             "Magnitude should be < 15.0, got {:.2}",
             magnitude_computed
         );
         assert_eq!(
             impulse.y, 0.0,
             "Y component should be zero for random brick (XZ plane only)"
         );
     }
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 9

__

Why: This suggestion correctly identifies that the test code uses an incorrect and non-compiling API for the rand crate. Fixing this is essential for the test to compile and accurately validate the random impulse generation logic.

High
Accumulate impulses additively
Suggestion Impact:Updated the impulse application to use `external_impulse.impulse += effect.impulse;` and adjusted the comment to reflect accumulation (plus minor logging/comment tweaks nearby).

code diff:

@@ -153,8 +153,8 @@
 
     match query.get_mut(effect.ball_entity) {
         Ok(mut external_impulse) => {
-            // Apply the impulse vector computed by collision system
-            external_impulse.impulse = effect.impulse;
+            // Accumulate impulses instead of overwriting to preserve collision response
+            external_impulse.impulse += effect.impulse;
 

Change the impulse application from an assignment (=) to an addition-assignment
(+=) to correctly compound the effects of multiple direction bricks hit in the
same frame.

src/systems/brick_effects.rs [157]

-external_impulse.impulse = effect.impulse;
+external_impulse.impulse += effect.impulse;

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: This is a valid and important correction. The current implementation overwrites the impulse, meaning if multiple direction bricks are hit in one frame, only the last one takes effect. Accumulating impulses with += correctly compounds their effects as described in the documentation.

Medium
  • Update

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is a very comprehensive pull request that adds the direction bricks feature, including implementation, extensive tests, and detailed documentation. The fix for the paddle shrink logic in multi-ball scenarios is also well-implemented and tested.

I've found a few inconsistencies and a bug that need to be addressed:

  • There's a duplicated type_id in assets/textures/manifest.ron.
  • The random direction brick (type 52) impulse is calculated in the wrong plane.
  • There are several documentation inconsistencies across src/signals.rs and src/systems/brick_effects.rs regarding the direction brick mappings and effects, which could be misleading.

Once these issues are resolved, this will be an excellent addition to the project.

Comment on lines +1384 to +1390
(
object_class: Brick,
type_id: 39,
profile_id: "brick/balls_3",
emissive_color: None,
animation: None,
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There appears to be a duplicated type_id: 39, which is mapped to both brick/balls_2 and brick/balls_3. This is likely a copy-paste error. Please assign the correct type_id for the brick/balls_3 profile.

Comment on lines 891 to 899
52 => {
// Random direction brick: RNG-based impulse in XY plane
// Magnitude: 5.0-15.0 units/sec, Direction: 0-2π radians
use rand::Rng;
let mut rng = rand::rng();
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
Vec3::new(magnitude * angle.cos(), magnitude * angle.sin(), 0.0)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The impulse for the random direction brick (type 52) is calculated in the XY plane. Based on the game's coordinate system and documentation, it should be in the XZ plane to affect horizontal movement. The Y component should be 0.

Suggested change
52 => {
// Random direction brick: RNG-based impulse in XY plane
// Magnitude: 5.0-15.0 units/sec, Direction: 0-2π radians
use rand::Rng;
let mut rng = rand::rng();
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
Vec3::new(magnitude * angle.cos(), magnitude * angle.sin(), 0.0)
}
52 => {
// Random direction brick: RNG-based impulse in XZ plane
// Magnitude: 5.0-15.0 units/sec, Direction: 0-2π radians
use rand::Rng;
let mut rng = rand::rng();
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin())
}

src/signals.rs Outdated
Comment on lines 185 to 193
/// # Per-Brick Behavior
///
/// - **Brick 43 (Left)**: Apply impulse `(-5.0, 0, 0)` units/sec
/// - **Brick 44 (Right)**: Apply impulse `(+5.0, 0, 0)` units/sec
/// - **Brick 45 (Up)**: Apply impulse `(0, +5.0, 0)` units/sec
/// - **Brick 46 (Down)**: Apply impulse `(0, -5.0, 0)` units/sec
/// - **Brick 47 (Forward)**: Apply impulse `(0, 0, +5.0)` units/sec
/// - **Brick 48 (Backward)**: Apply impulse `(0, 0, -5.0)` units/sec
/// - **Brick 52 (Random)**: Apply impulse with magnitude ∈ [5.0, 15.0], direction ∈ [0, 2π) in XY plane only (Z=0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The documentation for the per-brick behavior is inconsistent with the implementation. For example, brick 43 is documented here as 'Left' with a -X impulse, but it's implemented as 'Down' with a +X impulse. Please update the documentation to match the implementation to avoid confusion. This applies to all brick types listed here.

Suggested change
/// # Per-Brick Behavior
///
/// - **Brick 43 (Left)**: Apply impulse `(-5.0, 0, 0)` units/sec
/// - **Brick 44 (Right)**: Apply impulse `(+5.0, 0, 0)` units/sec
/// - **Brick 45 (Up)**: Apply impulse `(0, +5.0, 0)` units/sec
/// - **Brick 46 (Down)**: Apply impulse `(0, -5.0, 0)` units/sec
/// - **Brick 47 (Forward)**: Apply impulse `(0, 0, +5.0)` units/sec
/// - **Brick 48 (Backward)**: Apply impulse `(0, 0, -5.0)` units/sec
/// - **Brick 52 (Random)**: Apply impulse with magnitude ∈ [5.0, 15.0], direction ∈ [0, 2π) in XY plane only (Z=0)
/// # Per-Brick Behavior
///
/// - **Brick 43 (Down)**: Apply impulse `(+5.0, 0, 0)` units/sec
/// - **Brick 44 (Left)**: Apply impulse `(0, 0, +5.0)` units/sec
/// - **Brick 45 (Right)**: Apply impulse `(0, 0, -5.0)` units/sec
/// - **Brick 46 (Up)**: Apply impulse `(-5.0, 0, 0)` units/sec
/// - **Brick 47 (Up-Right)**: Apply impulse `(-5.0, 0, -5.0)` units/sec
/// - **Brick 48 (Up-Left)**: Apply impulse `(-5.0, 0, +5.0)` units/sec
/// - **Brick 52 (Random)**: Apply impulse with magnitude ∈ [5.0, 15.0], direction ∈ [0, 2π) in XZ plane only (Y=0)

Comment on lines +44 to +56
/// **Cardinal Directions (Bricks 43-48)**: 5.0 units/sec additive impulse per brick type
/// - Brick 43 (Left): `velocity.x -= 5.0`
/// - Brick 44 (Right): `velocity.x += 5.0`
/// - Brick 45 (Up): `velocity.y += 5.0`
/// - Brick 46 (Down): `velocity.y -= 5.0`
/// - Brick 47 (Forward): `velocity.z += 5.0`
/// - Brick 48 (Backward): `velocity.z -= 5.0`
///
/// **Random Direction (Brick 52)**: Magnitude 5.0-15.0 units/sec, direction 0.0..2π radians
/// - Replaces both X and Y velocities (NOT additive)
/// - Preserves Z velocity
/// - Uses seeded RNG for deterministic behavior in tests
///
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The documentation for impulse magnitudes is inconsistent with the implementation and other documentation. For example, brick 43 is for 'Down' movement, not 'Left'. Also, for brick 52, the effect is an additive impulse, not a replacement of velocity. Please update the documentation to be accurate.

Comment on lines 83 to 96
/// Brick type ID for Left direction brick.
pub const BRICK_TYPE_DIRECTION_LEFT: u8 = 43;
/// Brick type ID for Right direction brick.
pub const BRICK_TYPE_DIRECTION_RIGHT: u8 = 44;
/// Brick type ID for Up direction brick.
pub const BRICK_TYPE_DIRECTION_UP: u8 = 45;
/// Brick type ID for Down direction brick.
pub const BRICK_TYPE_DIRECTION_DOWN: u8 = 46;
/// Brick type ID for Forward direction brick.
pub const BRICK_TYPE_DIRECTION_FORWARD: u8 = 47;
/// Brick type ID for Backward direction brick.
pub const BRICK_TYPE_DIRECTION_BACKWARD: u8 = 48;
/// Brick type ID for Random direction brick.
pub const BRICK_TYPE_DIRECTION_RANDOM: u8 = 52;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These constants are inconsistent with the implementation. For example, BRICK_TYPE_DIRECTION_LEFT is defined as 43, but brick 43 is for 'Down'. Please update the constant names to match their effects.

Suggested change
/// Brick type ID for Left direction brick.
pub const BRICK_TYPE_DIRECTION_LEFT: u8 = 43;
/// Brick type ID for Right direction brick.
pub const BRICK_TYPE_DIRECTION_RIGHT: u8 = 44;
/// Brick type ID for Up direction brick.
pub const BRICK_TYPE_DIRECTION_UP: u8 = 45;
/// Brick type ID for Down direction brick.
pub const BRICK_TYPE_DIRECTION_DOWN: u8 = 46;
/// Brick type ID for Forward direction brick.
pub const BRICK_TYPE_DIRECTION_FORWARD: u8 = 47;
/// Brick type ID for Backward direction brick.
pub const BRICK_TYPE_DIRECTION_BACKWARD: u8 = 48;
/// Brick type ID for Random direction brick.
pub const BRICK_TYPE_DIRECTION_RANDOM: u8 = 52;
/// Brick type ID for Down direction brick.
pub const BRICK_TYPE_DIRECTION_DOWN: u8 = 43;
/// Brick type ID for Left direction brick.
pub const BRICK_TYPE_DIRECTION_LEFT: u8 = 44;
/// Brick type ID for Right direction brick.
pub const BRICK_TYPE_DIRECTION_RIGHT: u8 = 45;
/// Brick type ID for Up direction brick.
pub const BRICK_TYPE_DIRECTION_UP: u8 = 46;
/// Brick type ID for Up-Right direction brick.
pub const BRICK_TYPE_DIRECTION_UP_RIGHT: u8 = 47;
/// Brick type ID for Up-Left direction brick.
pub const BRICK_TYPE_DIRECTION_UP_LEFT: u8 = 48;
/// Brick type ID for Random direction brick.
pub const BRICK_TYPE_DIRECTION_RANDOM: u8 = 52;

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 5 potential issues.

Open in Devin Review

Comment on lines 476 to 484
for _event in life_lost_events.read() {
// Only shrink if ALL balls are gone (no more balls in play)
// This runs BEFORE the life decrement in enqueue_respawn_requests
let remaining_balls = balls.iter().count();

if remaining_balls != 0 {
// Other balls still in play, don't shrink
continue;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Paddle shrink never triggers because ball entity is still counted in same frame it’s despawned

apply_paddle_shrink checks balls.iter().count() to decide whether “ALL balls are gone”, but it runs in the same Update schedule immediately after detect_ball_loss. The lost ball is despawned via commands.entity(ball_entity).despawn() which is deferred until the end of the schedule, so the ball still exists during apply_paddle_shrink.

Actual: on the last ball loss, remaining_balls is still 1, so the system continues and never applies PaddleGrowing.

Expected: shrink should trigger when the last ball is lost (as the PR description/tests intend).

Click to expand

Relevant code:

  • detect_ball_loss defers despawn: commands.entity(ball_entity).despawn(); src/systems/respawn.rs:408-414
  • apply_paddle_shrink counts balls before that despawn is applied: let remaining_balls = balls.iter().count(); if remaining_balls != 0 { continue; } src/systems/respawn.rs:476-484

Impact:

  • Paddle shrink feedback won’t happen on actual game-over / last-ball loss.
  • The new/updated tests around shrink-on-last-ball should fail unless something else masks this ordering.

Recommendation: When processing each LifeLostEvent, treat event.ball as already gone (e.g., remaining = balls.iter().count() - balls.contains(event.ball) as usize) or mark the ball for despawn with a component and query Without<MarkedForDespawn>, or run shrink after command application (different schedule/ordering).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@llamapreview llamapreview bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review by LlamaPReview

🎯 TL;DR & Recommendation

Recommendation: Request Changes

This PR introduces direction brick impulses but has critical issues: a race condition in ball spawn scheduling and potential coordinate mapping errors that could break gameplay.

📄 Documentation Diagram

This diagram documents the new direction brick impulse workflow using the observer pattern.

sequenceDiagram
    participant B as Ball
    participant C as CollisionSystem
    participant O as ObserverSystem
    participant E as ExternalImpulse
    participant P as Physics

    B->>C: Collision with direction brick
    C->>O: Queue DirectionBrickEffect event
    note over C,O: PR #35;171 implements observer pattern for immediate impulses
    O->>E: Apply impulse vector
    E->>P: Update ball velocity
Loading

🌟 Strengths

  • Comprehensive test suite covering direction bricks and paddle shrink edge cases.
  • Well-documented specification with clear acceptance criteria.
Priority File Category Impact Summary Anchors
P1 src/systems/respawn.rs Bug Fixes premature paddle shrink in multi-ball scenarios method:apply_paddle_shrink, path:tests/paddle_shrink.rs
P1 src/systems/ball_spawn_bricks.rs Architecture Prevents race condition between ball spawn and respawn detection
P1 src/lib.rs Architecture Ensures direction brick impulses match documented coordinate system method:mark_brick_on_ball_collision, path:docs/direction-bricks-coordinates.md
P2 assets/textures/manifest.ron Bug Duplicate type_id causes texture rendering mismatch
P2 specs/026-direction-bricks/tasks.md Maintainability Bundling extensive spec docs increases review overhead path:specs/026-direction-bricks/

🔍 Notable Themes

  • Event-driven observer pattern for direction bricks enhances modularity.
  • Scheduling constraints are critical for deterministic game logic.

📈 Risk Diagram

This diagram illustrates the scheduling race condition risk in ball spawn and respawn logic.

sequenceDiagram
    participant BS as BallSpawnSystem
    participant RD as RespawnDetectSystem
    participant PS as PaddleShrinkSystem

    BS->>BS: Spawn new balls
    RD->>RD: Check for ball loss
    alt No balls and BS not run
        RD->>PS: Trigger paddle shrink
        note over BS,RD: R2(P1): Race condition if .before constraint missing
    end
Loading
⚠️ **Unanchored Suggestions (Manual Review Recommended)**

The following suggestions could not be precisely anchored to a specific line in the diff. This can happen if the code is outside the changed lines, has been significantly refactored, or if the suggestion is a general observation. Please review them carefully in the context of the full file.


📁 File: src/systems/ball_spawn_bricks.rs

Adding the .before(RespawnSystems::Detect) scheduling constraint to the ball spawn system is a critical architectural fix. Without this ordering, a race condition could occur where a LifeLostEvent is processed by the apply_paddle_shrink system (part of RespawnSystems::Detect) before new balls from a spawn power-up brick are added to the world. This would cause the system's balls query to be empty, incorrectly triggering a paddle shrink even though new balls are about to spawn. The change creates a deterministic order: spawn new balls first, then detect if any balls remain for the respawn/shrink logic. This directly supports the correctness of the P1 paddle shrink fix above.

Suggestion:

// In the system registration within the plugin
app.add_systems(
    Update,
    (
        ball_spawn_bricks_system,
        apply_paddle_shrink,
    )
        .chain()
        .run_if(in_state(GameState::Playing)),
)
// OR ensure explicit ordering if using system sets
.add_systems(
    Update,
    ball_spawn_bricks_system.before(RespawnSystems::Detect)
)

Related Code:

// Snippet from PR description: Adds scheduling constraint
  .before(RespawnSystems::Detect)

📁 File: src/lib.rs

The implementation introduces a new event-driven architecture (DirectionBrickEffect event & observer) for direction bricks, which is a sound Bevy 0.17 pattern. However, the architectural risk lies in the potential for the impulse logic within mark_brick_on_ball_collision to diverge from the documented coordinate system. The evidence anchor path:docs/direction-bricks-coordinates.md provides the canonical mapping (e.g., Brick 43 = Down = Vec3::new(+5.0, 0, 0)). A mismatch between this documentation and the calculation in lib.rs would cause subtle, hard-to-debug gameplay bugs where balls move in unexpected directions. While tests exist, the P1 priority reflects the high impact of an incorrect coordinate mapping on core gameplay.

Suggestion:

// In lib.rs or a shared constants module
pub const DIRECTION_BRICK_IMPULSES: [(u8, Vec3); 7] = [
    (43, Vec3::new( 5.0, 0.0,  0.0)), // Down
    (44, Vec3::new( 0.0, 0.0,  5.0)), // Left
    // ... etc
];

Related Code:

// Snippet from PR description: Logic added to mark_brick_on_ball_collision
  // Implements impulse vector calculation for all 7 brick types (43-48, 52) with RNG for randomizer

📁 File: assets/textures/manifest.ron

The texture manifest contains a duplicate entry for type_id: 39 (mapped to both "brick/balls_2" and "brick/balls_3"). In a typical RON deserialization into a HashMap or similar structure, the later entry would overwrite the earlier one, leading to a loss of one intended mapping. This will cause one of the ball-indicator brick types to render with the wrong texture. This is a deterministic bug in asset configuration.

Suggestion:

( object_class: Brick, type_id: 38, profile_id: "brick/balls_1", ... ),
        ( object_class: Brick, type_id: 39, profile_id: "brick/balls_2", ... ),
        ( object_class: Brick, type_id: 40, profile_id: "brick/balls_3", ... ),

Related Code:

// From the PR diff: Adds mapping for brick type 39 twice with different profiles
        (
            object_class: Brick,
            type_id: 39,
            profile_id: "brick/balls_2",
            emissive_color: None,
            animation: None,
        ),
        (
            object_class: Brick,
            type_id: 39,
            profile_id: "brick/balls_3",
            emissive_color: None,
            animation: None,
        ),

📁 File: specs/026-direction-bricks/tasks.md

The PR includes an extensive amount of specification and planning documentation (hundreds of lines across multiple files in specs/026-direction-bricks/). While thorough documentation is valuable, bundling it with the code changes in a single PR creates significant review overhead and dilutes focus on the critical code changes (systems, events, tests). In the future, consider a more streamlined approach: 1) Merge specification docs in a separate, early PR for agreement, or 2) Place finalized specs in a docs/ directory and reference them, rather than including the full evolution and planning history in the feature PR. This improves review efficiency and maintains a cleaner commit history.

Related Code:

// This is a conceptual finding about the PR's scope. No single code snippet.


💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.

Comment on lines 465 to 469
fn apply_paddle_shrink(
mut life_lost_events: MessageReader<LifeLostEvent>,
paddles: Query<(Entity, &Transform), (With<Paddle>, Without<PaddleGrowing>)>,
balls: Query<Entity, With<Ball>>,
respawn_schedule: Res<RespawnSchedule>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 | Confidence: High

The modified apply_paddle_shrink system now correctly checks for the total absence of balls before triggering the shrink animation. This fixes a critical bug where in multi-ball scenarios (e.g., after spawning power-ups), the paddle would shrink prematurely upon losing a single ball, leading to an inconsistent and overly difficult game state. The evidence anchor path:tests/paddle_shrink.rs confirms the fix's intent, as the corresponding tests have been updated to only trigger shrink when lives_remaining = 1 (implying no balls are left in play). This is a breaking change to the system's behavior, but it aligns with the expected game logic and corrects a significant gameplay flaw.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/bricks.md (1)

143-145: ⚠️ Potential issue | 🟡 Minor

Magnet brick indices are inconsistent with the Special Bricks table.

The Magnet System section states:

"When an enabled magnet (51) is destroyed, a random disabled magnet (52) becomes enabled"

However, the Special Bricks table (lines 109-110) defines:

  • 55 = Magnet (Enabled)
  • 56 = Magnet (Disabled)

Brick 51 is actually Hourglass/Slow and brick 52 is the Phone (random direction) brick per the same table.

📝 Proposed fix
 ### Magnet System
 
 - Only one magnet can be active at a time
-- When an enabled magnet (51) is destroyed, a random disabled magnet (52) becomes enabled
+- When an enabled magnet (55) is destroyed, a random disabled magnet (56) becomes enabled
 - The magnet creates a force field that attracts the ball
🤖 Fix all issues with AI agents
In `@assets/textures/manifest.ron`:
- Around line 1377-1388: The manifest contains two entries with object_class:
Brick and the same type_id: 39 (profile_id "brick/balls_2" and "brick/balls_3"),
which will cause one mapping to shadow the other; fix by assigning a unique
type_id to one of the entries (or merge the two profiles into a single
profile_id if they should be the same), ensuring each Brick type_id is unique
and updating any references that rely on the replaced profile_id/type_id.
- Around line 1433-1444: Variants with object_class Brick and type_id 47 and 48
reference profile_id "brick/up-right" and "brick/up-left" which don't exist;
either add matching profile entries named "brick/up-right" and "brick/up-left"
to the profiles list (ensure they include the expected texture/uv/frame data) or
change those profile_id values to existing profile IDs used by other brick
variants; update the profiles block and the variant tuples that contain
object_class: Brick, type_id: 47 and type_id: 48 accordingly so the references
resolve.

In `@specs/026-direction-bricks/checklists/acceptance.md`:
- Around line 10-15: Update the axis references in the acceptance checklist:
change Y-velocity to Z-velocity for CHK002 (brick 43, Down) and CHK005 (brick
46, Up), and update the additive impulse item CHK006 (brick 43 scenario) from
"-3.0 Y-velocity → -8.0" to "-3.0 Z-velocity → -8.0"; keep CHK003 and CHK004
(Left/Right) as X-velocity, and ensure each checklist item explicitly uses the
X/Z gameplay plane terminology for clarity.

In `@specs/026-direction-bricks/contracts/observer-event.md`:
- Around line 29-33: Update the observer contract for
apply_direction_brick_effects to match the implementation: replace the
Trigger<DirectionBrickEffect> signature with On<DirectionBrickEffect> and
replace Query<&mut LinearVelocity, With<Ball>> with Query<&mut ExternalImpulse,
With<Ball>> so the contract reflects Bevy 0.17 observer syntax and the actual
component used by the function.
- Around line 14-20: The DirectionBrickEffect event struct in the contract is
missing the impulse field; update the DirectionBrickEffect definition to include
a public impulse: Vec3 field so it matches the implementation (see
DirectionBrickEffect and the impulse used/constructed in src/lib.rs lines where
the effect is created), ensuring the struct derives Event, Clone, Debug and
contains ball_entity: Entity, brick_type: u32, brick_position: Vec3,
velocity_before: Vec3, and impulse: Vec3.

In `@specs/026-direction-bricks/data-model.md`:
- Around line 155-199: The doc example in apply_direction_brick_effects
incorrectly mutates LinearVelocity directly but the real implementation uses an
ExternalImpulse component; update the example to match by obtaining or inserting
an ExternalImpulse for the ball entity (or use a Query/Commands pattern that
mirrors the real code) and set its impulse to the intended vector instead of
writing to velocity.linvel, and adjust the tracing details to compare
velocity_before with the resulting impulse (or note that velocity_after is
produced by the physics system after applying ExternalImpulse) so the example
matches the actual ExternalImpulse-based implementation.
- Around line 113-118: The pseudocode incorrectly uses degrees for
direction_angle and zeroes the Z component; update the pseudocode to generate
direction_angle in radians (e.g., range 0.0..TAU) to match the implementation
used by cos()/sin(), and change the velocity.z assignment so it preserves the
existing Z value instead of setting it to 0.0 (i.e., keep the original z
component when computing velocity.x and velocity.y); refer to the
direction_angle variable and the velocity.z assignment to locate the lines to
fix.

In `@specs/026-direction-bricks/plan.md`:
- Around line 155-167: The plan's documented system signature is inconsistent
with the implemented function; update the docs to match the implementation by
replacing the listed signature Query<&mut LinearVelocity, With<Ball>> with the
actual signature used in brick_effects.rs: pub fn
apply_direction_brick_effects(trigger: On<DirectionBrickEffect>, mut query:
Query<&mut ExternalImpulse>), removing any mention of With<Ball> or
LinearVelocity so the plan reflects the real function parameters and types.
- Line 34: The documentation lists the bevy_rapier3d LinearVelocity component
but the implementation uses ExternalImpulse in brick_effects; update the plan to
reflect the actual approach by replacing or clarifying "LinearVelocity
component" with "ExternalImpulse (used to apply impulses/forces)" and ensure
bevy_rapier3d 0.32.0 is still cited for physics and the ExternalImpulse usage;
mention that impulses are applied via the ExternalImpulse component in the brick
effects implementation to keep docs and src/systems/brick_effects.rs consistent.

In `@specs/026-direction-bricks/quickstart.md`:
- Around line 115-166: The example currently mutates LinearVelocity directly in
apply_direction_brick_effects (and apply_randomizer), contradicting the PR which
uses ExternalImpulse; change the system to fetch the target ball entity
(DirectionBrickEffect.ball_entity) and either insert/update an ExternalImpulse
component on that entity or send an event that applies an ExternalImpulse
(rather than writing to LinearVelocity.linvel), compute impulses in the same
switch/branches (and in apply_randomizer compute an impulse vector instead of
setting linvel), and update tests to assert that ExternalImpulse was
added/updated or that the ball's velocity changed after physics integration
instead of asserting direct LinearVelocity field writes. Ensure references to
apply_direction_brick_effects, DirectionBrickEffect, apply_randomizer,
ExternalImpulse, LinearVelocity, and Ball are used to locate the changes.
- Around line 36-90: The docs/tests use the removed LinearVelocity API—update
test examples to use bevy_rapier3d v0.32.0's Velocity type and ExternalImpulse
for applying impulses: replace usages of LinearVelocity::new(...) with Velocity
{ linvel: Vec3::new(...), angvel: Vec3::ZERO } (or Velocity::linear(...)) and
change any direct velocity writes in the DirectionBrickEffect tests (e.g.,
test_brick_43_down_impulse, test_brick_44_left_impulse, etc.) to simulate the
implementation by inserting/updating ExternalImpulse on the ball entity instead
of setting LinearVelocity directly, and read results from Velocity.linvel when
asserting outcomes.

In `@specs/026-direction-bricks/tasks.md`:
- Around line 3-25: Update the prerequisites and coordinate/API terminology:
change the rand version from "rand 0.8" to "rand 0.9.2" in the prerequisites
list, replace references to the non-existent bevy_rapier3d `LinearVelocity` with
`Velocity` and mention its `linvel` field (e.g., use Velocity.linvel), and
correct the coordinate-system wording from "X and Y modification, Z preserved"
to clarify that the horizontal plane is XZ and Y is vertical (e.g., "modify X
and Z, preserve Y"). Ensure these exact symbols/phrases (rand version,
LinearVelocity -> Velocity/linvel, and XZ vs Y vertical) are updated wherever
they appear in tasks.md.

In `@src/level_loader.rs`:
- Around line 646-651: spawn_bricks_only uses an outdated condition for tagging
bricks with CountsTowardsCompletion; update the logic in spawn_bricks_only to
match spawn_level_entities_impl by checking if brick_type_id <
INDESTRUCTIBLE_BRICK || brick_type_id == BRICK_54 || brick_type_id == BRICK_50
before calling entity.insert(CountsTowardsCompletion) so bricks 50 and 54
receive the same marker during spawn transitions.

In `@src/lib.rs`:
- Around line 877-881: The computation of ball_velocity is dead code because
transforms.get(triggering_ball).map(|_t| Vec3::ZERO) always yields ZERO; either
remove ball_velocity or replace it with an actual query of the ball's Velocity
component (e.g., read the Velocity for triggering_ball before applying the
impulse) and store it in velocity_before for logging/tracing; update references
to ball_velocity to use velocity_before or drop them if unused and ensure you
use the same entity id (triggering_ball) when fetching the Velocity component
instead of reading Transforms.
- Around line 884-901: The doc/comments in src/systems/brick_effects.rs
describing brick type to axis mappings are incorrect; update the documentation
to match the actual implementation in the impulse match (where current_type
produces Vec3 impulses) so the mappings read: Brick 43 (Down) -> impulse.x +=
5.0, Brick 44 (Left) -> impulse.z += 5.0, Brick 45 (Right) -> impulse.z -= 5.0,
Brick 46 (Up) -> impulse.x -= 5.0, Brick 47 (Up-Right) -> impulse.x -= 5.0 and
impulse.z -= 5.0, Brick 48 (Up-Left) -> impulse.x -= 5.0 and impulse.z += 5.0;
ensure the doc references Vec3/X/Z axes consistently with the match arm logic
(the RNG-based brick 52 can remain as-is) and remove or correct any references
that claimed X/Y axes or different direction assignments.

In `@src/signals.rs`:
- Around line 180-193: The DirectionBrickEffect docs are inconsistent with the
code/tests: update the documentation for DirectionBrickEffect to state the
coordinate system is XZ horizontal with Y vertical (gravity), correct the
per-brick mappings so bricks 43/44 represent ±Z (left/right on XZ plane) and
bricks 45/46 represent ±Y (up/down), and change the Random brick (52)
description to apply a random impulse in the XZ plane (Z and X components, Y=0)
with magnitude in [5.0, 15.0] and direction in [0, 2π); ensure you reference the
exact brick IDs (43–48 and 52) and keep the note that no brick should modify the
vertical/gravity axis incorrectly.

In `@src/systems/respawn.rs`:
- Line 458: The doc comment above the respawn/shrink logic is wrong: it
currently says “not the last ball” but the implementation triggers the shrink
animation when all balls are gone (last ball removed). Update the documentation
in src/systems/respawn.rs to state that the shrink animation is triggered when
the last ball is removed (i.e., when no balls remain), and make the wording
around “last ball” / “all balls gone” match the actual behavior implemented by
the respawn/shrink logic.

In `@tests/direction_bricks.rs`:
- Around line 56-59: The test name test_z_axis_preserved contradicts the
implementation of apply_impulse_to_ball which currently sets
ExternalImpulse.impulse via assignment (external_impulse.impulse = impulse)
causing the Z component to be overwritten; either rename the test to reflect
overwrite semantics (e.g., test_impulse_overwrites_previous) and update the
comment near line 185, or change apply_impulse_to_ball to accumulate impulses
(e.g., add the incoming impulse to existing ExternalImpulse.impulse using
+=/addition) so the Z axis is preserved—update the test expectation accordingly
and ensure you reference the ExternalImpulse struct and the
apply_impulse_to_ball function when making the change.
- Around line 301-372: The brick 52 implementation uses the XY plane but tests
expect XZ: update the Vec3 construction in the brick 52 case in src/lib.rs to
Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin()) (i.e., Y = 0)
to satisfy the tests; also replace the unseeded rand::rng() in both
test_brick_52_random_magnitude_range and
test_brick_52_random_direction_distribution with a deterministic seeded RNG (use
SeedableRng::seed_from_u64 / StdRng or similar) so the tests are not flaky and
use the same gen_range calls for floats.

In `@tests/paddle_shrink_regression.rs`:
- Around line 54-161: The test
paddle_shrinks_only_once_when_losing_multiple_balls_on_last_life has mismatched
intent: it says “last life” but sets lives.lives_remaining = 3; update the name
or comment to reflect it’s testing the “last ball” behavior (e.g., rename to
paddle_shrinks_only_once_when_losing_multiple_balls_on_last_ball) or set
lives.lives_remaining to 1 if you intended to test last-life behavior; adjust
the test doc comment accordingly so the test name, comment and the setup of
lives.lives_remaining are consistent.
🧹 Nitpick comments (6)
src/systems/ball_spawn_bricks.rs (1)

343-349: Consider expanding test coverage for y_shaped_velocity.

The current test only verifies non-zero output. Consider adding tests for:

  • Angle symmetry verification (left and right should be symmetric around base direction)
  • Zero horizontal velocity edge case (the min_vel fallback path)
  • Preservation of Y component
🧪 Suggested additional test cases
 #[test]
 fn y_shaped_velocity_returns_offsets() {
     let base = Vec3::new(1.0, 0.0, 0.0);
     let (left, right) = y_shaped_velocity(base, 37.5);
     assert!(left.length() > 0.0);
     assert!(right.length() > 0.0);
 }
+
+#[test]
+fn y_shaped_velocity_preserves_y_component() {
+    let base = Vec3::new(1.0, 5.0, 0.0);
+    let (left, right) = y_shaped_velocity(base, 37.5);
+    assert!((left.y - 5.0).abs() < f32::EPSILON);
+    assert!((right.y - 5.0).abs() < f32::EPSILON);
+}
+
+#[test]
+fn y_shaped_velocity_handles_zero_horizontal() {
+    let base = Vec3::new(0.0, 1.0, 0.0);
+    let (left, right) = y_shaped_velocity(base, 37.5);
+    assert!(left.length() > 0.0);
+    assert!(right.length() > 0.0);
+    // Should return opposing minimal horizontal velocities
+    assert!((left.x + right.x).abs() < f32::EPSILON);
+}
specs/026-direction-bricks/plan.md (1)

105-108: Minor inconsistency in angle units.

Line 106 correctly states to use radians (0.0..TAU), but Line 24 and other places reference "0-360°" (degrees). The implementation correctly uses radians. Consider updating the degree references to radians for consistency throughout the document.

src/systems/brick_effects.rs (2)

44-50: Documentation describes velocity modification but implementation uses ExternalImpulse.

The doc comments describe direct velocity modification:

/// - Brick 43 (Left): `velocity.x -= 5.0`
/// - Brick 44 (Right): `velocity.x += 5.0`

But the actual implementation sets ExternalImpulse.impulse, which the physics system then integrates. Consider updating the documentation to reflect the actual behavior, or note that the impulse achieves an equivalent effect.


138-141: Consider adding With<Ball> filter for defensive querying.

The query Query<&mut ExternalImpulse> could theoretically match any entity with ExternalImpulse. While effect.ball_entity targets the specific entity, adding With<Ball> filter provides an extra safety check that only ball entities can have impulses applied:

 pub fn apply_direction_brick_effects(
     trigger: On<DirectionBrickEffect>,
-    mut query: Query<&mut ExternalImpulse>,
+    mut query: Query<&mut ExternalImpulse, With<Ball>>,
 )

This would require importing Ball from the crate root.

specs/026-direction-bricks/contracts/observer-event.md (1)

143-154: Physics integration details are accurate.

The description of LinearVelocity component properties is informative, though the actual implementation uses ExternalImpulse rather than direct velocity modification. The physics propagation description correctly explains how changes flow through the system.

specs/026-direction-bricks/quickstart.md (1)

343-365: Update troubleshooting guidance to match correct implementation.

The troubleshooting section at line 352 references checking for the LinearVelocity component, which should be updated to match the actual component used in the implementation (likely Velocity or ExternalImpulse based on the PR summary).

Additionally, consider adding troubleshooting guidance for:

  • Physics integration timing (impulses applied vs velocity changes observable)
  • Frame-by-frame behavior and when velocity changes take effect
  • Differences between impulse application and direct velocity writes

Comment on lines +1433 to +1444
(
object_class: Brick,
type_id: 47,
profile_id: "brick/up-right",
emissive_color: None,
animation: None,
),
(
object_class: Brick,
type_id: 48,
profile_id: "brick/up-left",
emissive_color: None,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing profiles for brick/up-right and brick/up-left.

Type variants for bricks 47 and 48 reference profile IDs that don’t exist in the profiles list. This will likely fail to resolve or fall back unexpectedly. Add the missing profiles or point these variants at existing ones.

🤖 Prompt for AI Agents
In `@assets/textures/manifest.ron` around lines 1433 - 1444, Variants with
object_class Brick and type_id 47 and 48 reference profile_id "brick/up-right"
and "brick/up-left" which don't exist; either add matching profile entries named
"brick/up-right" and "brick/up-left" to the profiles list (ensure they include
the expected texture/uv/frame data) or change those profile_id values to
existing profile IDs used by other brick variants; update the profiles block and
the variant tuples that contain object_class: Brick, type_id: 47 and type_id: 48
accordingly so the references resolve.

Comment on lines +10 to +15
- [x] CHK001 - Are all single-direction brick scenarios (43-46) specified with measurable initial velocity and expected final velocity values? [Clarity, Spec §User Story 1]
- [x] CHK002 - Can brick 43 (Down) scenario be tested by asserting Y-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-1]
- [x] CHK003 - Can brick 44 (Left) scenario be tested by asserting X-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-2]
- [x] CHK004 - Can brick 45 (Right) scenario be tested by asserting X-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-3]
- [x] CHK005 - Can brick 46 (Up) scenario be tested by asserting Y-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-4]
- [x] CHK006 - Does the additive impulse scenario (brick 43 on -3.0 Y-velocity → -8.0) provide concrete numbers for verification? [Clarity, Spec §US1-Acceptance-5]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Acceptance checks use X/Y axes, conflicting with XZ gameplay plane.

CHK002–CHK005 assert X/Y changes for down/left/right/up, but the coordinate system elsewhere is XZ (Y vertical). This will mis-specify tests. Please update these checks to X/Z.

Suggested correction
- - [x] CHK002 - Can brick 43 (Down) scenario be tested by asserting Y-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-1]
- - [x] CHK003 - Can brick 44 (Left) scenario be tested by asserting X-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-2]
- - [x] CHK004 - Can brick 45 (Right) scenario be tested by asserting X-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-3]
- - [x] CHK005 - Can brick 46 (Up) scenario be tested by asserting Y-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-4]
+ - [x] CHK002 - Can brick 43 (Down) scenario be tested by asserting X-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-1]
+ - [x] CHK003 - Can brick 44 (Left) scenario be tested by asserting Z-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-2]
+ - [x] CHK004 - Can brick 45 (Right) scenario be tested by asserting Z-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-3]
+ - [x] CHK005 - Can brick 46 (Up) scenario be tested by asserting X-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-4]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [x] CHK001 - Are all single-direction brick scenarios (43-46) specified with measurable initial velocity and expected final velocity values? [Clarity, Spec §User Story 1]
- [x] CHK002 - Can brick 43 (Down) scenario be tested by asserting Y-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-1]
- [x] CHK003 - Can brick 44 (Left) scenario be tested by asserting X-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-2]
- [x] CHK004 - Can brick 45 (Right) scenario be tested by asserting X-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-3]
- [x] CHK005 - Can brick 46 (Up) scenario be tested by asserting Y-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-4]
- [x] CHK006 - Does the additive impulse scenario (brick 43 on -3.0 Y-velocity → -8.0) provide concrete numbers for verification? [Clarity, Spec §US1-Acceptance-5]
- [x] CHK001 - Are all single-direction brick scenarios (43-46) specified with measurable initial velocity and expected final velocity values? [Clarity, Spec §User Story 1]
- [x] CHK002 - Can brick 43 (Down) scenario be tested by asserting X-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-1]
- [x] CHK003 - Can brick 44 (Left) scenario be tested by asserting Z-velocity increased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-2]
- [x] CHK004 - Can brick 45 (Right) scenario be tested by asserting Z-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-3]
- [x] CHK005 - Can brick 46 (Up) scenario be tested by asserting X-velocity decreased by exactly 5.0 units/sec? [Measurability, Spec §US1-Acceptance-4]
- [x] CHK006 - Does the additive impulse scenario (brick 43 on -3.0 Y-velocity → -8.0) provide concrete numbers for verification? [Clarity, Spec §US1-Acceptance-5]
🤖 Prompt for AI Agents
In `@specs/026-direction-bricks/checklists/acceptance.md` around lines 10 - 15,
Update the axis references in the acceptance checklist: change Y-velocity to
Z-velocity for CHK002 (brick 43, Down) and CHK005 (brick 46, Up), and update the
additive impulse item CHK006 (brick 43 scenario) from "-3.0 Y-velocity → -8.0"
to "-3.0 Z-velocity → -8.0"; keep CHK003 and CHK004 (Left/Right) as X-velocity,
and ensure each checklist item explicitly uses the X/Z gameplay plane
terminology for clarity.

Comment on lines +14 to +20
#[derive(Event, Clone, Debug)]
pub struct DirectionBrickEffect {
pub ball_entity: Entity,
pub brick_type: u32,
pub brick_position: Vec3,
pub velocity_before: Vec3,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

DirectionBrickEffect struct missing impulse field.

The contract shows the event struct without the impulse field, but based on the implementation in src/lib.rs (lines 903-909), the actual struct includes an impulse: Vec3 field that carries the computed impulse vector. Update the contract to include this field:

 pub struct DirectionBrickEffect {
     pub ball_entity: Entity,
     pub brick_type: u32,
     pub brick_position: Vec3,
     pub velocity_before: Vec3,
+    pub impulse: Vec3,
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Event, Clone, Debug)]
pub struct DirectionBrickEffect {
pub ball_entity: Entity,
pub brick_type: u32,
pub brick_position: Vec3,
pub velocity_before: Vec3,
}
#[derive(Event, Clone, Debug)]
pub struct DirectionBrickEffect {
pub ball_entity: Entity,
pub brick_type: u32,
pub brick_position: Vec3,
pub velocity_before: Vec3,
pub impulse: Vec3,
}
🤖 Prompt for AI Agents
In `@specs/026-direction-bricks/contracts/observer-event.md` around lines 14 - 20,
The DirectionBrickEffect event struct in the contract is missing the impulse
field; update the DirectionBrickEffect definition to include a public impulse:
Vec3 field so it matches the implementation (see DirectionBrickEffect and the
impulse used/constructed in src/lib.rs lines where the effect is created),
ensuring the struct derives Event, Clone, Debug and contains ball_entity:
Entity, brick_type: u32, brick_position: Vec3, velocity_before: Vec3, and
impulse: Vec3.

Comment on lines +29 to +33
```rust
pub fn apply_direction_brick_effects(
trigger: Trigger<DirectionBrickEffect>,
mut query: Query<&mut LinearVelocity, With<Ball>>,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Observer signature doesn't match implementation.

The contract shows:

trigger: Trigger<DirectionBrickEffect>,
mut query: Query<&mut LinearVelocity, With<Ball>>,

But the actual implementation uses:

trigger: On<DirectionBrickEffect>,
mut query: Query<&mut ExternalImpulse>,

Update the contract to reflect the actual implementation using On<T> (Bevy 0.17 observer syntax) and ExternalImpulse.

🤖 Prompt for AI Agents
In `@specs/026-direction-bricks/contracts/observer-event.md` around lines 29 - 33,
Update the observer contract for apply_direction_brick_effects to match the
implementation: replace the Trigger<DirectionBrickEffect> signature with
On<DirectionBrickEffect> and replace Query<&mut LinearVelocity, With<Ball>> with
Query<&mut ExternalImpulse, With<Ball>> so the contract reflects Bevy 0.17
observer syntax and the actual component used by the function.

Comment on lines +56 to +59
fn apply_impulse_to_ball(app: &mut App, ball: Entity, impulse: Vec3) -> Vec3 {
if let Some(mut external_impulse) = app.world_mut().get_mut::<ExternalImpulse>(ball) {
external_impulse.impulse = impulse;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l tests/direction_bricks.rs

Repository: cleder/brkrs

Length of output: 85


🏁 Script executed:

cat -n tests/direction_bricks.rs

Repository: cleder/brkrs

Length of output: 16396


Test name contradicts assertion: "preserved" vs. overwrite behavior.

apply_impulse_to_ball overwrites ExternalImpulse.impulse with =, so the second call replaces the Z impulse with 0. The test test_z_axis_preserved has a misleading name—it actually verifies overwriting, not preservation. Either rename the test to reflect overwrite semantics (e.g., test_impulse_overwrites_previous) or change the implementation to use += if accumulation is intended.

The comment on line 185 acknowledges this: "The second call overwrites, so Z is zero"—but the test name suggests the opposite.

🤖 Prompt for AI Agents
In `@tests/direction_bricks.rs` around lines 56 - 59, The test name
test_z_axis_preserved contradicts the implementation of apply_impulse_to_ball
which currently sets ExternalImpulse.impulse via assignment
(external_impulse.impulse = impulse) causing the Z component to be overwritten;
either rename the test to reflect overwrite semantics (e.g.,
test_impulse_overwrites_previous) and update the comment near line 185, or
change apply_impulse_to_ball to accumulate impulses (e.g., add the incoming
impulse to existing ExternalImpulse.impulse using +=/addition) so the Z axis is
preserved—update the test expectation accordingly and ensure you reference the
ExternalImpulse struct and the apply_impulse_to_ball function when making the
change.

Comment on lines +301 to +372
/// T026: Brick 52 (Random) applies impulse with magnitude in range [5.0, 15.0]
#[test]
fn test_brick_52_random_magnitude_range() {
use rand::Rng;

// Test multiple samples to verify RNG distribution
let mut rng = rand::rng();
for _ in 0..100 {
// Simulate the RNG logic from lib.rs brick 52 case
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
let impulse = Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin());
let magnitude_computed = (impulse.x * impulse.x + impulse.z * impulse.z).sqrt();

assert!(
magnitude_computed >= 5.0,
"Magnitude should be >= 5.0, got {:.2}",
magnitude_computed
);
assert!(
magnitude_computed < 15.0,
"Magnitude should be < 15.0, got {:.2}",
magnitude_computed
);
assert_eq!(
impulse.y, 0.0,
"Y component should be zero for random brick (XZ plane only)"
);
}
}

/// T027: Brick 52 (Random) produces varied impulse directions across 0-2π range
#[test]
fn test_brick_52_random_direction_distribution() {
use rand::Rng;

// Collect samples and verify we get varied angles
let mut rng = rand::rng();
let mut angles = Vec::new();
let mut magnitudes = Vec::new();

for _ in 0..50 {
let magnitude = rng.random_range(5.0..15.0);
let angle: f32 = rng.random_range(0.0..std::f32::consts::TAU);
let impulse = Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin());
let computed_angle = impulse.z.atan2(impulse.x);
let computed_magnitude = (impulse.x * impulse.x + impulse.z * impulse.z).sqrt();

angles.push(computed_angle);
magnitudes.push(computed_magnitude);
}

// Verify we have diverse angles (not all clustered in one direction)
let min_angle = angles.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_angle = angles.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let angle_range = (max_angle - min_angle).abs();
assert!(
angle_range > 3.0, // At least ~π radians of spread
"Random angles should be distributed across range, got range {:.2}",
angle_range
);

// Verify we have diverse magnitudes
let min_mag = magnitudes.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let max_mag = magnitudes.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let mag_range = max_mag - min_mag;
assert!(
mag_range > 5.0,
"Random magnitudes should be distributed, got range {:.2}",
mag_range
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Explore repository structure
find . -type f -name "*.rs" | head -20

Repository: cleder/brkrs

Length of output: 669


🏁 Script executed:

# Look at the test file to understand the full context
wc -l tests/direction_bricks.rs

Repository: cleder/brkrs

Length of output: 85


🏁 Script executed:

# Check for lib.rs location
find . -type f -name "lib.rs"

Repository: cleder/brkrs

Length of output: 106


🏁 Script executed:

# Read the full test file to understand context
cat -n tests/direction_bricks.rs

Repository: cleder/brkrs

Length of output: 16396


🏁 Script executed:

# Search for brick 52 implementation in lib.rs
grep -n "brick 52\|brick_52\|52 =>" src/lib.rs -i | head -20

Repository: cleder/brkrs

Length of output: 95


🏁 Script executed:

# Also search for the random case in lib.rs to see the actual implementation
rg -n "case 52|brick.*(52|random)" src/lib.rs -i -A 10 | head -50

Repository: cleder/brkrs

Length of output: 695


🏁 Script executed:

# Get more context around brick 52 implementation
sed -n '875,950p' src/lib.rs | cat -n

Repository: cleder/brkrs

Length of output: 4627


🏁 Script executed:

# Check if there are any other tests using seeded RNG in the codebase
rg "StdRng|seed_from_u64|SeedableRng" tests/ --type rs

Repository: cleder/brkrs

Length of output: 82


🏁 Script executed:

# Check rand crate documentation and typical patterns for test flakiness
rg "rand::rng\|random_range" tests/ --type rs -B 2 -A 2

Repository: cleder/brkrs

Length of output: 82


🏁 Script executed:

# Check for seeded RNG usage in tests directory
rg "StdRng|seed_from_u64|SeedableRng" tests/

Repository: cleder/brkrs

Length of output: 38


🏁 Script executed:

# Check other RNG patterns in tests
rg "rand::rng|random_range" tests/ -B 2 -A 2

Repository: cleder/brkrs

Length of output: 5598


🏁 Script executed:

# Check Cargo.toml to see rand version
grep -A 5 "\[dependencies\]" Cargo.toml | head -20

Repository: cleder/brkrs

Length of output: 193


🏁 Script executed:

# Search for any #[ignore] or #[skip] marks on these tests
grep -B 3 "fn test_brick_52_random" tests/direction_bricks.rs

Repository: cleder/brkrs

Length of output: 332


🏁 Script executed:

# Get the exact impulse construction in the test to confirm XY vs XZ mismatch
sed -n '301,330p' tests/direction_bricks.rs | grep -A 5 "Vec3::new"

Repository: cleder/brkrs

Length of output: 342


🏁 Script executed:

# Check the exact implementation from lib.rs again for brick 52
sed -n '891,925p' src/lib.rs | grep -A 5 "52 =>"

Repository: cleder/brkrs

Length of output: 453


🏁 Script executed:

# Try running cargo test on these specific tests to see if they pass
cd . && cargo test test_brick_52 --lib 2>&1 | head -100

Repository: cleder/brkrs

Length of output: 302


🏁 Script executed:

# Verify the exact coordinate discrepancy between implementation and test
echo "=== Test expectation ===" && grep -A 2 "impulse = Vec3::new" tests/direction_bricks.rs | head -3
echo ""
echo "=== Implementation ===" && sed -n '917,924p' src/lib.rs

Repository: cleder/brkrs

Length of output: 629


🏁 Script executed:

# Check if there are any skip/ignore attributes or if tests are in a separate module
head -30 tests/direction_bricks.rs | grep -E "mod|#\[|ignore"

Repository: cleder/brkrs

Length of output: 152


🏁 Script executed:

# Look for comment in lib.rs about XY plane to confirm the mismatch
grep -B 2 -A 2 "XY plane" src/lib.rs

Repository: cleder/brkrs

Length of output: 390


🏁 Script executed:

# Get the exact line 924 context from lib.rs to confirm
sed -n '917,925p' src/lib.rs | cat -n

Repository: cleder/brkrs

Length of output: 640


🏁 Script executed:

# Double-check the test impulse line
sed -n '310,315p' tests/direction_bricks.rs | cat -n

Repository: cleder/brkrs

Length of output: 416


🏁 Script executed:

# Check the file header comments to see which plane should be used
head -20 tests/direction_bricks.rs | grep -i "plane\|coordinate"

Repository: cleder/brkrs

Length of output: 222


🏁 Script executed:

# Search for coordinate system documentation in the codebase
rg "coordinate|XY plane|XZ plane" src/ tests/ -i | head -20

Repository: cleder/brkrs

Length of output: 1918


Fix coordinate system mismatch in brick 52 implementation: tests expect XZ plane, not XY plane.

The implementation in src/lib.rs (line 924) incorrectly uses the XY plane (Vec3::new(magnitude * angle.cos(), magnitude * angle.sin(), 0.0)), but the test file header and all test assertions expect the XZ plane (Vec3::new(magnitude * angle.cos(), 0.0, magnitude * angle.sin())). The test at line 327 explicitly asserts that the Y component must be zero, which the current implementation violates.

Additionally, replace unseeded rand::rng() with a deterministic seeded RNG to prevent test flakiness:

-    use rand::Rng;
+    use rand::{Rng, SeedableRng};
+    use rand::rngs::StdRng;
@@
-    let mut rng = rand::rng();
+    let mut rng = StdRng::seed_from_u64(42);

Apply the same seeding fix to both test_brick_52_random_magnitude_range and test_brick_52_random_direction_distribution.

🤖 Prompt for AI Agents
In `@tests/direction_bricks.rs` around lines 301 - 372, The brick 52
implementation uses the XY plane but tests expect XZ: update the Vec3
construction in the brick 52 case in src/lib.rs to Vec3::new(magnitude *
angle.cos(), 0.0, magnitude * angle.sin()) (i.e., Y = 0) to satisfy the tests;
also replace the unseeded rand::rng() in both
test_brick_52_random_magnitude_range and
test_brick_52_random_direction_distribution with a deterministic seeded RNG (use
SeedableRng::seed_from_u64 / StdRng or similar) so the tests are not flaky and
use the same gen_range calls for floats.

Comment on lines +54 to +161
fn paddle_shrinks_only_once_when_losing_multiple_balls_on_last_life() {
let mut app = test_app();
let lower_goal = app.world_mut().spawn(LowerGoal).id();

// Set lives to 1 so any ball loss that removes all balls would trigger shrink
{
let mut lives = app
.world_mut()
.resource_mut::<brkrs::systems::respawn::LivesState>();
lives.lives_remaining = 3;
}

// Spawn multiple balls
let ball1 = app
.world_mut()
.spawn((Ball, ball_handle_at(Vec3::new(0.0, 2.0, 0.0))))
.id();
let ball2 = app
.world_mut()
.spawn((Ball, ball_handle_at(Vec3::new(2.0, 2.0, 0.0))))
.id();
let ball3 = app
.world_mut()
.spawn((Ball, ball_handle_at(Vec3::new(-2.0, 2.0, 0.0))))
.id();

let paddle = app
.world_mut()
.spawn((
Paddle,
Transform::from_scale(Vec3::ONE),
Velocity::zero(),
paddle_handle_at(Vec3::new(0.0, 2.0, 0.0)),
))
.id();

// Trigger loss of first ball ONLY
app.world_mut()
.resource_mut::<Messages<CollisionEvent>>()
.write(CollisionEvent::Started(
ball1,
lower_goal,
CollisionEventFlags::SENSOR,
));

advance_time(&mut app, 0.016);
app.update();

// Verify paddle does NOT have PaddleGrowing because other balls still exist
// (other balls exist, so paddle should NOT shrink even though a ball was lost)
assert!(
app.world().entity(paddle).get::<PaddleGrowing>().is_none(),
"Paddle should NOT shrink when other balls still exist, even if a ball was lost"
);

// Trigger loss of second ball
app.world_mut()
.resource_mut::<Messages<CollisionEvent>>()
.write(CollisionEvent::Started(
ball2,
lower_goal,
CollisionEventFlags::SENSOR,
));

advance_time(&mut app, 0.016);
app.update();

// Still should not shrink (ball3 still exists)
assert!(
app.world().entity(paddle).get::<PaddleGrowing>().is_none(),
"Paddle should NOT shrink when other balls still exist"
);

// Trigger loss of last ball
app.world_mut()
.resource_mut::<Messages<CollisionEvent>>()
.write(CollisionEvent::Started(
ball3,
lower_goal,
CollisionEventFlags::SENSOR,
));

advance_time(&mut app, 0.016);
app.update();

// Now paddle SHOULD have PaddleGrowing because all balls are gone (last ball lost)
let growing = app.world().entity(paddle).get::<PaddleGrowing>();

assert!(
growing.is_some(),
"Paddle should shrink when the last ball is lost (all balls gone), regardless of lives count"
);

// Verify the shrink component is configured correctly
let growing_component = growing.unwrap();

assert_eq!(
growing_component.start_scale,
Vec3::ONE,
"Paddle shrink should start from full size (Vec3::ONE)"
);

assert_eq!(
growing_component.target_scale,
Vec3::splat(0.01),
"Paddle shrink should target very small size (0.01)"
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align test name/comment with the lives setup.
The test name/comment mention “last life,” but lives_remaining is set to 3. Consider renaming or updating the comment to reflect “last ball” behavior.

✏️ Suggested rename/comment alignment
-fn paddle_shrinks_only_once_when_losing_multiple_balls_on_last_life() {
+fn paddle_shrinks_only_once_when_losing_multiple_balls() {
@@
-    // Set lives to 1 so any ball loss that removes all balls would trigger shrink
+    // Keep lives > 1 to show shrink is tied to "last ball" rather than "last life"

Optional: Codacy flags this test as >50 lines; consider extracting a helper if you want to quiet that warning.

🧰 Tools
🪛 GitHub Check: Codacy Static Code Analysis

[warning] 54-54: tests/paddle_shrink_regression.rs#L54
Method paddle_shrinks_only_once_when_losing_multiple_balls_on_last_life has 82 lines of code (limit is 50)

🤖 Prompt for AI Agents
In `@tests/paddle_shrink_regression.rs` around lines 54 - 161, The test
paddle_shrinks_only_once_when_losing_multiple_balls_on_last_life has mismatched
intent: it says “last life” but sets lives.lives_remaining = 3; update the name
or comment to reflect it’s testing the “last ball” behavior (e.g., rename to
paddle_shrinks_only_once_when_losing_multiple_balls_on_last_ball) or set
lives.lives_remaining to 1 if you intended to test last-life behavior; adjust
the test doc comment accordingly so the test name, comment and the setup of
lives.lives_remaining are consistent.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/systems/brick_effects.rs`:
- Around line 43-55: The module docs for the cardinal-direction bricks are using
X/Y axis language but the implementation uses X/Z; update the comment block
describing bricks 43–48 so each mapping matches the actual Vec3 impulses in
src/lib.rs (e.g. the Vec3::new(...) entries used for Brick 43, 44, etc.) and
align wording with src/signals.rs (use X and Z axis labels and correct
directions for Left/Right/Forward/Backward/Up/Down); keep the Random Direction
(Brick 52) description but note it replaces X and Z (not X and Y) and preserves
Y or Z as per the implementation so the docs accurately reflect the code.
🧹 Nitpick comments (1)
tests/direction_bricks.rs (1)

301-330: Unseeded RNG may cause non-deterministic test behavior.

Using rand::rng() without seeding could lead to flaky tests in certain CI environments or when debugging failures. While the test checks bounds rather than exact values, seeded RNG improves reproducibility.

🔧 Suggested improvement
+use rand::SeedableRng;
+use rand::rngs::StdRng;
+
 fn test_brick_52_random_magnitude_range() {
-    use rand::Rng;
-
-    let mut rng = rand::rng();
+    use rand::Rng;
+    let mut rng = StdRng::seed_from_u64(42);

Comment on lines +43 to +55
///
/// **Cardinal Directions (Bricks 43-48)**: 5.0 units/sec additive impulse per brick type
/// - Brick 43 (Left): `velocity.x -= 5.0`
/// - Brick 44 (Right): `velocity.x += 5.0`
/// - Brick 45 (Up): `velocity.y += 5.0`
/// - Brick 46 (Down): `velocity.y -= 5.0`
/// - Brick 47 (Forward): `velocity.z += 5.0`
/// - Brick 48 (Backward): `velocity.z -= 5.0`
///
/// **Random Direction (Brick 52)**: Magnitude 5.0-15.0 units/sec, direction 0.0..2π radians
/// - Replaces both X and Y velocities (NOT additive)
/// - Preserves Z velocity
/// - Uses seeded RNG for deterministic behavior in tests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Module docs describe incorrect axis mappings.

The documentation (lines 43-50) describes brick behaviors using X/Y axes:

  • "Brick 43 (Left): velocity.x -= 5.0"
  • "Brick 45 (Up): velocity.y += 5.0"

But the actual implementation in src/lib.rs uses X/Z axes:

  • Brick 43: Vec3::new(5.0, 0.0, 0.0) (Forward, +X)
  • Brick 44: Vec3::new(0.0, 0.0, 5.0) (Left, +Z)
  • etc.

Update the docs to match the implementation and align with src/signals.rs which has the correct mappings.

📝 Suggested correction
 /// **Cardinal Directions (Bricks 43-48)**: 5.0 units/sec additive impulse per brick type
-/// - Brick 43 (Left): `velocity.x -= 5.0`
-/// - Brick 44 (Right): `velocity.x += 5.0`
-/// - Brick 45 (Up): `velocity.y += 5.0`
-/// - Brick 46 (Down): `velocity.y -= 5.0`
-/// - Brick 47 (Forward): `velocity.z += 5.0`
-/// - Brick 48 (Backward): `velocity.z -= 5.0`
+/// - Brick 43 (Forward): `impulse.x += 5.0` (toward far wall)
+/// - Brick 44 (Left): `impulse.z += 5.0`
+/// - Brick 45 (Right): `impulse.z -= 5.0`
+/// - Brick 46 (Backward): `impulse.x -= 5.0` (toward paddle)
+/// - Brick 47 (Backward-Right): `impulse.x -= 5.0, impulse.z -= 5.0`
+/// - Brick 48 (Backward-Left): `impulse.x -= 5.0, impulse.z += 5.0`
🤖 Prompt for AI Agents
In `@src/systems/brick_effects.rs` around lines 43 - 55, The module docs for the
cardinal-direction bricks are using X/Y axis language but the implementation
uses X/Z; update the comment block describing bricks 43–48 so each mapping
matches the actual Vec3 impulses in src/lib.rs (e.g. the Vec3::new(...) entries
used for Brick 43, 44, etc.) and align wording with src/signals.rs (use X and Z
axis labels and correct directions for Left/Right/Forward/Backward/Up/Down);
keep the Random Direction (Brick 52) description but note it replaces X and Z
(not X and Y) and preserves Y or Z as per the implementation so the docs
accurately reflect the code.

@qodo-code-review
Copy link

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Test Suite

Failed stage: Run cargo test [❌]

Failed test name: paddle_shrink_multiball

Failure summary:

The GitHub Action failed because cargo test --all-features returned exit code 101 after failing
integration tests in tests/paddle_shrink_multiball.rs.
- The test binary paddle_shrink_multiball had
3 failing tests (1 passed, 3 failed):
- no_visual_effects_when_multiple_balls_in_play panicked at
tests/paddle_shrink_multiball.rs:209:5 with: Paddle should NOT shrink when other balls are still in
play.
- paddle_does_not_shrink_when_multiple_balls_in_play panicked at
tests/paddle_shrink_multiball.rs:105:5 with: Paddle should NOT shrink when other balls are still in
play.
- paddle_shrinks_when_last_ball_is_lost panicked at tests/paddle_shrink_multiball.rs:128:5
with: Paddle should not shrink when lives > 1 (respawn will happen).
This indicates the paddle
shrink/visual-effects logic is being triggered in situations where the tests expect it not to (e.g.,
when multiple balls are still in play, or when a respawn is expected because lives remain).

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

289:  ##[group]Run : set $CARGO_HOME
290:  �[36;1m: set $CARGO_HOME�[0m
291:  �[36;1mecho CARGO_HOME=${CARGO_HOME:-"$HOME/.cargo"} >> $GITHUB_ENV�[0m
292:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
293:  env:
294:  CARGO_TERM_COLOR: always
295:  CARGO_INCREMENTAL: 0
296:  CARGO_NET_RETRY: 10
297:  RUSTUP_MAX_RETRIES: 10
298:  CARGO_BUILD_JOBS: 4
299:  RUST_TEST_THREADS: 1
300:  ##[endgroup]
301:  ##[group]Run : install rustup if needed
302:  �[36;1m: install rustup if needed�[0m
303:  �[36;1mif ! command -v rustup &>/dev/null; then�[0m
304:  �[36;1m  curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail https://sh.rustup.rs | sh -s -- --default-toolchain none -y�[0m
305:  �[36;1m  echo "$CARGO_HOME/bin" >> $GITHUB_PATH�[0m
...

402:  �[36;1m  elif rustc +stable --version --verbose | grep -q '^release: 1\.6[67]\.'; then�[0m
403:  �[36;1m    touch "/home/runner/work/_temp"/.implicit_cargo_registries_crates_io_protocol || true�[0m
404:  �[36;1m    echo CARGO_REGISTRIES_CRATES_IO_PROTOCOL=git >> $GITHUB_ENV�[0m
405:  �[36;1m  fi�[0m
406:  �[36;1mfi�[0m
407:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
408:  env:
409:  CARGO_TERM_COLOR: always
410:  CARGO_INCREMENTAL: 0
411:  CARGO_NET_RETRY: 10
412:  RUSTUP_MAX_RETRIES: 10
413:  CARGO_BUILD_JOBS: 4
414:  RUST_TEST_THREADS: 1
415:  CARGO_HOME: /home/runner/.cargo
416:  ##[endgroup]
417:  ##[group]Run : work around spurious network errors in curl 8.0
418:  �[36;1m: work around spurious network errors in curl 8.0�[0m
419:  �[36;1m# https://rust-lang.zulipchat.com/#narrow/stream/246057-t-cargo/topic/timeout.20investigation�[0m
...

520:  Restored from cache key "v0-rust-Linux-ci-test-634defc8a68815d4c8d12649b08689f7d75cba3c3561881aba2b84061c7cb84f-026-direction-bricks-Linux-x64-9e314f71-2e2d1383" full match: true.
521:  ##[group]Run sudo apt-get update
522:  �[36;1msudo apt-get update�[0m
523:  �[36;1m# Ensure pkg-config and development headers are available so native�[0m
524:  �[36;1m# crates like wayland-sys can find system libraries (wayland-client.pc)�[0m
525:  �[36;1msudo apt-get install --no-install-recommends -y pkg-config libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev�[0m
526:  shell: /usr/bin/bash -e {0}
527:  env:
528:  CARGO_TERM_COLOR: always
529:  CARGO_INCREMENTAL: 0
530:  CARGO_NET_RETRY: 10
531:  RUSTUP_MAX_RETRIES: 10
532:  CARGO_BUILD_JOBS: 4
533:  RUST_TEST_THREADS: 1
534:  CARGO_HOME: /home/runner/.cargo
535:  CACHE_ON_FAILURE: false
536:  ##[endgroup]
...

667:  No services need to be restarted.
668:  No containers need to be restarted.
669:  No user sessions are running outdated binaries.
670:  No VM guests are running outdated hypervisor (qemu) binaries on this host.
671:  ##[group]Run echo "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV
672:  �[36;1mecho "PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig" >> $GITHUB_ENV�[0m
673:  shell: /usr/bin/bash -e {0}
674:  env:
675:  CARGO_TERM_COLOR: always
676:  CARGO_INCREMENTAL: 0
677:  CARGO_NET_RETRY: 10
678:  RUSTUP_MAX_RETRIES: 10
679:  CARGO_BUILD_JOBS: 4
680:  RUST_TEST_THREADS: 1
681:  CARGO_HOME: /home/runner/.cargo
682:  CACHE_ON_FAILURE: false
683:  ##[endgroup]
684:  ##[group]Run cargo fetch --locked
685:  �[36;1mcargo fetch --locked�[0m
686:  �[36;1mcargo test --no-run --all-features || true�[0m
687:  shell: /usr/bin/bash -e {0}
688:  env:
689:  CARGO_TERM_COLOR: always
690:  CARGO_INCREMENTAL: 0
691:  CARGO_NET_RETRY: 10
692:  RUSTUP_MAX_RETRIES: 10
693:  CARGO_BUILD_JOBS: 4
694:  RUST_TEST_THREADS: 1
695:  CARGO_HOME: /home/runner/.cargo
696:  CACHE_ON_FAILURE: false
697:  PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
...

714:  �[1m�[94m|�[0m         �[1m�[94m|�[0m
715:  �[1m�[94m|�[0m         �[1m�[94mhelp: remove this `mut`�[0m
716:  �[1m�[94m|�[0m
717:  �[1m�[94m= �[0m�[1mnote�[0m: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
718:  �[1m�[33mwarning�[0m: `brkrs` (test "paddle_destroyable_brick") generated 1 warning (run `cargo fix --test "paddle_destroyable_brick" -p brkrs` to apply 1 suggestion)
719:  �[1m�[33mwarning�[0m�[1m: use of deprecated type alias `bevy::prelude::Trigger`: Renamed to `On`.�[0m
720:  �[1m�[94m--> �[0mtests/merkaba_paddle.rs:25:15
721:  �[1m�[94m|�[0m
722:  �[1m�[94m25�[0m �[1m�[94m|�[0m     _trigger: Trigger<MerkabaPaddleCollision>,
723:  �[1m�[94m|�[0m               �[1m�[33m^^^^^^^�[0m
724:  �[1m�[94m|�[0m
725:  �[1m�[94m= �[0m�[1mnote�[0m: `#[warn(deprecated)]` on by default
726:  �[1m�[33mwarning�[0m�[1m: use of deprecated method `bevy::prelude::App::add_event`: Use `add_message` instead.�[0m
727:  �[1m�[94m--> �[0mtests/merkaba_paddle.rs:60:10
728:  �[1m�[94m|�[0m
729:  �[1m�[94m60�[0m �[1m�[94m|�[0m �[1m�[94m...�[0m   .add_event::<CollisionEvent>() // Rapier uses Events, not Messages in Bevy 0.17+ context usually, but earlier errors suggested Mess�[1m�[94m...�[0m
730:  �[1m�[94m|�[0m        �[1m�[33m^^^^^^^^^�[0m
...

816:  �[1m�[92m  Executable�[0m tests/ui_compliance_audit.rs (target/debug/deps/ui_compliance_audit-b9d30d56d13e8a96)
817:  �[1m�[92m  Executable�[0m tests/ui_level_label.rs (target/debug/deps/ui_level_label-0b54731b4a1a8fd4)
818:  �[1m�[92m  Executable�[0m tests/ui_overlays.rs (target/debug/deps/ui_overlays-e912298d75737e27)
819:  �[1m�[92m  Executable�[0m tests/uv_transforms.rs (target/debug/deps/uv_transforms-2fa8f844455dd0b8)
820:  ##[group]Run cargo test --all-features
821:  �[36;1mcargo test --all-features�[0m
822:  shell: /usr/bin/bash -e {0}
823:  env:
824:  CARGO_TERM_COLOR: always
825:  CARGO_INCREMENTAL: 0
826:  CARGO_NET_RETRY: 10
827:  RUSTUP_MAX_RETRIES: 10
828:  CARGO_BUILD_JOBS: 4
829:  RUST_TEST_THREADS: 1
830:  CARGO_HOME: /home/runner/.cargo
831:  CACHE_ON_FAILURE: false
832:  PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig
...

837:  �[1m�[94m431�[0m �[1m�[94m|�[0m     let ball = spawn_test_ball(&mut app, Vec3::ZERO, Vec3::new(1.0, 0.0, 0.5));
838:  �[1m�[94m|�[0m         �[1m�[33m^^^^�[0m �[1m�[33mhelp: if this is intentional, prefix it with an underscore: `_ball`�[0m
839:  �[1m�[94m|�[0m
840:  �[1m�[94m= �[0m�[1mnote�[0m: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default
841:  �[1m�[33mwarning�[0m: `brkrs` (test "ball_spawn_bricks") generated 1 warning (run `cargo fix --test "ball_spawn_bricks" -p brkrs` to apply 1 suggestion)
842:  �[1m�[33mwarning�[0m�[1m: use of deprecated type alias `bevy::prelude::Trigger`: Renamed to `On`.�[0m
843:  �[1m�[94m--> �[0mtests/merkaba_paddle.rs:25:15
844:  �[1m�[94m|�[0m
845:  �[1m�[94m25�[0m �[1m�[94m|�[0m     _trigger: Trigger<MerkabaPaddleCollision>,
846:  �[1m�[94m|�[0m               �[1m�[33m^^^^^^^�[0m
847:  �[1m�[94m|�[0m
848:  �[1m�[94m= �[0m�[1mnote�[0m: `#[warn(deprecated)]` on by default
849:  �[1m�[33mwarning�[0m�[1m: use of deprecated method `bevy::prelude::App::add_event`: Use `add_message` instead.�[0m
850:  �[1m�[94m--> �[0mtests/merkaba_paddle.rs:60:10
851:  �[1m�[94m|�[0m
852:  �[1m�[94m60�[0m �[1m�[94m|�[0m �[1m�[94m...�[0m   .add_event::<CollisionEvent>() // Rapier uses Events, not Messages in Bevy 0.17+ context usually, but earlier errors suggested Mess�[1m�[94m...�[0m
853:  �[1m�[94m|�[0m        �[1m�[33m^^^^^^^^^�[0m
...

922:  test systems::respawn::multi_respawn::sequential_life_losses_complete_in_order ... ok
923:  test systems::respawn::respawn_timer::ball_remains_frozen_until_launch_unlock ... ok
924:  test systems::respawn::respawn_timer::respawn_timer_finishes_within_frame_tolerance ... ok
925:  test systems::respawn::respawn_visual::controls_wait_until_overlay_finishes ... ok
926:  test systems::respawn::respawn_visual::overlay_spawns_on_respawn_schedule ... ok
927:  test systems::respawn::tests::ball_unlocks_only_after_paddle_ready ... ok
928:  test systems::respawn::tests::collision_triggers_respawn_schedule ... ok
929:  test systems::respawn::tests::control_stage_unlocks_paddle_after_growth ... ok
930:  test systems::respawn::tests::control_stage_waits_for_growth_or_timer ... ok
931:  test systems::respawn::tests::executor_respawns_paddle_and_ball ... ok
932:  test systems::respawn::tests::queued_life_losses_run_after_pending_respawn ... ok
933:  test systems::respawn::tests::scheduler_emits_game_over_when_no_lives ... ok
934:  test ui::gravity_indicator::tests::mixed_axes_selects_highest_level ... ok
935:  test ui::gravity_indicator::tests::tolerance_rounding_maps_correctly ... ok
936:  test ui::gravity_indicator::tests::unknown_when_outside_tolerance ... ok
937:  test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.37s
938:  �[1m�[92m     Running�[0m unittests src/main.rs (target/debug/deps/brkrs-441bfb23e6003882)
939:  running 0 tests
940:  test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
941:  �[1m�[92m     Running�[0m tests/audio_config.rs (target/debug/deps/audio_config-1f729087585779a3)
942:  running 7 tests
943:  test audio_config_clamps_volume_in_constructor ... ok
944:  test audio_config_default_has_full_volume ... ok
945:  test audio_config_is_valid_checks_volume_range ... ok
946:  test audio_config_set_volume_clamps ... ok
947:  test audio_config_toggle_mute_returns_new_state ... ok
948:  test sound_type_has_all_expected_variants ... ok
949:  test sound_type_is_hashable ... ok
950:  test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
951:  �[1m�[92m     Running�[0m tests/audio_events.rs (target/debug/deps/audio_events-830adb4c92f13dbe)
952:  running 9 tests
953:  test audio_config_default_is_valid ... ok
954:  test audio_config_mute_toggle_works ... ok
955:  test audio_config_volume_clamps_correctly ... ok
956:  test audio_events_are_cloneable ... ok
957:  test ball_wall_hit_event_has_correct_fields ... ok
958:  test brick_destroyed_message_has_correct_fields ... ok
959:  test graceful_degradation_app_initializes_without_audio_assets ... ok
960:  test level_completed_event_has_correct_fields ... ok
961:  test level_started_event_has_correct_fields ... ok
962:  test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
963:  �[1m�[92m     Running�[0m tests/backward_compatibility.rs (target/debug/deps/backward_compatibility-ab377533c131dc8d)
964:  running 4 tests
965:  test backward_compatibility_tests::test_backward_compatibility_contract_conversion ... ok
966:  test backward_compatibility_tests::test_backward_compatibility_make_material_no_new_textures ... ok
967:  test backward_compatibility_tests::test_backward_compatibility_no_new_textures ... ok
968:  test backward_compatibility_tests::test_backward_compatibility_partial_fields ... ok
969:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
970:  �[1m�[92m     Running�[0m tests/ball_lives.rs (target/debug/deps/ball_lives-8c3cbc243f70cef9)
971:  running 9 tests
972:  test game_over_requested_when_lives_reach_zero ... ok
973:  test life_lost_only_once_when_all_balls_gone_same_frame ... ok
974:  test life_lost_only_when_all_balls_gone_sequential ... ok
975:  test lives_can_be_reset_manually ... ok
976:  test lives_clamp_at_zero ... ok
977:  test lives_decrement_on_life_lost_event ... ok
978:  test lives_start_at_3 ... ok
979:  test multiple_events_same_frame_decrement_individually ... ok
980:  test no_game_over_requested_when_lives_above_zero ... ok
981:  test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
982:  �[1m�[92m     Running�[0m tests/ball_material_startup.rs (target/debug/deps/ball_material_startup-5849ea0c632dca1b)
983:  running 3 tests
984:  test ball_entities_spawn_with_materials ... ok
985:  test ball_receives_fallback_materials_on_startup ... ok
986:  test plugin_initializes_required_resources ... ok
987:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
988:  �[1m�[92m     Running�[0m tests/ball_spawn_bricks.rs (target/debug/deps/ball_spawn_bricks-9729d89bb6b3cdd7)
...

995:  test red_1_despawns_all_except_triggering ... ok
996:  test red_1_despawns_off_screen_balls ... ok
997:  test red_1_no_respawn_after_despawn ... ok
998:  test red_1_with_single_ball_unchanged ... ok
999:  test red_2_spawned_ball_has_inverse_velocity ... ok
1000:  test red_2_spawned_ball_persists_10_frames ... ok
1001:  test red_2_spawns_at_brick_position ... ok
1002:  test red_2_spawns_from_multiple_balls ... ok
1003:  test red_2_spawns_one_additional_ball ... ok
1004:  test red_2_uses_brick_destroyed_message ... ok
1005:  test red_3_spawned_balls_persist_10_frames ... ok
1006:  test red_3_spawns_from_multiple_balls ... ok
1007:  test red_3_spawns_once_per_destruction ... ok
1008:  test red_3_spawns_two_additional_balls ... ok
1009:  test red_3_spawns_y_shaped_pattern ... ok
1010:  test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
1011:  �[1m�[92m     Running�[0m tests/brick_42_91_life_loss.rs (target/debug/deps/brick_42_91_life_loss-e6b4463020ddcfe7)
1012:  running 13 tests
1013:  test test_brick_91_hazard_status_for_indestructibility ... ok
1014:  test test_frame_loss_state_initialization ... ok
1015:  test test_frame_loss_state_persistence_across_frames ... ok
1016:  test test_frame_loss_state_tracks_emission ... ok
1017:  test test_hazard_brick_constant_value ... ok
1018:  test test_hazard_brick_type_distinction ... ok
1019:  test test_is_hazard_brick_identifies_type_42 ... ok
1020:  test test_is_hazard_brick_identifies_type_91 ... ok
1021:  test test_level_completion_excludes_type_91 ... ok
1022:  test test_non_hazard_bricks_not_identified_as_hazard ... ok
1023:  test test_paddle_hazard_life_loss_event_emission ... ok
1024:  test test_scoring_constants_for_type_42 ... ok
1025:  test test_single_life_loss_per_frame_prevention ... ok
1026:  test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1027:  �[1m�[92m     Running�[0m tests/brick_50_level_up.rs (target/debug/deps/brick_50_level_up-41d54b1986120e48)
1028:  running 6 tests
1029:  test test_brick_50_advances_to_next_level ... ignored, RED phase test: expects level transition not yet implemented
1030:  test test_brick_50_awards_zero_points ... ignored, RED phase test: expects brick destruction scoring not yet implemented
1031:  test test_brick_50_collision_emits_brick_destroyed_message ... ignored, RED phase test: expects brick destruction message not yet implemented
1032:  test test_brick_50_collision_emits_level_switch_requested ... ignored, RED phase test: expects brick 50 collision logic not yet implemented
1033:  test test_brick_50_level_state_persists_across_frames ... ignored, RED phase test: expects multi-frame persistence not yet verified
1034:  test test_brick_50_on_final_level_shows_victory_screen ... ignored, RED phase test: expects final level boundary logic not yet implemented
1035:  test result: ok. 0 passed; 0 failed; 6 ignored; 0 measured; 0 filtered out; finished in 0.00s
1036:  �[1m�[92m     Running�[0m tests/brick_destroy_dedupe.rs (target/debug/deps/brick_destroy_dedupe-6dd7dcc37f262f08)
1037:  running 1 test
1038:  test test_duplicate_brick_destroy_emissions_are_deduped ... ok
1039:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1040:  �[1m�[92m     Running�[0m tests/brick_level_navigation.rs (target/debug/deps/brick_level_navigation-f60b1419a2bd3e2f)
1041:  running 4 tests
1042:  test brick_50_level_up_message_can_be_written ... ok
1043:  test brick_54_level_down_message_can_be_written ... ok
1044:  test level_definition_with_brick_50_loads ... ok
1045:  test level_definition_with_brick_54_loads ... ok
1046:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1047:  �[1m�[92m     Running�[0m tests/brick_regression.rs (target/debug/deps/brick_regression-6174afa334d3c55d)
1048:  running 3 tests
1049:  test indestructible_brick_90_still_spawns ... ok
1050:  test multi_hit_bricks_10_to_13_still_spawn ... ok
1051:  test simple_stone_brick_20_still_spawns ... ok
1052:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1053:  �[1m�[92m     Running�[0m tests/change_detection.rs (target/debug/deps/change_detection-978f31757e99face)
1054:  running 0 tests
1055:  test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1056:  �[1m�[92m     Running�[0m tests/cheat_mode.rs (target/debug/deps/cheat_mode-06644deb71c1c0bc)
1057:  running 4 tests
1058:  test blocked_level_switch_emits_beep_only ... ok
1059:  test n_and_p_allowed_when_cheat_active_no_beep ... ok
1060:  test r_allowed_when_cheat_active_no_beep ... ok
1061:  test toggling_cheat_removes_game_over_overlay ... ok
1062:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1063:  �[1m�[92m     Running�[0m tests/combined_textures.rs (target/debug/deps/combined_textures-70cf9f4450a7c108)
1064:  running 3 tests
1065:  test combined_textures_tests::test_combined_textures_all_five_types ... ok
1066:  test combined_textures_tests::test_combined_textures_partial_overlap ... ok
1067:  test combined_textures_tests::test_combined_textures_struct_construction ... ok
1068:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1069:  �[1m�[92m     Running�[0m tests/depth_textures.rs (target/debug/deps/depth_textures-3144fef9ed323eac)
1070:  running 3 tests
1071:  test test_depth_fallback_missing_file ... ok
1072:  test test_depth_path_deserialization_minimal ... ok
1073:  test test_depth_scale_parameter ... ok
1074:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1075:  �[1m�[92m     Running�[0m tests/direction_bricks.rs (target/debug/deps/direction_bricks-2f4525a957041439)
...

1077:  test test_all_six_directions ... ok
1078:  test test_ball_component_set ... ok
1079:  test test_brick_43_down_impulse ... ok
1080:  test test_brick_44_left_impulse ... ok
1081:  test test_brick_45_right_impulse ... ok
1082:  test test_brick_46_up_impulse ... ok
1083:  test test_brick_47_up_right_impulse ... ok
1084:  test test_brick_48_up_left_impulse ... ok
1085:  test test_brick_52_random_direction_distribution ... ok
1086:  test test_brick_52_random_magnitude_range ... ok
1087:  test test_diagonal_impulse_components ... ok
1088:  test test_impulse_component_persistence ... ok
1089:  test test_impulse_component_setting ... ok
1090:  test test_stationary_ball_receives_impulse ... ok
1091:  test test_z_axis_preserved ... ok
1092:  test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1093:  �[1m�[92m     Running�[0m tests/editor_palette.rs (target/debug/deps/editor_palette-b0da004b7accd4e3)
1094:  running 2 tests
1095:  test click_selects_palette_item_and_updates_resource ... ok
1096:  test pressing_p_opens_and_closes_palette ... ok
1097:  test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1098:  �[1m�[92m     Running�[0m tests/emissive_textures.rs (target/debug/deps/emissive_textures-2f8e5c35d0591657)
1099:  running 4 tests
1100:  test test_emissive_color_texture_combination ... ok
1101:  test test_emissive_fallback_missing_file ... ok
1102:  test test_emissive_path_deserialization_minimal ... ok
1103:  test test_emissive_texture_loading_srgb_color_space ... ok
1104:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1105:  �[1m�[92m     Running�[0m tests/extra_ball_brick.rs (target/debug/deps/extra_ball_brick-142319f12ba7f1bd)
1106:  running 3 tests
1107:  test t007_brick_41_life_clamped_to_max ... ok
1108:  test t007_brick_41_single_hit_grants_life_no_score ... ok
1109:  test t008_brick_41_multi_ball_only_one_life_award ... ok
1110:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1111:  �[1m�[92m     Running�[0m tests/extra_ball_brick_audio.rs (target/debug/deps/extra_ball_brick_audio-6dcebbc30be19070)
1112:  running 4 tests
1113:  test t014_brick_41_plays_unique_sound_once ... ok
1114:  test t014_other_bricks_use_generic_sound ... ok
1115:  test t015_brick_41_multi_ball_single_sound_with_fallback ... ok
1116:  test t015_missing_asset_fallback_to_generic ... ok
1117:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1118:  �[1m�[92m     Running�[0m tests/fallback_chain.rs (target/debug/deps/fallback_chain-193dd462bc8120a7)
1119:  running 5 tests
1120:  test fallback_chain_tests::test_fallback_chain_deserialization ... ok
1121:  test fallback_chain_tests::test_fallback_chain_empty ... ok
1122:  test fallback_chain_tests::test_fallback_chain_multi_level ... ok
1123:  test fallback_chain_tests::test_fallback_chain_with_all_new_textures ... ok
1124:  test fallback_chain_tests::test_fallback_chain_with_missing_new_textures ... ok
1125:  test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1126:  �[1m�[92m     Running�[0m tests/fallback_registry.rs (target/debug/deps/fallback_registry-3c0f7c620deab045)
1127:  running 2 tests
1128:  test fallback_handles_exist_for_all_material_classes ... ok
1129:  test log_once_warns_only_on_first_use ... ok
1130:  test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1131:  �[1m�[92m     Running�[0m tests/fallible_systems.rs (target/debug/deps/fallible_systems-b48a9b4e3fa67cef)
1132:  running 5 tests
1133:  test audio_plugin_requires_asset_server ... ok
1134:  test paddle_size_plugin_initializes ... ok
1135:  test physics_config_resources_are_inserted_by_production_plugins ... ok
1136:  test respawn_plugin_initializes_required_resources ... ok
1137:  test texture_plugin_requires_asset_server ... ok
1138:  test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1139:  �[1m�[92m     Running�[0m tests/gravity_bricks.rs (target/debug/deps/gravity_bricks-18b1b591e5b67b0f)
...

1179:  test tests::test_gravity_reset_on_ball_loss ... ok
1180:  test tests::test_gravity_reset_to_zero_gravity_fallback ... ok
1181:  test tests::test_last_gravity_wins_sequential_destruction ... ok
1182:  test tests::test_level_definition_minimal_backward_compat ... ok
1183:  test tests::test_level_definition_with_default_gravity ... ok
1184:  test tests::test_level_definition_without_default_gravity ... ok
1185:  test tests::test_multiple_gravity_bricks_sequential ... ok
1186:  test tests::test_placeholder_gravity_application ... ok
1187:  test tests::test_placeholder_gravity_brick_destruction ... ok
1188:  test tests::test_queer_gravity_no_correlation ... ok
1189:  test tests::test_queer_gravity_x_range ... ok
1190:  test tests::test_queer_gravity_y_zero ... ok
1191:  test tests::test_queer_gravity_z_range ... ok
1192:  test tests::test_rapid_multiple_brick_destruction ... ok
1193:  test tests::test_score_updated_on_gravity_brick_destruction ... ok
1194:  test result: ok. 53 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1195:  �[1m�[92m     Running�[0m tests/gravity_effect_integration.rs (target/debug/deps/gravity_effect_integration-035efa60cb166b9b)
1196:  running 1 test
1197:  test test_gravity_change_affects_ball_velocity ... ok
1198:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1199:  �[1m�[92m     Running�[0m tests/gravity_indicator_ui.rs (target/debug/deps/gravity_indicator_ui-ca73e1cd5dc87ff2)
1200:  running 14 tests
1201:  test test_gravity_level_asset_names ... ok
1202:  test test_gravity_level_equality ... ok
1203:  test test_indicator_opposite_corner_from_developer ... ignored, Integration test hangs during app.update() with MinimalPlugins + UiPlugin; unit tests pass
1204:  test test_indicator_overlay_visibility ... ignored, Integration test hangs during app.update() with MinimalPlugins + UiPlugin; unit tests pass
1205:  test test_indicator_positioning_bottom_left ... ignored, Integration test hangs during app.update() with MinimalPlugins + UiPlugin; unit tests pass
1206:  test test_map_gravity_exact_values ... ok
1207:  test test_map_gravity_infinity ... ok
1208:  test test_map_gravity_mixed_axes_highest_wins ... ok
1209:  test test_map_gravity_nan ... ok
1210:  test test_map_gravity_negative_values ... ok
1211:  test test_map_gravity_tolerance_outside ... ok
1212:  test test_map_gravity_tolerance_within ... ok
1213:  test test_map_gravity_unknown ... ok
1214:  test test_map_gravity_y_axis_ignored ... ok
1215:  test result: ok. 11 passed; 0 failed; 3 ignored; 0 measured; 0 filtered out; finished in 0.00s
1216:  �[1m�[92m     Running�[0m tests/gravity_integration_test.rs (target/debug/deps/gravity_integration_test-e5f3bc479fb69396)
1217:  running 3 tests
1218:  test test_complete_gravity_flow ... ok
1219:  test test_gravity_zero_brick ... ok
1220:  test test_sequential_gravity_changes ... ok
1221:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1222:  �[1m�[92m     Running�[0m tests/gravity_loader_idempotence.rs (target/debug/deps/gravity_loader_idempotence-299241d8e3569300)
1223:  running 1 test
1224:  test test_loader_does_not_overwrite_current_after_initial_load ... ok
1225:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1226:  �[1m�[92m     Running�[0m tests/gravity_message_sequence.rs (target/debug/deps/gravity_message_sequence-04a8c83eee6c18c8)
1227:  running 1 test
1228:  test test_gravity_message_sequence_applies_last ... ok
1229:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1230:  �[1m�[92m     Running�[0m tests/gravity_playtest_sim.rs (target/debug/deps/gravity_playtest_sim-76b51719e7d7986c)
1231:  running 1 test
1232:  test gravity_playtest_sequence_simulation ... ok
1233:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1234:  �[1m�[92m     Running�[0m tests/grid_debug.rs (target/debug/deps/grid_debug-b3f39f5a4d0cb8fc)
1235:  running 1 test
1236:  test toggle_grid_visibility_only_runs_on_config_change ... ok
1237:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1238:  �[1m�[92m     Running�[0m tests/indestructible_visual.rs (target/debug/deps/indestructible_visual-748cda86fc52f1fc)
1239:  running 1 test
1240:  test indestructible_bricks_have_material_component ... ok
1241:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1242:  �[1m�[92m     Running�[0m tests/integration_transition.rs (target/debug/deps/integration_transition-98318595486b7139)
1243:  running 4 tests
1244:  test backward_compat_warning_future ... ignored
1245:  test level_advance_state_default ... ok
1246:  test merkabas_despawn_on_level_advance ... ok
1247:  test transition_two_stage_unfreeze_future ... ignored
1248:  test result: ok. 2 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
1249:  �[1m�[92m     Running�[0m tests/level_definition.rs (target/debug/deps/level_definition-91b7f1a0d2da9935)
...

1258:  test test_description_with_special_chars ... ok
1259:  test test_empty_author_treated_as_none ... ok
1260:  test test_empty_description_treated_as_none ... ok
1261:  test test_extract_author_edge_cases ... ok
1262:  test test_extract_author_markdown_email ... ok
1263:  test test_extract_author_markdown_url ... ok
1264:  test test_extract_author_plain_text ... ok
1265:  test test_level_with_author_plain_string ... ok
1266:  test test_level_with_description_only ... ok
1267:  test test_level_with_full_metadata ... ok
1268:  test test_level_with_gravity_and_metadata ... ok
1269:  test test_level_with_only_author ... ok
1270:  test test_level_with_only_description ... ok
1271:  test test_level_without_metadata_backward_compat ... ok
1272:  test test_multiline_description ... ok
1273:  test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
1274:  �[1m�[92m     Running�[0m tests/level_label.rs (target/debug/deps/level_label-4c9d8bf6fd9e585b)
1275:  running 4 tests
1276:  test level_label_is_left_aligned ... ok
1277:  test level_label_spawns_and_updates_on_level_started ... ok
1278:  test spawn_shows_current_level_if_present ... ok
1279:  test updates_when_current_level_changes ... ok
1280:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1281:  �[1m�[92m     Running�[0m tests/level_overrides.rs (target/debug/deps/level_overrides-94c486a2f5161438)
1282:  running 6 tests
1283:  test custom_level_texture_set_overrides_profiles ... ok
1284:  test default_profiles_apply_when_no_level_override_exists ... ok
1285:  test different_levels_have_isolated_overrides ... ok
1286:  test level_presentation_resolve_material_uses_profile_or_fallback ... ok
1287:  test partial_override_uses_canonical_for_missing_fields ... ok
1288:  test tint_modifier_applies_to_presentation ... ok
1289:  test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1290:  �[1m�[92m     Running�[0m tests/level_switcher.rs (target/debug/deps/level_switcher-e9bc9f61f3e93380)
1291:  running 2 tests
1292:  test clear_bk_level_after_switcher_test ... ok
1293:  test key_l_cycles_levels_and_wraps_with_spawn_resets ... ok
1294:  test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.19s
1295:  �[1m�[92m     Running�[0m tests/merkaba_audio.rs (target/debug/deps/merkaba_audio-f36919eccfbc66a2)
1296:  running 1 test
1297:  test t030b_helicopter_loop_lifecycle_start_stop_idempotent ... ok
1298:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1299:  �[1m�[92m     Running�[0m tests/merkaba_goal.rs (target/debug/deps/merkaba_goal-511d2e4511b94efd)
1300:  running 1 test
1301:  test t022_merkaba_despawns_on_goal_contact ... ok
1302:  test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1303:  �[1m�[92m     Running�[0m tests/merkaba_paddle.rs (target/debug/deps/merkaba_paddle-0b3cdb7f14e250e1)
1304:  running 2 tests
1305:  test t029_paddle_contact_triggers_life_loss_and_sound ... ok
1306:  test t030_paddle_contact_despawns_balls_and_merkabas ... ok
1307:  test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1308:  �[1m�[92m     Running�[0m tests/merkaba_physics.rs (target/debug/deps/merkaba_physics-a2212ffee8ff8713)
1309:  running 5 tests
1310:  test t019_wall_bounce_with_distinct_sound ... ok
1311:  test t020_brick_bounce_no_destruction_with_distinct_sound ... ok
1312:  test t021_minimum_z_speed_clamped_to_3_0 ... ok
1313:  test t022b_multiple_merkabas_coexist_60fps_baseline ... ok
1314:  test t022c_merkaba_y_plane_constrained_to_tolerance ... ok
1315:  test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1316:  �[1m�[92m     Running�[0m tests/merkaba_spawn.rs (target/debug/deps/merkaba_spawn-033702152c87a197)
1317:  running 3 tests
1318:  test t010_spawn_message_emitted_on_rotor_brick_hit ... ok
1319:  test t011_merkaba_spawned_after_0_5s_with_dual_tetrahedron ... ok
1320:  test t012b_rotor_brick_destroyed_on_collision ... ok
1321:  test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1322:  �[1m�[92m     Running�[0m tests/message_boundaries.rs (target/debug/deps/message_boundaries-a81b4fbd76c8a1f3)
1323:  running 5 tests
1324:  test audio_system_reads_ui_beep_messages ... ok
1325:  test brick_destroyed_is_message_not_event ... ok
1326:  test no_observer_path_for_brick_destroyed ... ok
1327:  test no_observer_path_for_ui_beep ... ok
1328:  test ui_beep_is_message_not_event ... ok
1329:  test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1330:  �[1m�[92m     Running�[0m tests/multi_frame_persistence.rs (target/debug/deps/multi_frame_persistence-21dff7590d85f8a2)
1331:  running 4 tests
1332:  test gravity_change_persists_with_loader_running ... ok
1333:  test gravity_change_persists_without_loader ... ok
1334:  test level_transition_resets_gravity_then_persists ... ok
1335:  test sequential_gravity_changes_persist ... ok
1336:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1337:  �[1m�[92m     Running�[0m tests/multi_hit_bricks.rs (target/debug/deps/multi_hit_bricks-c680bae0c05e9251)
1338:  running 7 tests
1339:  test is_multi_hit_brick_returns_false_for_other_indices ... ok
1340:  test is_multi_hit_brick_returns_true_for_indices_10_to_13 ... ok
1341:  test multi_hit_brick_10_transitions_to_20 ... ok
1342:  test multi_hit_brick_13_transitions_to_12 ... ok
1343:  test multi_hit_brick_full_lifecycle ... ok
1344:  test multi_hit_brick_spawns_with_correct_type_id ... ok
1345:  test multi_hit_bricks_have_counts_towards_completion ... ok
1346:  test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1347:  �[1m�[92m     Running�[0m tests/orm_textures.rs (target/debug/deps/orm_textures-79f3fec0a1bd0a65)
1348:  running 4 tests
1349:  test test_orm_fallback_missing_file ... ok
1350:  test test_orm_path_deserialization_minimal ... ok
1351:  test test_orm_scalar_multiplier ... ok
1352:  test test_orm_texture_loading_linear_color_space ... ok
1353:  test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1354:  �[1m�[92m     Running�[0m tests/paddle_destroyable_brick.rs (target/debug/deps/paddle_destroyable_brick-f80e095f3e4fb8ba)
1355:  running 6 tests
1356:  test ball_collision_does_not_destroy_type_57_brick ... ok
1357:  test ball_collision_no_brick_destroyed_message ... ok
1358:  test is_paddle_destroyable_brick_returns_false_for_other_types ... ok
1359:  test is_paddle_destroyable_brick_returns_true_for_type_57 ... ok
1360:  test paddle_collision_destroys_type_57_brick ... ok
1361:  test paddle_hazard_collision_integration_setup ... ok
1362:  test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1363:  �[1m�[92m     Running�[0m tests/paddle_shrink.rs (target/debug/deps/paddle_shrink-a4abf470473dfb52)
1364:  running 8 tests
1365:  test input_locked_during_shrink ... ok
1366:  test paddle_remains_visible_during_shrink ... ok
1367:  test paddle_shrinks_on_ball_loss ... ok
1368:  test rapid_consecutive_losses_handled ... ok
1369:  test shrink_component_configuration ... ok
1370:  test shrink_duration_matches_respawn_delay ... ok
1371:  test shrink_interrupts_growth_animation ... ok
1372:  test shrink_reaches_minimum_scale ... ok
1373:  test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
1374:  �[1m�[92m     Running�[0m tests/paddle_shrink_multiball.rs (target/debug/deps/paddle_shrink_multiball-b9a8a992a10d7caa)
1375:  running 4 tests
1376:  test no_visual_effects_when_multiple_balls_in_play ... FAILED
1377:  test paddle_does_not_shrink_when_multiple_balls_in_play ... FAILED
1378:  test paddle_shrinks_when_last_ball_is_lost ... FAILED
1379:  test paddle_shrinks_when_single_ball_lost_on_last_life ... ok
1380:  failures:
1381:  ---- no_visual_effects_when_multiple_balls_in_play stdout ----
1382:  thread 'no_visual_effects_when_multiple_balls_in_play' (80381) panicked at tests/paddle_shrink_multiball.rs:209:5:
1383:  Paddle should NOT shrink when other balls are still in play
1384:  note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
1385:  ---- paddle_does_not_shrink_when_multiple_balls_in_play stdout ----
1386:  thread 'paddle_does_not_shrink_when_multiple_balls_in_play' (80386) panicked at tests/paddle_shrink_multiball.rs:105:5:
1387:  Paddle should NOT shrink when other balls are still in play
1388:  ---- paddle_shrinks_when_last_ball_is_lost stdout ----
1389:  thread 'paddle_shrinks_when_last_ball_is_lost' (80387) panicked at tests/paddle_shrink_multiball.rs:128:5:
1390:  Paddle should not shrink when lives > 1 (respawn will happen)
1391:  failures:
1392:  no_visual_effects_when_multiple_balls_in_play
1393:  paddle_does_not_shrink_when_multiple_balls_in_play
1394:  paddle_shrinks_when_last_ball_is_lost
1395:  test result: FAILED. 1 passed; 3 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
1396:  �[1m�[91merror�[0m: test failed, to rerun pass `--test paddle_shrink_multiball`
1397:  ##[error]Process completed with exit code 101.
1398:  Post job cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Levels 🆙 Issue requests new level(s) or PR implements level, or other level related work Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Direction Bricks

2 participants