diff --git a/CLAUDE.md b/CLAUDE.md index 0892bd6..a3be179 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,87 +1,88 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -Console Jack is a console-based, text-graphics implementation of the classic casino game, aiming to be a "Card RPG" where players progress through casino ranks. Built using Java 21 with Lanterna for terminal UI rendering. - -## Development Commands - -### Build and Run - -- `mvn compile` - Compile the project -- `mvn exec:java` - Run the application directly -- `mvn clean package` - Build JAR with dependencies -- `java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar` - Run the packaged JAR - -### Platform-Specific Packaging - -- `mvn clean package -Pwindows` - Package for Windows (creates `build-win/`) -- `mvn clean package -Pmac` - Package for macOS (creates `build-mac/`) -- `mvn clean package -Plinux` - Package for Linux (creates `build-linux/`) - -### Code Quality - -- Qodana static analysis configured in `qodana.yaml` (uses `jetbrains/qodana-jvm-community:2025.1`) - -## Architecture - -### Core Design Pattern - -The application uses the **enum singleton pattern** (Effective Java, Item 3) for core subsystems. Access subsystems via: - -- `RenderSubsystem.INSTANCE` -- `InputSubsystem.INSTANCE` -- `AudioSubsystem.INSTANCE` -- `MasterSubsystem.INSTANCE` -- `StateMachineSubsystem.INSTANCE` - -### Threading Model - -- **Master Thread**: Runs the main game loop at 8 UPS (125ms per update) -- **Render Thread**: Handles terminal UI rendering using Lanterna -- **Input Thread**: Processes keyboard/input events -- **Audio Thread**: Manages audio subsystem - -### State Management - -- State machine managed by `StateMachineManager` (static facade) -- States implement `LoopableState` interface -- Initial state: `MainMenuState` -- State transitions via push/pop/replace operations - -### ECS (Entity Component System) - -- `EntityPool`: Manages game entities -- Components: `Position`, `Visual`, `Card`, `CardArt`, `CardSprite` -- Systems: `DisplayListSystem` for rendering -- ECS updates run in the main game loop - -### Package Structure - -- `net.luxsolari.engine.*`: Core game engine - - `ecs/`: Entity-Component-System implementation - - `manager/`: Static utility managers (Input, Audio, Render, StateMachine) - - `systems/`: Subsystem interfaces and implementations - - `states/`: Base state interfaces -- `net.luxsolari.game.*`: Game-specific implementation - - `states/`: Concrete game states (MainMenu, Gameplay, Pause) - - `ecs/`: Game-specific components - -### Key Technologies - -- **Lanterna 3.1.2**: Terminal/console UI framework -- **Java 21**: Language version with preview features enabled -- **Maven**: Build system with multi-platform packaging profiles - -## Entry Point - -Main class: `net.luxsolari.game.Main` - Initializes logging and starts `MasterSubsystem.INSTANCE` - -- Enter architect mode when commanded with either "Enter Architect Mode" or "/architect-mode". Use docs/ARCHITECT_MODE.md ruleset. -- Enter RIPER mode when commanded with either "Enter RIPER Mode" or "/riper-mode". Use docs/RIPER_MODE.md ruleset. - -## Project Memories -- Always refer to main docs inside the @docs/ directory and base your work on them. -- Always check documentation is aligned with changes, refactors or modifications you made to the code. If documentation gaps exist, update relevant docs or create new where appropiate. Make sure all documentation for the projects lives under the @docs/ directory. +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Console Jack is a console-based, text-graphics implementation of the classic casino game, aiming to be a "Card RPG" where players progress through casino ranks. Built using Java 21 with Lanterna for terminal UI rendering. + +## Development Commands + +### Build and Run + +- `mvn compile` - Compile the project +- `mvn exec:java` - Run the application directly +- `mvn clean package` - Build JAR with dependencies +- `java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar` - Run the packaged JAR + +### Platform-Specific Packaging + +- `mvn clean package -Pwindows` - Package for Windows (creates `build-win/`) +- `mvn clean package -Pmac` - Package for macOS (creates `build-mac/`) +- `mvn clean package -Plinux` - Package for Linux (creates `build-linux/`) + +### Code Quality + +- Qodana static analysis configured in `qodana.yaml` (uses `jetbrains/qodana-jvm-community:2025.1`) + +## Architecture + +### Core Design Pattern + +The application uses the **enum singleton pattern** (Effective Java, Item 3) for core subsystems. Access subsystems via: + +- `RenderSubsystem.INSTANCE` +- `InputSubsystem.INSTANCE` +- `AudioSubsystem.INSTANCE` +- `MasterSubsystem.INSTANCE` +- `StateMachineSubsystem.INSTANCE` + +### Threading Model + +- **Master Thread**: Runs the main game loop at 8 UPS (125ms per update) +- **Render Thread**: Handles terminal UI rendering using Lanterna +- **Input Thread**: Processes keyboard/input events +- **Audio Thread**: Manages audio subsystem + +### State Management + +- State machine managed by `StateMachineManager` (static facade) +- States implement `LoopableState` interface +- Initial state: `MainMenuState` +- State transitions via push/pop/replace operations + +### ECS (Entity Component System) + +- `EntityPool`: Manages game entities +- Components: `Position`, `Visual`, `Card`, `CardArt`, `CardSprite` +- Systems: `DisplayListSystem` for rendering +- ECS updates run in the main game loop + +### Package Structure + +- `net.luxsolari.engine.*`: Core game engine + - `ecs/`: Entity-Component-System implementation + - `manager/`: Static utility managers (Input, Audio, Render, StateMachine) + - `systems/`: Subsystem interfaces and implementations + - `states/`: Base state interfaces +- `net.luxsolari.game.*`: Game-specific implementation + - `states/`: Concrete game states (MainMenu, Gameplay, Pause) + - `ecs/`: Game-specific components + +### Key Technologies + +- **Lanterna 3.1.2**: Terminal/console UI framework +- **Java 21**: Language version with preview features enabled +- **Maven**: Build system with multi-platform packaging profiles + +## Entry Point + +Main class: `net.luxsolari.game.Main` - Initializes logging and starts `MasterSubsystem.INSTANCE` + +- Enter architect mode when commanded with either "Enter Architect Mode" or "/architect-mode". Use docs/ARCHITECT_MODE.md ruleset. +- Enter RIPER mode when commanded with either "Enter RIPER Mode" or "/riper-mode". Use docs/RIPER_MODE.md ruleset. + +## Project Memories +- Always refer to main docs inside the @docs/ directory and base your work on them. +- Always check documentation is aligned with changes, refactors or modifications you made to the code. If documentation gaps exist, update relevant docs or create new where appropiate. Make sure all documentation for the projects lives under the @docs/ directory. +- When making architectural changes or major refactors, always review your work to ensure the documentation is aligned with the codebase. \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index df99467..4f1a724 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,264 +1,264 @@ -# Console Jack - Architecture Documentation - -## Overview - -Console Jack is built using a multi-threaded, subsystem-based architecture with an Entity Component System (ECS) for game logic and a custom UI framework for terminal rendering. - -## Core Design Principles - -### 1. Enum Singleton Pattern -All core subsystems use the enum singleton pattern (Effective Java, Item 3) for JVM-wide singleton enforcement: - -```java -public enum MasterSubsystem implements Subsystem { - INSTANCE; - // implementation -} -``` - -**Benefits:** -- Thread-safe by default -- Serialization-safe -- Reflection-proof -- Memory efficient - -### 2. Thread Separation -Each major concern runs in its own thread to prevent blocking: - -- **Master Thread**: Game loop and ECS updates -- **Render Thread**: Terminal UI rendering -- **Input Thread**: Keyboard event processing -- **Audio Thread**: Sound management - -## Architecture Layers - -### Layer 1: Engine Core (`net.luxsolari.engine`) - -#### Subsystems (`systems/internal/`) -Core subsystems that form the foundation: - -- **`MasterSubsystem`**: Orchestrates the main game loop at 8 UPS -- **`RenderSubsystem`**: Manages Lanterna terminal rendering -- **`InputSubsystem`**: Handles keyboard input events -- **`AudioSubsystem`**: Manages sound effects and background music -- **`StateMachineSubsystem`**: Coordinates game state transitions - -#### Managers (`manager/`) -Static facades providing centralized access: - -- **`StateMachineManager`**: State push/pop/replace operations -- **`RenderManager`**: Rendering commands and utilities -- **`InputManager`**: Input event distribution -- **`AudioManager`**: Sound playback control - -#### ECS Framework (`ecs/`) -Entity Component System implementation: - -``` -EntityPool -├── Entity (ID-based) -├── Component (data containers) -│ ├── Position -│ ├── Visual -│ └── Layer -└── EcsSystem (logic processors) - └── DisplayListSystem -``` - -#### UI Framework (`ui/`) -Custom terminal UI components: - -``` -UIComponent (base) -├── UIWidget (single components) -│ ├── Label -│ └── MenuItem -└── UIContainer (composite components) - └── Menu -``` - -**Key Interfaces:** -- **`Focusable`**: Components that can receive input focus -- **`InputHandler`**: Components that process input events - -### Layer 2: Game Implementation (`net.luxsolari.game`) - -#### Game States (`states/`) -Concrete implementations of game screens: - -- **`MainMenuState`**: Main menu with navigation -- **`GameplayState`**: Blackjack game logic -- **`PauseState`**: Pause menu overlay - -#### Game Components (`ecs/`) -Blackjack-specific ECS components: - -- **`Card`**: Playing card data -- **`CardArt`**: Visual representation -- **`CardSprite`**: Rendering information - -## Threading Model - -### Master Thread (8 UPS) -```java -while (running) { - long startTime = System.nanoTime(); - - // Update ECS systems - for (EcsSystem system : ecsSystems) { - system.update(entityPool); - } - - // Update current game state - StateMachineManager.getCurrentState().update(); - - // Sleep to maintain target UPS - sleepForTargetUps(startTime); -} -``` - -### Thread Communication -- **Lock-free queues** for inter-thread communication -- **Immutable records** for data transfer (`RenderCmd`, `ZLayerData`) -- **Volatile flags** for state coordination - -## State Management - -### State Machine Pattern -``` -StateMachineManager -├── State Stack (LIFO) -├── Push State (overlay) -├── Pop State (return) -└── Replace State (transition) -``` - -### State Lifecycle -```java -interface LoopableState { - void onEnter(); // Initialize state - void update(); // Per-frame logic - void onExit(); // Cleanup -} -``` - -## ECS Architecture - -### Entity Management -- **Entities**: Unique integer IDs -- **Component Storage**: Type-indexed maps -- **System Processing**: Component iteration - -### Component Design -```java -// Data-only components -public record Position(int x, int y) implements Component {} -public record Visual(String text, TextColor color) implements Component {} -``` - -### System Processing -```java -public class DisplayListSystem implements EcsSystem { - @Override - public void update(EntityPool entityPool) { - // Query entities with Position + Visual - // Generate render commands - // Submit to render subsystem - } -} -``` - -## Rendering Pipeline - -### Z-Layer System -```java -public enum ZLayer { - BACKGROUND(0), - GAME_OBJECTS(100), - UI_BACKGROUND(200), - UI_FOREGROUND(300), - DEBUG_OVERLAY(400); -} -``` - -### Render Command Flow -``` -ECS Systems → RenderCmd → RenderSubsystem → Lanterna → Terminal -``` - -## Resource Management - -### Subsystem Lifecycle -```java -interface Subsystem { - void init() throws ResourceInitializationException; - void start(); - void stop(); - void cleanUp() throws ResourceCleanupException; -} -``` - -### Asset Loading -- **Fonts**: TTF files in `resources/fonts/` -- **Audio**: WAV files in `resources/audio/` -- **Configurations**: Properties files in `resources/` - -## Error Handling Strategy - -### Exception Hierarchy -``` -RuntimeException -├── ResourceInitializationException -└── ResourceCleanupException -``` - -### Error Recovery -- **Graceful degradation** for non-critical failures -- **Logging** at appropriate levels -- **Resource cleanup** in finally blocks - -## Performance Considerations - -### Hot Paths -- **Game loop**: 8 UPS target (125ms budget) -- **Render loop**: 60 FPS target when possible -- **ECS queries**: Optimized for component iteration - -### Memory Management -- **Object pooling** for frequently created objects -- **Immutable records** to reduce garbage collection -- **Primitive collections** where appropriate - -## Extension Points - -### Adding New Components -1. Create record implementing `Component` -2. Add to relevant entities in `EntityPool` -3. Create system to process the component - -### Adding New States -1. Implement `LoopableState` -2. Add to game package -3. Register transitions in existing states - -### Adding New UI Components -1. Extend `UIWidget` or `UIContainer` -2. Implement `Focusable` if interactive -3. Handle input in `handleInput()` method - -## Testing Strategy - -### Unit Testing -- **Component logic**: Isolated component behavior -- **System logic**: ECS system processing -- **State logic**: State transition behavior - -### Integration Testing -- **Subsystem coordination**: Thread interaction -- **State machine**: State transition flows -- **UI framework**: Component interaction - -### Performance Testing -- **Game loop timing**: UPS consistency -- **Memory usage**: Garbage collection impact +# Console Jack - Architecture Documentation + +## Overview + +Console Jack is built using a multi-threaded, subsystem-based architecture with an Entity Component System (ECS) for game logic and a custom UI framework for terminal rendering. + +## Core Design Principles + +### 1. Enum Singleton Pattern +All core subsystems use the enum singleton pattern (Effective Java, Item 3) for JVM-wide singleton enforcement: + +```java +public enum MasterSubsystem implements Subsystem { + INSTANCE; + // implementation +} +``` + +**Benefits:** +- Thread-safe by default +- Serialization-safe +- Reflection-proof +- Memory efficient + +### 2. Thread Separation +Each major concern runs in its own thread to prevent blocking: + +- **Master Thread**: Game loop and ECS updates +- **Render Thread**: Terminal UI rendering +- **Input Thread**: Keyboard event processing +- **Audio Thread**: Sound management + +## Architecture Layers + +### Layer 1: Engine Core (`net.luxsolari.engine`) + +#### Subsystems (`systems/internal/`) +Core subsystems that form the foundation: + +- **`MasterSubsystem`**: Orchestrates the main game loop at 8 UPS +- **`RenderSubsystem`**: Manages Lanterna terminal rendering +- **`InputSubsystem`**: Handles keyboard input events +- **`AudioSubsystem`**: Manages sound effects and background music +- **`StateMachineSubsystem`**: Coordinates game state transitions + +#### Managers (`manager/`) +Static facades providing centralized access: + +- **`StateMachineManager`**: State push/pop/replace operations +- **`RenderManager`**: Rendering commands and utilities +- **`InputManager`**: Input event distribution +- **`AudioManager`**: Sound playback control + +#### ECS Framework (`ecs/`) +Entity Component System implementation: + +``` +EntityPool +├── Entity (ID-based) +├── Component (data containers) +│ ├── Position +│ ├── Visual +│ └── Layer +└── EcsSystem (logic processors) + └── DisplayListSystem +``` + +#### UI Framework (`ui/`) +Custom terminal UI components: + +``` +UIComponent (base) +├── UIWidget (single components) +│ ├── Label +│ └── MenuItem +└── UIContainer (composite components) + └── Menu +``` + +**Key Interfaces:** +- **`Focusable`**: Components that can receive input focus +- **`InputHandler`**: Components that process input events + +### Layer 2: Game Implementation (`net.luxsolari.game`) + +#### Game States (`states/`) +Concrete implementations of game screens: + +- **`MainMenuState`**: Main menu with navigation +- **`GameplayState`**: Blackjack game logic +- **`PauseState`**: Pause menu overlay + +#### Game Components (`ecs/`) +Blackjack-specific ECS components: + +- **`Card`**: Playing card data +- **`CardArt`**: Visual representation +- **`CardSprite`**: Rendering information + +## Threading Model + +### Master Thread (8 UPS) +```java +while (running) { + long startTime = System.nanoTime(); + + // Update ECS systems + for (EcsSystem system : ecsSystems) { + system.update(entityPool); + } + + // Update current game state + StateMachineManager.getCurrentState().update(); + + // Sleep to maintain target UPS + sleepForTargetUps(startTime); +} +``` + +### Thread Communication +- **Lock-free queues** for inter-thread communication +- **Immutable records** for data transfer (`RenderCmd`, `ZLayerData`) +- **Volatile flags** for state coordination + +## State Management + +### State Machine Pattern +``` +StateMachineManager +├── State Stack (LIFO) +├── Push State (overlay) +├── Pop State (return) +└── Replace State (transition) +``` + +### State Lifecycle +```java +interface LoopableState { + void onEnter(); // Initialize state + void update(); // Per-frame logic + void onExit(); // Cleanup +} +``` + +## ECS Architecture + +### Entity Management +- **Entities**: Unique integer IDs +- **Component Storage**: Type-indexed maps +- **System Processing**: Component iteration + +### Component Design +```java +// Data-only components +public record Position(int x, int y) implements Component {} +public record Visual(String text, TextColor color) implements Component {} +``` + +### System Processing +```java +public class DisplayListSystem implements EcsSystem { + @Override + public void update(EntityPool entityPool) { + // Query entities with Position + Visual + // Generate render commands + // Submit to render subsystem + } +} +``` + +## Rendering Pipeline + +### Z-Layer System +```java +public enum ZLayer { + BACKGROUND(0), + GAME_OBJECTS(100), + UI_BACKGROUND(200), + UI_FOREGROUND(300), + DEBUG_OVERLAY(400); +} +``` + +### Render Command Flow +``` +ECS Systems → RenderCmd → RenderSubsystem → Lanterna → Terminal +``` + +## Resource Management + +### Subsystem Lifecycle +```java +interface Subsystem { + void init() throws ResourceInitializationException; + void start(); + void stop(); + void cleanUp() throws ResourceCleanupException; +} +``` + +### Asset Loading +- **Fonts**: TTF files in `resources/fonts/` +- **Audio**: WAV files in `resources/audio/` +- **Configurations**: Properties files in `resources/` + +## Error Handling Strategy + +### Exception Hierarchy +``` +RuntimeException +├── ResourceInitializationException +└── ResourceCleanupException +``` + +### Error Recovery +- **Graceful degradation** for non-critical failures +- **Logging** at appropriate levels +- **Resource cleanup** in finally blocks + +## Performance Considerations + +### Hot Paths +- **Game loop**: 8 UPS target (125ms budget) +- **Render loop**: 60 FPS target when possible +- **ECS queries**: Optimized for component iteration + +### Memory Management +- **Object pooling** for frequently created objects +- **Immutable records** to reduce garbage collection +- **Primitive collections** where appropriate + +## Extension Points + +### Adding New Components +1. Create record implementing `Component` +2. Add to relevant entities in `EntityPool` +3. Create system to process the component + +### Adding New States +1. Implement `LoopableState` +2. Add to game package +3. Register transitions in existing states + +### Adding New UI Components +1. Extend `UIWidget` or `UIContainer` +2. Implement `Focusable` if interactive +3. Handle input in `handleInput()` method + +## Testing Strategy + +### Unit Testing +- **Component logic**: Isolated component behavior +- **System logic**: ECS system processing +- **State logic**: State transition behavior + +### Integration Testing +- **Subsystem coordination**: Thread interaction +- **State machine**: State transition flows +- **UI framework**: Component interaction + +### Performance Testing +- **Game loop timing**: UPS consistency +- **Memory usage**: Garbage collection impact - **Thread contention**: Lock-free communication \ No newline at end of file diff --git a/docs/ARCHITECT_MODE.md b/docs/ARCHITECT_MODE.md index d40d97d..35ee201 100644 --- a/docs/ARCHITECT_MODE.md +++ b/docs/ARCHITECT_MODE.md @@ -1,134 +1,134 @@ -# Architect Mode for Claude Code - -## Your Role - -You are a senior software architect with extensive experience designing scalable, maintainable systems. Your purpose is to thoroughly analyze requirements and design optimal solutions before any implementation begins. You must resist the urge to immediately write code and instead focus on comprehensive planning and architecture design using Claude Code's console-based tools. - -## Your Behavior Rules - -- You must thoroughly understand requirements before proposing solutions -- You must reach 90% confidence in your understanding before suggesting implementation -- You must identify and resolve ambiguities through targeted questions -- You must document all assumptions clearly -- You must use TodoWrite to track progress through all phases -- You must leverage Claude Code's file analysis capabilities extensively - -## Process You Must Follow - -### Phase 1: Requirements Analysis - -1. Create TodoWrite list tracking all 5 phases of architectural analysis -2. Mark Phase 1 as in_progress -3. Carefully read all provided information about the project or feature -4. Extract and list all functional requirements explicitly stated -5. Identify implied requirements not directly stated -6. Determine non-functional requirements including: - - Performance expectations - - Security requirements - - Scalability needs - - Maintenance considerations -7. Ask clarifying questions about any ambiguous requirements -8. Report your current understanding confidence (0-100%) -9. Mark Phase 1 as completed when confidence > 70% - -### Phase 2: System Context Examination - -1. Mark Phase 2 as in_progress in TodoWrite -2. Use Glob and Grep tools to examine codebase structure: - - Search for existing patterns and architectural decisions - - Identify key interfaces and integration points - - Map out current component relationships -3. Use Read tool to examine critical files and understand existing architecture -4. Use Task tool with general-purpose agent for complex codebase analysis if needed -5. Identify all external systems that will interact with this feature -6. Define clear system boundaries and responsibilities -7. Create high-level system context in markdown format -8. Update your understanding confidence percentage -9. Mark Phase 2 as completed when analysis is thorough - -### Phase 3: Architecture Design - -1. Mark Phase 3 as in_progress in TodoWrite -2. Propose 2-3 potential architecture patterns that could satisfy requirements -3. For each pattern, explain: - - Why it's appropriate for these requirements - - How it fits with existing codebase patterns (reference specific files/classes) - - Key advantages in this specific context - - Potential drawbacks or challenges -4. Recommend the optimal architecture pattern with justification -5. Define core components needed, with clear responsibilities for each -6. Design all necessary interfaces between components -7. If applicable, design database schema or data structures -8. Address cross-cutting concerns including: - - Authentication/authorization approach - - Error handling strategy - - Logging and monitoring - - Security considerations -9. Reference existing codebase patterns and conventions -10. Update your understanding confidence percentage -11. Mark Phase 3 as completed when design is comprehensive - -### Phase 4: Technical Specification - -1. Mark Phase 4 as in_progress in TodoWrite -2. Recommend specific technologies for implementation, with justification based on existing stack -3. Break down implementation into distinct TodoWrite tasks with dependencies -4. Identify technical risks and propose mitigation strategies -5. Create detailed component specifications including: - - API contracts (referencing existing patterns in codebase) - - Data formats - - State management approach - - Validation rules -6. Define technical success criteria for the implementation -7. Create preliminary file structure showing where new code will live -8. Update your understanding confidence percentage -9. Mark Phase 4 as completed when specification is detailed - -### Phase 5: Transition Decision - -1. Mark Phase 5 as in_progress in TodoWrite -2. Summarize your architectural recommendation concisely -3. Present implementation roadmap with phases as TodoWrite tasks -4. Reference specific files and classes that will be modified/created -5. State your final confidence level in the solution -6. If confidence ≥ 90%: - - State: "**ARCHITECT MODE COMPLETE** - I'm ready to implement! Exit Architect Mode and proceed with implementation." - - Present final TodoWrite implementation task list -7. If confidence < 90%: - - List specific areas requiring clarification - - Ask targeted questions to resolve remaining uncertainties - - State: "**ARCHITECT MODE INCOMPLETE** - I need additional information before we start coding." -8. Mark Phase 5 as completed - -## Console-Specific Response Format - -Always structure your responses in this order: -1. **Phase**: Current phase you're working on -2. **Progress**: TodoWrite status update -3. **Findings**: Deliverables for current phase -4. **Confidence**: Current confidence percentage (0-100%) -5. **Questions**: To resolve ambiguities (if any) -6. **Next Steps**: What happens next - -## Tool Usage Guidelines - -- **TodoWrite**: ALWAYS use to track phase progress and implementation tasks -- **Glob/Grep**: Use extensively to understand existing codebase patterns -- **Read**: Use to examine key files and understand current architecture -- **Task**: Use general-purpose agent for complex analysis requiring multiple search rounds -- **Bash**: Use for running existing build/test commands to understand current setup - -## Activation - -When user says "**Enter Architect Mode**" or "**Switch to Architect Mode**": -1. Immediately create initial TodoWrite with all 5 phases -2. Begin Phase 1: Requirements Analysis -3. State: "**ARCHITECT MODE ACTIVATED** - Beginning comprehensive requirements analysis..." - -## Exit Conditions - -- **Successful**: Confidence ≥ 90% → "**ARCHITECT MODE COMPLETE**" -- **Incomplete**: Confidence < 90% → "**ARCHITECT MODE INCOMPLETE**" -- **User Override**: User says "**Exit Architect Mode**" → Switch back to normal mode - +# Architect Mode for Claude Code + +## Your Role + +You are a senior software architect with extensive experience designing scalable, maintainable systems. Your purpose is to thoroughly analyze requirements and design optimal solutions before any implementation begins. You must resist the urge to immediately write code and instead focus on comprehensive planning and architecture design using Claude Code's console-based tools. + +## Your Behavior Rules + +- You must thoroughly understand requirements before proposing solutions +- You must reach 90% confidence in your understanding before suggesting implementation +- You must identify and resolve ambiguities through targeted questions +- You must document all assumptions clearly +- You must use TodoWrite to track progress through all phases +- You must leverage Claude Code's file analysis capabilities extensively + +## Process You Must Follow + +### Phase 1: Requirements Analysis + +1. Create TodoWrite list tracking all 5 phases of architectural analysis +2. Mark Phase 1 as in_progress +3. Carefully read all provided information about the project or feature +4. Extract and list all functional requirements explicitly stated +5. Identify implied requirements not directly stated +6. Determine non-functional requirements including: + - Performance expectations + - Security requirements + - Scalability needs + - Maintenance considerations +7. Ask clarifying questions about any ambiguous requirements +8. Report your current understanding confidence (0-100%) +9. Mark Phase 1 as completed when confidence > 70% + +### Phase 2: System Context Examination + +1. Mark Phase 2 as in_progress in TodoWrite +2. Use Glob and Grep tools to examine codebase structure: + - Search for existing patterns and architectural decisions + - Identify key interfaces and integration points + - Map out current component relationships +3. Use Read tool to examine critical files and understand existing architecture +4. Use Task tool with general-purpose agent for complex codebase analysis if needed +5. Identify all external systems that will interact with this feature +6. Define clear system boundaries and responsibilities +7. Create high-level system context in markdown format +8. Update your understanding confidence percentage +9. Mark Phase 2 as completed when analysis is thorough + +### Phase 3: Architecture Design + +1. Mark Phase 3 as in_progress in TodoWrite +2. Propose 2-3 potential architecture patterns that could satisfy requirements +3. For each pattern, explain: + - Why it's appropriate for these requirements + - How it fits with existing codebase patterns (reference specific files/classes) + - Key advantages in this specific context + - Potential drawbacks or challenges +4. Recommend the optimal architecture pattern with justification +5. Define core components needed, with clear responsibilities for each +6. Design all necessary interfaces between components +7. If applicable, design database schema or data structures +8. Address cross-cutting concerns including: + - Authentication/authorization approach + - Error handling strategy + - Logging and monitoring + - Security considerations +9. Reference existing codebase patterns and conventions +10. Update your understanding confidence percentage +11. Mark Phase 3 as completed when design is comprehensive + +### Phase 4: Technical Specification + +1. Mark Phase 4 as in_progress in TodoWrite +2. Recommend specific technologies for implementation, with justification based on existing stack +3. Break down implementation into distinct TodoWrite tasks with dependencies +4. Identify technical risks and propose mitigation strategies +5. Create detailed component specifications including: + - API contracts (referencing existing patterns in codebase) + - Data formats + - State management approach + - Validation rules +6. Define technical success criteria for the implementation +7. Create preliminary file structure showing where new code will live +8. Update your understanding confidence percentage +9. Mark Phase 4 as completed when specification is detailed + +### Phase 5: Transition Decision + +1. Mark Phase 5 as in_progress in TodoWrite +2. Summarize your architectural recommendation concisely +3. Present implementation roadmap with phases as TodoWrite tasks +4. Reference specific files and classes that will be modified/created +5. State your final confidence level in the solution +6. If confidence ≥ 90%: + - State: "**ARCHITECT MODE COMPLETE** - I'm ready to implement! Exit Architect Mode and proceed with implementation." + - Present final TodoWrite implementation task list +7. If confidence < 90%: + - List specific areas requiring clarification + - Ask targeted questions to resolve remaining uncertainties + - State: "**ARCHITECT MODE INCOMPLETE** - I need additional information before we start coding." +8. Mark Phase 5 as completed + +## Console-Specific Response Format + +Always structure your responses in this order: +1. **Phase**: Current phase you're working on +2. **Progress**: TodoWrite status update +3. **Findings**: Deliverables for current phase +4. **Confidence**: Current confidence percentage (0-100%) +5. **Questions**: To resolve ambiguities (if any) +6. **Next Steps**: What happens next + +## Tool Usage Guidelines + +- **TodoWrite**: ALWAYS use to track phase progress and implementation tasks +- **Glob/Grep**: Use extensively to understand existing codebase patterns +- **Read**: Use to examine key files and understand current architecture +- **Task**: Use general-purpose agent for complex analysis requiring multiple search rounds +- **Bash**: Use for running existing build/test commands to understand current setup + +## Activation + +When user says "**Enter Architect Mode**" or "**Switch to Architect Mode**": +1. Immediately create initial TodoWrite with all 5 phases +2. Begin Phase 1: Requirements Analysis +3. State: "**ARCHITECT MODE ACTIVATED** - Beginning comprehensive requirements analysis..." + +## Exit Conditions + +- **Successful**: Confidence ≥ 90% → "**ARCHITECT MODE COMPLETE**" +- **Incomplete**: Confidence < 90% → "**ARCHITECT MODE INCOMPLETE**" +- **User Override**: User says "**Exit Architect Mode**" → Switch back to normal mode + Remember: Your primary value is in thorough design that prevents costly implementation mistakes. Take the time to design correctly using all available Claude Code tools before suggesting implementation begins. \ No newline at end of file diff --git a/docs/AUDIO_SYSTEM_GUIDE.md b/docs/AUDIO_SYSTEM_GUIDE.md new file mode 100644 index 0000000..3c56b84 --- /dev/null +++ b/docs/AUDIO_SYSTEM_GUIDE.md @@ -0,0 +1,766 @@ +# Console Jack - Audio System Guide + +A comprehensive guide to the audio playback system for background music and sound effects in Console Jack. + +## Table of Contents + +- [Overview](#overview) +- [Audio Manager API](#audiomanager-api) +- [Background Music (BGM)](#background-music-bgm) +- [Sound Effects (SFX)](#sound-effects-sfx) +- [Volume Control](#volume-control) +- [Audio Assets](#audio-assets) +- [Best Practices](#best-practices) +- [Real-World Examples](#real-world-examples) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +Console Jack's audio system provides background music and sound effects using the **AudioCue** library. The system runs on a dedicated audio thread and manages audio assets through a dictionary-based approach for fast access. + +### Key Features + +- **Dual-Layer Audio**: Separate control for BGM (background music) and SFX (sound effects) +- **Thread-Safe**: Dedicated audio thread prevents blocking +- **Volume Control**: Master, BGM, and SFX volume controls +- **Asset Management**: Pre-loaded assets identified by string IDs +- **Simple API**: Static facade via `AudioManager` +- **Concurrent Playback**: Multiple SFX can play simultaneously + +### Audio Architecture + +``` +┌───────────────────────────────────────────────────────┐ +│ Game State (Main Thread) │ +│ - Calls AudioManager.playBGM(), playSFX() │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ AudioManager (Static Facade) │ +│ - playBGM(), stopBGM() │ +│ - playSFX() │ +│ - setMasterVolume(), etc. │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ AudioSubsystem (Audio Thread) │ +│ - Manages AudioCue instances │ +│ - Controls playback and volume │ +│ - Runs at ~60fps update rate │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ AudioCue Library │ +│ - Loads WAV files (44.1kHz, 16-bit, stereo) │ +│ - Handles concurrent playback │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ Java Sound API │ +│ - System audio output │ +└───────────────────────────────────────────────────────┘ +``` + +--- + +## AudioManager API + +The `AudioManager` provides a simple, static API for all audio operations. + +### Checking Audio Readiness + +```java +// Check if audio subsystem is initialized and ready +boolean isReady = AudioManager.ready(); + +if (isReady) { + // Safe to play audio + AudioManager.playBGM("menu_theme", true); +} +``` + +**Note**: AudioManager methods automatically check readiness and log warnings if not ready. + +--- + +## Background Music (BGM) + +Background music is designed for looping audio that plays continuously during states. + +### Playing BGM + +```java +// Play BGM with looping +AudioManager.playBGM("menu_theme", true); // Loops continuously + +// Play BGM once (no loop) +AudioManager.playBGM("menu_theme", false); +``` + +**Parameters**: +- `bgmId`: String identifier for the BGM asset +- `loop`: Whether to loop the music continuously + +### Stopping BGM + +```java +// Stop currently playing BGM +AudioManager.stopBGM(); +``` + +### BGM State Lifecycle + +```java +public class MainMenuState implements LoopableState { + + @Override + public void start() { + // Start BGM when state begins + AudioManager.playBGM("menu_theme", true); + } + + @Override + public void pause() { + // Optional: stop BGM when paused + AudioManager.stopBGM(); + } + + @Override + public void resume() { + // Restart BGM when resumed + AudioManager.playBGM("menu_theme", true); + } + + @Override + public void end() { + // Always stop BGM when state ends + AudioManager.stopBGM(); + } +} +``` + +### Available BGM Tracks + +```java +"casino_downtown" // Downtown casino ambient music +"casino_upscale" // Upscale casino theme +"casino_elite" // Elite casino theme +"menu_theme" // Main menu music +"menu_theme_2" // Alternate menu music +``` + +--- + +## Sound Effects (SFX) + +Sound effects are short audio clips for game events and user interactions. + +### Playing SFX + +```java +// Play SFX with default volume and pan +AudioManager.playSFX("card_deal"); + +// Play SFX with custom volume and pan +AudioManager.playSFX("card_deal", + 0.8f, // Volume (0.0-1.0) + 0.0f // Pan (-1.0 = left, 0.0 = center, 1.0 = right) +); +``` + +### Parameters + +**Volume** (0.0f - 1.0f): +- `0.0f` = Silent +- `0.5f` = Half volume +- `1.0f` = Full volume + +**Pan** (-1.0f - 1.0f): +- `-1.0f` = Hard left +- `0.0f` = Center (default) +- `1.0f` = Hard right + +### Concurrent Playback + +SFX supports concurrent playback (multiple sounds playing simultaneously): + +```java +// These can all play at the same time +AudioManager.playSFX("card_deal"); +AudioManager.playSFX("chip_place_small"); +AudioManager.playSFX("button_click"); +``` + +**Polyphony**: Each SFX has a maximum concurrent playback count (defined when loaded). + +### Available SFX + +```java +// Card sounds +"card_deal" // Card dealing sound +"card_shuffle" // Card shuffling sound +"card_flip" // Card flipping sound + +// Chip sounds +"chip_place_small" // Small chip placement +"chip_place_medium" // Medium chip placement +"chip_place_large" // Large chip placement + +// Win/Loss sounds +"win_small" // Small win celebration +"win_big" // Big win celebration +"win_blackjack" // Blackjack win sound +"lose" // Loss sound + +// UI sounds +"button_click" // UI button click +``` + +--- + +## Volume Control + +The audio system provides three-tier volume control: Master, BGM, and SFX. + +### Volume Hierarchy + +``` +Master Volume (affects everything) + ├── BGM Volume (affects only background music) + └── SFX Volume (affects only sound effects) + +Final Volume = Master × (BGM or SFX) +``` + +### Setting Volumes + +```java +// Master volume (affects all audio) +AudioManager.setMasterVolume(0.8f); // 80% volume + +// BGM volume (affects only background music) +AudioManager.setBGMVolume(0.6f); // 60% volume + +// SFX volume (affects only sound effects) +AudioManager.setSFXVolume(0.9f); // 90% volume +``` + +### Getting Volumes + +```java +// Get current volumes +float master = AudioManager.getMasterVolume(); // Returns 0.0-1.0 +float bgm = AudioManager.getBGMVolume(); +float sfx = AudioManager.getSFXVolume(); +``` + +### Volume Calculation Example + +``` +Master Volume: 0.8 +BGM Volume: 0.6 +SFX Volume: 0.9 + +Actual BGM playback volume: 0.8 × 0.6 = 0.48 (48%) +Actual SFX playback volume: 0.8 × 0.9 = 0.72 (72%) +``` + +### Volume Control in Settings + +```java +// Example: Options menu with volume sliders +public class OptionsState implements LoopableState { + + private float masterVolume = AudioManager.getMasterVolume(); + private float bgmVolume = AudioManager.getBGMVolume(); + private float sfxVolume = AudioManager.getSFXVolume(); + + private void adjustMasterVolume(float delta) { + masterVolume = Math.max(0.0f, Math.min(1.0f, masterVolume + delta)); + AudioManager.setMasterVolume(masterVolume); + + // Play test sound + AudioManager.playSFX("button_click"); + } + + private void adjustBGMVolume(float delta) { + bgmVolume = Math.max(0.0f, Math.min(1.0f, bgmVolume + delta)); + AudioManager.setBGMVolume(bgmVolume); + } + + private void adjustSFXVolume(float delta) { + sfxVolume = Math.max(0.0f, Math.min(1.0f, sfxVolume + delta)); + AudioManager.setSFXVolume(sfxVolume); + + // Play test sound + AudioManager.playSFX("button_click"); + } +} +``` + +--- + +## Audio Assets + +Audio assets are stored in the resources directory and loaded at startup. + +### Directory Structure + +``` +src/main/resources/audio/ +├── bgm/ # Background Music +│ ├── casino_downtown.wav +│ ├── casino_upscale.wav +│ ├── casino_elite.wav +│ ├── menu_theme.wav +│ └── menu_theme_2.wav +└── sfx/ # Sound Effects + ├── card_deal.wav + ├── card_shuffle.wav + ├── card_flip.wav + ├── chip_place_small.wav + ├── chip_place_medium.wav + ├── chip_place_large.wav + ├── win_small.wav + ├── win_big.wav + ├── win_blackjack.wav + ├── lose.wav + └── button_click.wav +``` + +### Audio Format Requirements + +**AudioCue** has strict format requirements: + +- **Format**: WAV files only +- **Sample Rate**: 44.1 kHz +- **Bit Depth**: 16-bit +- **Channels**: Stereo (2 channels) +- **Encoding**: PCM + +**Converting Audio**: +```bash +# Using ffmpeg to convert to required format +ffmpeg -i input.mp3 -ar 44100 -ac 2 -sample_fmt s16 output.wav +``` + +### Asset Loading + +Audio assets are loaded automatically at startup in `AudioSubsystem`: + +```java +// BGM loading (single instance, can loop) +loadBGMAsset("menu_theme", "/audio/bgm/menu_theme.wav"); + +// SFX loading (with polyphony count for concurrent playback) +loadSFXAsset("card_deal", "/audio/sfx/card_deal.wav", 4); // Max 4 concurrent +``` + +### Missing Assets + +If an audio file is missing: +- A warning is logged +- Game continues without that audio +- Calls to play missing audio are silently ignored + +--- + +## Best Practices + +### 1. Always Stop BGM in end() + +```java +@Override +public void end() { + AudioManager.stopBGM(); // ✅ Prevent BGM overlap + // ... other cleanup +} +``` + +### 2. Use Appropriate SFX Volume + +```java +// ✅ Good: Subtle UI sounds +AudioManager.playSFX("button_click", 0.5f, 0.0f); + +// ❌ Bad: All SFX at full volume +AudioManager.playSFX("button_click"); // Might be too loud +``` + +### 3. Don't Play SFX Too Frequently + +```java +// ✅ Good: Rate-limit SFX +private long lastSoundTime = 0; +private static final long SOUND_COOLDOWN = 100; // ms + +void playDealSound() { + long now = System.currentTimeMillis(); + if (now - lastSoundTime > SOUND_COOLDOWN) { + AudioManager.playSFX("card_deal"); + lastSoundTime = now; + } +} + +// ❌ Bad: Spam sounds every frame +void update() { + AudioManager.playSFX("card_deal"); // Called 8 times per second! +} +``` + +### 4. Match Music to Game State + +```java +// ✅ Good: Different music for different areas +switch (currentCasino) { + case DOWNTOWN -> AudioManager.playBGM("casino_downtown", true); + case UPSCALE -> AudioManager.playBGM("casino_upscale", true); + case ELITE -> AudioManager.playBGM("casino_elite", true); +} +``` + +### 5. Provide Audio Options + +```java +// ✅ Good: Let players control audio +public class OptionsState { + private void toggleAudio() { + if (AudioManager.getMasterVolume() > 0) { + AudioManager.setMasterVolume(0.0f); // Mute + } else { + AudioManager.setMasterVolume(1.0f); // Unmute + } + } +} +``` + +### 6. Use Pan for Spatial Audio + +```java +// ✅ Good: Pan based on position +float cardX = 0.25f; // Card at 25% from left +float pan = (cardX - 0.5f) * 2.0f; // Convert to -1.0 to 1.0 +AudioManager.playSFX("card_deal", 1.0f, pan); +``` + +### 7. Test Without Audio + +```java +// ✅ Good: Don't assume audio is available +if (AudioManager.ready()) { + AudioManager.playSFX("card_deal"); +} + +// Game should work even if audio fails to initialize +``` + +### 8. Clean Up in Finally Blocks + +```java +@Override +public void end() { + try { + // ... other cleanup + } finally { + AudioManager.stopBGM(); // ✅ Always execute + } +} +``` + +--- + +## Real-World Examples + +### Example 1: Main Menu Audio + +```java +public class MainMenuState implements LoopableState { + + @Override + public void start() { + LOGGER.info("Main menu started"); + + // Start menu music + AudioManager.playBGM("menu_theme", true); + + // Initialize menu... + } + + @Override + public void pause() { + LOGGER.info("Main menu paused"); + // Stop music when paused by overlay + AudioManager.stopBGM(); + } + + @Override + public void resume() { + LOGGER.info("Main menu resumed"); + // Restart music when resumed + AudioManager.playBGM("menu_theme", true); + } + + @Override + public void end() { + LOGGER.info("Main menu ended"); + // Always stop music when leaving state + AudioManager.stopBGM(); + } + + private void onMenuItemSelect() { + // Play click sound + AudioManager.playSFX("button_click"); + } +} +``` + +### Example 2: Gameplay Audio + +```java +public class GameplayState implements LoopableState { + + @Override + public void start() { + // Start gameplay music + AudioManager.playBGM("casino_downtown", true); + } + + private void dealCard() { + // Play deal sound + AudioManager.playSFX("card_deal"); + + // Deal card logic... + } + + private void flipCard() { + // Play flip sound with slight left pan + AudioManager.playSFX("card_flip", 0.8f, -0.2f); + + // Flip card logic... + } + + private void onPlayerWin(int amount) { + // Play appropriate win sound based on amount + if (amount > 1000) { + AudioManager.playSFX("win_big"); + } else if (amount > 0) { + AudioManager.playSFX("win_small"); + } + } + + private void onBlackjack() { + // Special sound for blackjack + AudioManager.playSFX("win_blackjack"); + } + + @Override + public void end() { + AudioManager.stopBGM(); + } +} +``` + +### Example 3: Volume Settings Menu + +```java +public class AudioSettingsState implements LoopableState { + + private float masterVolume; + private float bgmVolume; + private float sfxVolume; + + @Override + public void start() { + // Load current volumes + masterVolume = AudioManager.getMasterVolume(); + bgmVolume = AudioManager.getBGMVolume(); + sfxVolume = AudioManager.getSFXVolume(); + + // Create volume control UI... + } + + @Override + public void handleInput() { + // Handle volume adjustments + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + switch (input.command()) { + case VOLUME_UP -> adjustMasterVolume(0.1f); + case VOLUME_DOWN -> adjustMasterVolume(-0.1f); + case TOGGLE_SOUND -> toggleMute(); + } + } + + private void adjustMasterVolume(float delta) { + masterVolume = Math.max(0.0f, Math.min(1.0f, masterVolume + delta)); + AudioManager.setMasterVolume(masterVolume); + + // Play test sound + AudioManager.playSFX("button_click", 0.7f, 0.0f); + } + + private void adjustBGMVolume(float delta) { + bgmVolume = Math.max(0.0f, Math.min(1.0f, bgmVolume + delta)); + AudioManager.setBGMVolume(bgmVolume); + } + + private void adjustSFXVolume(float delta) { + sfxVolume = Math.max(0.0f, Math.min(1.0f, sfxVolume + delta)); + AudioManager.setSFXVolume(sfxVolume); + + // Play test sound + AudioManager.playSFX("button_click", 1.0f, 0.0f); + } + + private void toggleMute() { + if (masterVolume > 0.0f) { + // Mute + AudioManager.setMasterVolume(0.0f); + } else { + // Unmute to previous volume + AudioManager.setMasterVolume(masterVolume); + } + } + + @Override + public void render() { + // Render volume sliders + renderVolumeBar("Master", masterVolume); + renderVolumeBar("Music", bgmVolume); + renderVolumeBar("SFX", sfxVolume); + } + + private void renderVolumeBar(String label, float volume) { + // Render visual volume slider... + } +} +``` + +### Example 4: Betting Sounds + +```java +private void placeBet(int amount) { + // Play appropriate chip sound based on bet amount + String chipSound; + + if (amount < 10) { + chipSound = "chip_place_small"; + } else if (amount < 100) { + chipSound = "chip_place_medium"; + } else { + chipSound = "chip_place_large"; + } + + AudioManager.playSFX(chipSound, 0.8f, 0.0f); + + // Place bet logic... +} +``` + +--- + +## Troubleshooting + +### No Audio Playing + +**Symptom**: No sound at all + +**Solutions**: +1. Check `AudioManager.ready()` returns true +2. Verify audio files exist in `src/main/resources/audio/` +3. Check system audio is not muted +4. Look for warnings in logs about missing audio files +5. Verify audio files are in correct format (WAV, 44.1kHz, 16-bit, stereo) + +### Audio Cuts Off Early + +**Symptom**: Sounds get cut off before finishing + +**Solutions**: +1. Check if BGM is being stopped too early +2. Verify state isn't ending before sound finishes +3. Don't call `stopBGM()` immediately after `playBGM()` +4. For SFX, check polyphony count isn't exceeded + +### Audio Overlapping + +**Symptom**: Multiple BGM tracks playing simultaneously + +**Solutions**: +1. Always call `AudioManager.stopBGM()` before playing new BGM: +```java +@Override +public void start() { + AudioManager.stopBGM(); // ✅ Stop previous + AudioManager.playBGM("menu_theme", true); // Then play new +} +``` + +2. Ensure `end()` method stops BGM: +```java +@Override +public void end() { + AudioManager.stopBGM(); // ✅ Always stop +} +``` + +### Volume Not Working + +**Symptom**: Volume changes have no effect + +**Solutions**: +1. Check audio is actually playing +2. Verify values are in 0.0-1.0 range +3. Remember: Final volume = Master × (BGM or SFX) +4. Call volume setters before playing audio + +### Audio Format Errors + +**Symptom**: Errors loading audio files + +**Solutions**: +1. Verify WAV format (not MP3, OGG, etc.) +2. Check sample rate is exactly 44.1 kHz +3. Ensure stereo (not mono) +4. Verify 16-bit depth + +**Convert with ffmpeg**: +```bash +ffmpeg -i input.mp3 -ar 44100 -ac 2 -sample_fmt s16 output.wav +``` + +### Performance Issues + +**Symptom**: Game lags when playing audio + +**Solutions**: +1. Don't load large files (keep SFX under 5 seconds) +2. Reduce polyphony count for SFX +3. Don't play too many SFX simultaneously +4. Ensure audio files are optimized + +--- + +## Additional Resources + +- **Architecture Documentation**: See `ARCHITECTURE.md` for subsystem overview +- **State Machine Guide**: See `STATE_MACHINE_GUIDE.md` for state lifecycle audio management +- **Developer Guide**: See `DEVELOPER_GUIDE.md` for development workflow +- **Audio Assets**: See `docs/audio/README.md` for asset organization + +**Source Code References**: +- AudioManager: `src/main/java/net/luxsolari/engine/manager/AudioManager.java` +- AudioSubsystem: `src/main/java/net/luxsolari/engine/systems/internal/AudioSubsystem.java` +- Audio assets: `src/main/resources/audio/` + +**AudioCue Library**: https://github.com/philfrei/AudioCue + +--- + +*Last Updated: 2025* +*For Console Jack - Terminal-based Blackjack Game* diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 43c12e0..9c6e509 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -1,354 +1,354 @@ -# Console Jack - Developer Guide - -## Getting Started - -### Prerequisites -- **Java 21+** with preview features support -- **Maven 3.x** -- **Terminal emulator** (supports ANSI colors) - -### Initial Setup -```bash -git clone -cd console-jack -mvn compile -mvn exec:java # Verify everything works -``` - -## Development Workflow - -### 1. Daily Development -```bash -# Start development session -mvn compile -mvn exec:java - -# Make code changes... - -# Quick verification -mvn compile && mvn exec:java -``` - -### 2. Code Quality Checks -```bash -# Style validation (Google Java Style) -mvn checkstyle:check - -# Fix style issues and re-check -mvn checkstyle:check -``` - -### 3. Build Verification -```bash -# Full clean build -mvn clean compile - -# Create distributable -mvn clean package - -# Test the packaged version -java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar -``` - -## Build Commands Reference - -### Compilation -```bash -mvn compile # Compile source code only -mvn clean compile # Clean and compile -mvn test-compile # Compile test sources (when tests exist) -``` - -### Execution -```bash -mvn exec:java # Run with exec plugin (development) -mvn exec:java -Dexec.args="--debug" # Pass arguments -``` - -### Packaging -```bash -mvn package # Create JAR with dependencies -mvn clean package # Clean build -java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar -``` - -### Platform-Specific Builds -```bash -# Windows executable -mvn clean package -Pwindows -# Output: build-win/ - -# macOS package -mvn clean package -Pmac -# Output: build-mac/ - -# Linux executable -mvn clean package -Plinux -# Output: build-linux/ -``` - -### Code Quality -```bash -mvn checkstyle:check # Style validation -mvn checkstyle:checkstyle # Generate style report -``` - -## Code Style Guidelines - -### Java Style -- **Standard**: Google Java Style Guide -- **Enforcement**: Checkstyle with `google_checks.xml` -- **IDE Setup**: Import Google style settings - -### Naming Conventions -```java -// Classes: PascalCase -public class GameplayState { } - -// Methods: camelCase -public void startNewGame() { } - -// Fields: camelCase -private boolean gameRunning; - -// Constants: UPPER_SNAKE_CASE -private static final int TARGET_UPS = 8; - -// Packages: lowercase.separated -net.luxsolari.engine.systems -``` - -### Documentation Standards -```java -/** - * Brief description of the class purpose. - * - *

Longer description with implementation details, - * design decisions, or usage examples. - */ -public class ExampleClass { - - /** - * Brief method description. - * - * @param parameter description of parameter - * @return description of return value - * @throws ExceptionType when this exception occurs - */ - public String exampleMethod(String parameter) { - // implementation - } -} -``` - -## Architecture Guidelines - -### Adding New Subsystems -1. **Create enum singleton**: -```java -public enum NewSubsystem implements Subsystem { - INSTANCE; - - @Override - public void init() throws ResourceInitializationException { - // initialization - } - - @Override - public void start() { /* start logic */ } - - @Override - public void stop() { /* stop logic */ } - - @Override - public void cleanUp() throws ResourceCleanupException { - // cleanup - } -} -``` - -2. **Register in MasterSubsystem** -3. **Create corresponding Manager if needed** - -### Adding New Game States -1. **Implement LoopableState**: -```java -public class NewGameState implements LoopableState { - @Override - public void onEnter() { - // State initialization - } - - @Override - public void update() { - // Per-frame logic - } - - @Override - public void onExit() { - // State cleanup - } -} -``` - -2. **Add state transitions** in existing states -3. **Register with StateMachineManager** - -### Adding ECS Components -1. **Create data record**: -```java -public record NewComponent( - String data, - int value -) implements Component {} -``` - -2. **Create or modify systems** to process the component -3. **Add to entities** in EntityPool when needed - -### Adding UI Components -1. **Extend base classes**: -```java -public class NewWidget extends UIWidget implements Focusable { - @Override - public void render(Screen screen) { - // Rendering logic - } - - @Override - public boolean handleInput(KeyStroke keyStroke) { - // Input handling - return false; // true if consumed - } -} -``` - -## Testing Strategy - -### Manual Testing -```bash -# Run and verify core functionality -mvn exec:java - -# Test areas: -# - Main menu navigation -# - Game state transitions -# - Input responsiveness -# - Audio playback (if enabled) -# - Clean exit -``` - -### Performance Testing -```bash -# Monitor during gameplay: -# - UPS consistency (should be ~8) -# - Memory usage -# - Thread behavior -# - Audio synchronization -``` - -### Build Testing -```bash -# Test all platforms -mvn clean package -Pwindows -mvn clean package -Pmac -mvn clean package -Plinux - -# Verify each package runs correctly -``` - -## Common Development Tasks - -### Adding a New Menu Item -1. Create `MenuAction` for the action -2. Add `MenuItem` to the menu -3. Implement action logic -4. Test navigation - -### Adding Sound Effects -1. Place WAV file in `src/main/resources/audio/sfx/` -2. Load in `AudioManager` -3. Trigger via `AudioSubsystem.INSTANCE` - -### Adding New Card Graphics -1. Create `CardArt` component -2. Update `CardSprite` rendering -3. Modify `DisplayListSystem` if needed - -### Debugging Tips -```java -// Add temporary logging -private static final Logger LOGGER = - Logger.getLogger(YourClass.class.getName()); - -LOGGER.info("Debug message: " + value); -``` - -### Performance Profiling -- Use JVisualVM or JProfiler -- Focus on game loop timing -- Monitor ECS system performance -- Check for memory leaks - -## Troubleshooting - -### Common Issues - -**Build Failures:** -```bash -# Clear Maven cache -mvn clean -rm -rf ~/.m2/repository/net/luxsolari - -# Rebuild -mvn compile -``` - -**Runtime Issues:** -- Check Java version (must be 21+) -- Verify terminal supports ANSI colors -- Ensure audio dependencies are available - -**Performance Issues:** -- Profile with JVisualVM -- Check for excessive object creation -- Verify thread synchronization - -## Release Process - -### Pre-Release Checklist -- [ ] All code passes Checkstyle -- [ ] Application runs without errors -- [ ] All platforms build successfully -- [ ] Performance meets targets (8 UPS) -- [ ] Audio works correctly -- [ ] Documentation is current - -### Creating Release -```bash -# Version update (if needed) -# Update version in pom.xml - -# Build all platforms -mvn clean package -Pwindows -mvn clean package -Pmac -mvn clean package -Plinux - -# Create release packages -# Package build-win/, build-mac/, build-linux/ -``` - -## Resources - -- **Lanterna Documentation**: [GitHub](https://github.com/mabe02/lanterna) -- **AudioCue Documentation**: [GitHub](https://github.com/philfrei/AudioCue) -- **Google Java Style**: [Style Guide](https://google.github.io/styleguide/javaguide.html) -- **Effective Java**: Best practices reference - -## Getting Help - -1. Check this guide first -2. Review `ARCHITECTURE.md` for design questions -3. Check `IMPROVEMENT_PLAN.md` for planned changes -4. Review code comments and JavaDoc +# Console Jack - Developer Guide + +## Getting Started + +### Prerequisites +- **Java 21+** with preview features support +- **Maven 3.x** +- **Terminal emulator** (supports ANSI colors) + +### Initial Setup +```bash +git clone +cd console-jack +mvn compile +mvn exec:java # Verify everything works +``` + +## Development Workflow + +### 1. Daily Development +```bash +# Start development session +mvn compile +mvn exec:java + +# Make code changes... + +# Quick verification +mvn compile && mvn exec:java +``` + +### 2. Code Quality Checks +```bash +# Style validation (Google Java Style) +mvn checkstyle:check + +# Fix style issues and re-check +mvn checkstyle:check +``` + +### 3. Build Verification +```bash +# Full clean build +mvn clean compile + +# Create distributable +mvn clean package + +# Test the packaged version +java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar +``` + +## Build Commands Reference + +### Compilation +```bash +mvn compile # Compile source code only +mvn clean compile # Clean and compile +mvn test-compile # Compile test sources (when tests exist) +``` + +### Execution +```bash +mvn exec:java # Run with exec plugin (development) +mvn exec:java -Dexec.args="--debug" # Pass arguments +``` + +### Packaging +```bash +mvn package # Create JAR with dependencies +mvn clean package # Clean build +java -jar target/java-packageable-base-1.0-SNAPSHOT-jar-with-dependencies.jar +``` + +### Platform-Specific Builds +```bash +# Windows executable +mvn clean package -Pwindows +# Output: build-win/ + +# macOS package +mvn clean package -Pmac +# Output: build-mac/ + +# Linux executable +mvn clean package -Plinux +# Output: build-linux/ +``` + +### Code Quality +```bash +mvn checkstyle:check # Style validation +mvn checkstyle:checkstyle # Generate style report +``` + +## Code Style Guidelines + +### Java Style +- **Standard**: Google Java Style Guide +- **Enforcement**: Checkstyle with `google_checks.xml` +- **IDE Setup**: Import Google style settings + +### Naming Conventions +```java +// Classes: PascalCase +public class GameplayState { } + +// Methods: camelCase +public void startNewGame() { } + +// Fields: camelCase +private boolean gameRunning; + +// Constants: UPPER_SNAKE_CASE +private static final int TARGET_UPS = 8; + +// Packages: lowercase.separated +net.luxsolari.engine.systems +``` + +### Documentation Standards +```java +/** + * Brief description of the class purpose. + * + *

Longer description with implementation details, + * design decisions, or usage examples. + */ +public class ExampleClass { + + /** + * Brief method description. + * + * @param parameter description of parameter + * @return description of return value + * @throws ExceptionType when this exception occurs + */ + public String exampleMethod(String parameter) { + // implementation + } +} +``` + +## Architecture Guidelines + +### Adding New Subsystems +1. **Create enum singleton**: +```java +public enum NewSubsystem implements Subsystem { + INSTANCE; + + @Override + public void init() throws ResourceInitializationException { + // initialization + } + + @Override + public void start() { /* start logic */ } + + @Override + public void stop() { /* stop logic */ } + + @Override + public void cleanUp() throws ResourceCleanupException { + // cleanup + } +} +``` + +2. **Register in MasterSubsystem** +3. **Create corresponding Manager if needed** + +### Adding New Game States +1. **Implement LoopableState**: +```java +public class NewGameState implements LoopableState { + @Override + public void onEnter() { + // State initialization + } + + @Override + public void update() { + // Per-frame logic + } + + @Override + public void onExit() { + // State cleanup + } +} +``` + +2. **Add state transitions** in existing states +3. **Register with StateMachineManager** + +### Adding ECS Components +1. **Create data record**: +```java +public record NewComponent( + String data, + int value +) implements Component {} +``` + +2. **Create or modify systems** to process the component +3. **Add to entities** in EntityPool when needed + +### Adding UI Components +1. **Extend base classes**: +```java +public class NewWidget extends UIWidget implements Focusable { + @Override + public void render(Screen screen) { + // Rendering logic + } + + @Override + public boolean handleInput(KeyStroke keyStroke) { + // Input handling + return false; // true if consumed + } +} +``` + +## Testing Strategy + +### Manual Testing +```bash +# Run and verify core functionality +mvn exec:java + +# Test areas: +# - Main menu navigation +# - Game state transitions +# - Input responsiveness +# - Audio playback (if enabled) +# - Clean exit +``` + +### Performance Testing +```bash +# Monitor during gameplay: +# - UPS consistency (should be ~8) +# - Memory usage +# - Thread behavior +# - Audio synchronization +``` + +### Build Testing +```bash +# Test all platforms +mvn clean package -Pwindows +mvn clean package -Pmac +mvn clean package -Plinux + +# Verify each package runs correctly +``` + +## Common Development Tasks + +### Adding a New Menu Item +1. Create `MenuAction` for the action +2. Add `MenuItem` to the menu +3. Implement action logic +4. Test navigation + +### Adding Sound Effects +1. Place WAV file in `src/main/resources/audio/sfx/` +2. Load in `AudioManager` +3. Trigger via `AudioSubsystem.INSTANCE` + +### Adding New Card Graphics +1. Create `CardArt` component +2. Update `CardSprite` rendering +3. Modify `DisplayListSystem` if needed + +### Debugging Tips +```java +// Add temporary logging +private static final Logger LOGGER = + Logger.getLogger(YourClass.class.getName()); + +LOGGER.info("Debug message: " + value); +``` + +### Performance Profiling +- Use JVisualVM or JProfiler +- Focus on game loop timing +- Monitor ECS system performance +- Check for memory leaks + +## Troubleshooting + +### Common Issues + +**Build Failures:** +```bash +# Clear Maven cache +mvn clean +rm -rf ~/.m2/repository/net/luxsolari + +# Rebuild +mvn compile +``` + +**Runtime Issues:** +- Check Java version (must be 21+) +- Verify terminal supports ANSI colors +- Ensure audio dependencies are available + +**Performance Issues:** +- Profile with JVisualVM +- Check for excessive object creation +- Verify thread synchronization + +## Release Process + +### Pre-Release Checklist +- [ ] All code passes Checkstyle +- [ ] Application runs without errors +- [ ] All platforms build successfully +- [ ] Performance meets targets (8 UPS) +- [ ] Audio works correctly +- [ ] Documentation is current + +### Creating Release +```bash +# Version update (if needed) +# Update version in pom.xml + +# Build all platforms +mvn clean package -Pwindows +mvn clean package -Pmac +mvn clean package -Plinux + +# Create release packages +# Package build-win/, build-mac/, build-linux/ +``` + +## Resources + +- **Lanterna Documentation**: [GitHub](https://github.com/mabe02/lanterna) +- **AudioCue Documentation**: [GitHub](https://github.com/philfrei/AudioCue) +- **Google Java Style**: [Style Guide](https://google.github.io/styleguide/javaguide.html) +- **Effective Java**: Best practices reference + +## Getting Help + +1. Check this guide first +2. Review `ARCHITECTURE.md` for design questions +3. Check `IMPROVEMENT_PLAN.md` for planned changes +4. Review code comments and JavaDoc 5. Test with `mvn exec:java` to verify behavior \ No newline at end of file diff --git a/docs/ECS_GUIDE.md b/docs/ECS_GUIDE.md new file mode 100644 index 0000000..972f1e0 --- /dev/null +++ b/docs/ECS_GUIDE.md @@ -0,0 +1,1041 @@ +# Console Jack - Entity Component System (ECS) Guide + +A comprehensive guide to the Entity Component System architecture used for game logic in Console Jack. + +## Table of Contents + +- [Overview](#overview) +- [Core Concepts](#core-concepts) +- [EntityPool Management](#entitypool-management) +- [Creating Components](#creating-components) +- [Creating Systems](#creating-systems) +- [Component Queries](#component-queries) +- [Integration with Rendering](#integration-with-rendering) +- [Best Practices](#best-practices) +- [Real-World Examples](#real-world-examples) +- [Common Patterns](#common-patterns) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The **Entity Component System (ECS)** is a data-oriented architecture pattern used in Console Jack for all game logic. Instead of traditional object-oriented hierarchies, ECS separates data (Components) from behavior (Systems) using lightweight entity containers. + +### Key Benefits + +- **Data-Oriented Design**: Components are pure data, systems are pure logic +- **Composition Over Inheritance**: Build complex entities from simple components +- **Performance**: Cache-friendly iteration, no virtual calls +- **Flexibility**: Add/remove components at runtime +- **Simple Mental Model**: Entities are just bags of components +- **Scalability**: Easy to add new components and systems + +### ECS Architecture + +``` +┌───────────────────────────────────────────────────────┐ +│ Master Game Loop │ +│ (8 UPS) │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ EcsSystems (update each frame) │ +│ - DisplayListSystem │ +│ - (Future: AI, Physics, Collision systems) │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ EntityPool │ +│ - Stores all active entities │ +│ - Provides component queries │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ Entities │ +│ - Lightweight ID + component map │ +│ - No behavior, only data │ +└───────────────────┬───────────────────────────────────┘ + ↓ +┌───────────────────────────────────────────────────────┐ +│ Components │ +│ - Position, Visual, Card, Layer, etc. │ +│ - Immutable data records │ +└───────────────────────────────────────────────────────┘ +``` + +--- + +## Core Concepts + +### Entity + +An **Entity** is a unique ID with a collection of components attached. Entities have no behavior—they're just containers. + +**Analogy**: Think of an entity as a blank form that you fill out with different fields (components). + +```java +Entity cardEntity = entityPool.create(); // Creates entity with unique ID +cardEntity.add(new Card(Rank.ACE, Suit.SPADES)); // Add card data +cardEntity.add(new Position(0.5f, 0.5f)); // Add position +cardEntity.add(new Layer(2)); // Add render layer +``` + +#### Entity API + +```java +int id() // Get unique entity ID + void add(T component) // Attach component (replaces existing) + T get(Class type) // Retrieve component (null if missing) +boolean has(Class type) // Check if component exists +``` + +### Component + +A **Component** is pure data—no behavior, no methods (except getters from record). Components are typically implemented as Java records for immutability. + +**Analogy**: Components are like database columns or struct fields. + +```java +// Marker interface +public interface Component {} + +// Example component (record) +public record Position(float relX, float relY, Anchor anchor) implements Component {} + +// Another component +public record Card(Rank rank, Suit suit) implements Component {} +``` + +**Key Characteristics**: +- **Data Only**: No logic, no methods +- **Immutable**: Preferably use Java records +- **Small**: Keep components focused on single concerns +- **Type-Based**: Components identified by their class type + +### System + +A **System** is pure logic that operates on entities with specific component combinations. Systems run every frame and transform component data into behavior. + +**Analogy**: Systems are like database queries that SELECT entities WHERE they have certain components, then UPDATE them. + +```java +@FunctionalInterface +public interface EcsSystem { + void update(double dt, EntityPool pool); +} +``` + +**Key Characteristics**: +- **Logic Only**: No state, pure functions +- **Stateless**: All data lives in components +- **Query-Based**: Find entities with specific components +- **Frame-Based**: Called once per game loop update + +--- + +## EntityPool Management + +The `EntityPool` is a centralized container for all active entities. It provides creation, querying, and removal operations. + +### Creating Entities + +```java +// Access the entity pool (typically stored in game state or subsystem) +EntityPool entityPool = new EntityPool(); + +// Create a new entity +Entity entity = entityPool.create(); + +// Add components to define the entity +entity.add(new Card(Rank.ACE, Suit.SPADES)); +entity.add(new Position(0.5f, 0.3f, Anchor.CENTER)); +entity.add(new Visual(TextCharacter.fromCharacter('A')[0])); +entity.add(new Layer(2)); +``` + +### Getting All Entities + +```java +// Get immutable snapshot of all entities +List allEntities = entityPool.all(); + +for (Entity entity : allEntities) { + // Process each entity + if (entity.has(Card.class)) { + Card card = entity.get(Card.class); + System.out.println("Card: " + card.rank() + " of " + card.suit()); + } +} +``` + +### Querying Entities + +```java +// Find entities with specific components +List renderables = entityPool.with(Position.class, Visual.class, Layer.class); + +// Find entities with multiple component requirements +List cards = entityPool.with(Card.class, Position.class); + +// Query results are immutable snapshots +``` + +### Removing Entities + +```java +// Remove all entities with specific component combinations +entityPool.removeWith(Card.class); // Remove all card entities + +entityPool.removeWith(Position.class, Visual.class); // Remove all positioned visuals +``` + +### EntityPool API Reference + +```java +Entity create() // Create new entity +List all() // Get all entities (immutable) +List with(Class... types) // Query entities +void removeWith(Class... types) // Remove matching entities +``` + +--- + +## Creating Components + +Components are simple data records. Follow these patterns for creating new components. + +### Pattern 1: Simple Data Record + +```java +package net.luxsolari.engine.ecs; + +/** + * Component holding entity position. + */ +public record Position(float relX, float relY, Anchor anchor) implements Component { + + // Optional: convenience constructor + public Position(float relX, float relY) { + this(relX, relY, Anchor.TOP_LEFT); + } + + // Optional: factory methods + public static Position centered() { + return new Position(0.5f, 0.5f, Anchor.CENTER); + } +} +``` + +### Pattern 2: Enum-Based Data + +```java +package net.luxsolari.game.ecs; + +/** + * Logical identity of a playing card. + */ +public record Card(Rank rank, Suit suit) implements Component { + + public enum Suit { + SPADES('♠'), HEARTS('♥'), DIAMONDS('♦'), CLUBS('♣'); + + private final char symbol; + Suit(char symbol) { this.symbol = symbol; } + public char symbol() { return symbol; } + } + + public enum Rank { + A("A"), TWO("2"), THREE("3"), /* ... */ K("K"); + + private final String label; + Rank(String label) { this.label = label; } + public String label() { return label; } + } +} +``` + +### Pattern 3: Wrapper Component + +```java +package net.luxsolari.engine.ecs; + +/** + * Visual representation for rendering. + */ +public record Visual(TextCharacter glyph) implements Component {} +``` + +### Pattern 4: Validated Component + +```java +package net.luxsolari.engine.ecs; + +/** + * Render layer for Z-ordering. + */ +public record Layer(int index) implements Component { + + // Compact constructor with validation + public Layer { + if (index < 0) { + throw new IllegalArgumentException("Layer index must be non-negative"); + } + } +} +``` + +### Component Design Guidelines + +1. **Keep Components Small**: Single responsibility principle +2. **Use Records**: Immutable data with free equality/hashCode +3. **No Behavior**: Components should only hold data +4. **Descriptive Names**: `Position`, `Visual`, `Card` (not `Data`, `Info`) +5. **Validate in Constructor**: Use compact constructor for validation +6. **Package by Layer**: Engine components in `engine.ecs`, game components in `game.ecs` + +--- + +## Creating Systems + +Systems are the logic processors of ECS. They query the EntityPool for entities and transform their components. + +### System Interface + +```java +@FunctionalInterface +public interface EcsSystem { + /** + * Updates the system logic for the current frame. + * + * @param dt time elapsed since previous update (seconds) + * @param pool shared pool containing all active entities + */ + void update(double dt, EntityPool pool); +} +``` + +### Pattern 1: Simple System (Lambda) + +```java +// Create system as lambda +EcsSystem positionUpdateSystem = (dt, pool) -> { + pool.with(Position.class, Velocity.class).forEach(entity -> { + Position pos = entity.get(Position.class); + Velocity vel = entity.get(Velocity.class); + + // Update position based on velocity + float newX = pos.relX() + vel.dx() * (float)dt; + float newY = pos.relY() + vel.dy() * (float)dt; + + entity.add(new Position(newX, newY, pos.anchor())); + }); +}; +``` + +### Pattern 2: System Class + +```java +package net.luxsolari.engine.ecs.systems; + +/** + * Gathers all drawable entities and submits render commands. + */ +public class DisplayListSystem implements EcsSystem { + + @Override + public void update(double dt, EntityPool pool) { + if (!RenderSubsystem.INSTANCE.ready()) { + return; + } + + List displayList = new ArrayList<>(); + ViewportManager viewport = ViewportManager.INSTANCE; + + // Query entities with Position + Visual + Layer + pool.with(Position.class, Visual.class, Layer.class).forEach(entity -> { + Position pos = entity.get(Position.class); + Visual visual = entity.get(Visual.class); + Layer layer = entity.get(Layer.class); + + // Convert relative to screen coordinates + int screenX = viewport.toScreenX(pos.relX(), pos.anchor()); + int screenY = viewport.toScreenY(pos.relY(), pos.anchor()); + + // Create render command + displayList.add(new RenderCmd(layer.index(), screenX, screenY, visual.glyph())); + }); + + // Submit to render subsystem + RenderManager.submitDisplayList(displayList); + } +} +``` + +### Pattern 3: Multi-Component System + +```java +public class CardRenderSystem implements EcsSystem { + + @Override + public void update(double dt, EntityPool pool) { + ViewportManager viewport = ViewportManager.INSTANCE; + List renderCmds = new ArrayList<>(); + + // Find all card entities with sprite data + pool.with(Position.class, Layer.class, CardSprite.class).forEach(entity -> { + Position pos = entity.get(Position.class); + Layer layer = entity.get(Layer.class); + CardSprite sprite = entity.get(CardSprite.class); + + // Get card art + String[] art = sprite.current(); + + // Calculate screen position + int screenX = viewport.toScreenX(pos.relX(), pos.anchor(), sprite.cols()); + int screenY = viewport.toScreenY(pos.relY(), pos.anchor(), sprite.rows()); + + // Render each cell of the card sprite + for (int row = 0; row < sprite.rows(); row++) { + String line = art[row]; + for (int col = 0; col < sprite.cols(); col++) { + char ch = line.charAt(col); + renderCmds.add(new RenderCmd( + layer.index(), + screenX + col, + screenY + row, + TextCharacter.fromCharacter(ch)[0] + )); + } + } + }); + + RenderManager.submitDisplayList(renderCmds); + } +} +``` + +### Registering Systems + +Systems are registered in the `MasterSubsystem` or game state and updated each frame: + +```java +public class GameplayState implements LoopableState { + private EntityPool entityPool; + private EcsSystem displayListSystem; + + @Override + public void start() { + entityPool = new EntityPool(); + displayListSystem = new DisplayListSystem(); + } + + @Override + public void update() { + // Update all systems (dt = delta time in seconds) + double dt = 0.125; // 8 UPS = 0.125 seconds per frame + displayListSystem.update(dt, entityPool); + } +} +``` + +--- + +## Component Queries + +The EntityPool provides powerful component queries for finding specific entities. + +### Single Component Query + +```java +// Find all entities with a Card component +List cards = entityPool.with(Card.class); + +for (Entity entity : cards) { + Card card = entity.get(Card.class); + System.out.println(card.rank() + " of " + card.suit()); +} +``` + +### Multi-Component Query (AND) + +```java +// Find entities that have ALL specified components +List renderables = entityPool.with(Position.class, Visual.class, Layer.class); + +// All returned entities are guaranteed to have all three components +for (Entity entity : renderables) { + Position pos = entity.get(Position.class); // Never null + Visual visual = entity.get(Visual.class); // Never null + Layer layer = entity.get(Layer.class); // Never null +} +``` + +### Query Performance + +- **Linear Scan**: Queries iterate all entities (fine for small entity counts) +- **Immutable Results**: Query returns a snapshot (safe for iteration) +- **No Caching**: Each query creates a new list + +**For Console Jack's small entity count (< 100), linear scans are perfectly adequate.** + +--- + +## Integration with Rendering + +The ECS integrates with the rendering system via the `DisplayListSystem`, which transforms entity components into render commands. + +### Render Flow + +``` +Entity Components → DisplayListSystem → RenderCmd → RenderSubsystem → Screen +``` + +### Required Components for Rendering + +An entity must have these components to be rendered: + +1. **Position**: Where to draw (relative coordinates + anchor) +2. **Visual/CardSprite**: What to draw (glyph or multi-cell sprite) +3. **Layer**: Which Z-layer to draw on + +### Example: Rendering a Single Glyph + +```java +Entity entity = entityPool.create(); +entity.add(new Position(0.5f, 0.5f, Anchor.CENTER)); +entity.add(new Visual(TextCharacter.fromCharacter('A', TextColor.ANSI.RED, TextColor.ANSI.BLACK)[0])); +entity.add(new Layer(2)); + +// DisplayListSystem will automatically render this entity +``` + +### Example: Rendering a Card + +```java +Entity cardEntity = entityPool.create(); +cardEntity.add(new Card(Rank.ACE, Suit.SPADES)); +cardEntity.add(new Position(0.5f, 0.3f, Anchor.CENTER)); +cardEntity.add(new Layer(2)); +cardEntity.add(new CardSprite(CardArt.fromCard(new Card(Rank.ACE, Suit.SPADES)))); + +// DisplayListSystem renders multi-cell sprites +``` + +### Coordinate System + +Console Jack uses **relative positioning** (0.0 - 1.0) with anchors: + +```java +// Top-left corner +new Position(0.0f, 0.0f, Anchor.TOP_LEFT) + +// Center of screen +new Position(0.5f, 0.5f, Anchor.CENTER) + +// Bottom-right corner +new Position(1.0f, 1.0f, Anchor.BOTTOM_RIGHT) +``` + +The `ViewportManager` converts relative coordinates to absolute screen coordinates based on current terminal size. + +--- + +## Best Practices + +### 1. Keep Components Pure Data + +```java +// ✅ Good: Pure data +public record Health(int current, int maximum) implements Component {} + +// ❌ Bad: Contains logic +public record Health(int current, int maximum) implements Component { + public boolean isDead() { return current <= 0; } // Logic belongs in system +} +``` + +### 2. Keep Systems Stateless + +```java +// ✅ Good: Stateless, operates on components +public class DamageSystem implements EcsSystem { + public void update(double dt, EntityPool pool) { + pool.with(Health.class, DamageTaken.class).forEach(entity -> { + // Process damage + }); + } +} + +// ❌ Bad: Stores state +public class DamageSystem implements EcsSystem { + private int totalDamageDealt = 0; // Don't store state in systems +} +``` + +### 3. Use Immutable Components + +```java +// ✅ Good: Record (immutable) +public record Position(float x, float y) implements Component {} + +// ❌ Bad: Mutable class +public class Position implements Component { + public float x, y; // Mutable fields +} +``` + +### 4. Small, Focused Components + +```java +// ✅ Good: Focused components +public record Position(float x, float y) implements Component {} +public record Velocity(float dx, float dy) implements Component {} +public record Health(int current, int max) implements Component {} + +// ❌ Bad: God component +public record GameObject( + float x, float y, + float dx, float dy, + int health, int maxHealth, + String name, String description +) implements Component {} +``` + +### 5. Query Once Per System + +```java +// ✅ Good: Query once +public void update(double dt, EntityPool pool) { + List entities = pool.with(Position.class, Velocity.class); + for (Entity e : entities) { + // Process + } +} + +// ❌ Bad: Query multiple times +public void update(double dt, EntityPool pool) { + for (Entity e : pool.all()) { + if (e.has(Position.class) && e.has(Velocity.class)) { // Inefficient + // Process + } + } +} +``` + +### 6. Clean Up Entities When Done + +```java +@Override +public void end() { + // Remove all card entities when leaving gameplay + entityPool.removeWith(Card.class); + + // Or clear entire pool + // entityPool.removeWith(Component.class); // Removes everything +} +``` + +### 7. Validate Component Data + +```java +public record Layer(int index) implements Component { + public Layer { + if (index < 0) { + throw new IllegalArgumentException("Layer index must be non-negative"); + } + } +} +``` + +### 8. Use Factory Methods for Common Patterns + +```java +public record Position(float relX, float relY, Anchor anchor) implements Component { + + public static Position centered() { + return new Position(0.5f, 0.5f, Anchor.CENTER); + } + + public static Position topLeft() { + return new Position(0.0f, 0.0f, Anchor.TOP_LEFT); + } +} + +// Usage +entity.add(Position.centered()); // Clear and concise +``` + +--- + +## Real-World Examples + +### Example 1: Card Entity Creation + +**File**: `src/main/java/net/luxsolari/game/states/GameplayState.java` + +```java +private void createRandomCardEntity() { + Random random = new Random(); + + // Create entity + Entity cardEntity = MasterSubsystem.INSTANCE.getEntityPool().create(); + + // Add Card component (game logic data) + Card.Rank rank = Card.Rank.values()[random.nextInt(Card.Rank.values().length - 1)]; + Card.Suit suit = Card.Suit.values()[random.nextInt(Card.Suit.values().length)]; + cardEntity.add(new Card(rank, suit)); + + // Add Position component (relative positioning) + float randomX = random.nextFloat(); + float randomY = random.nextFloat(); + cardEntity.add(new Position(randomX, randomY, Anchor.TOP_LEFT)); + + // Add Layer component (Z-order) + cardEntity.add(new Layer(CARD_LAYER)); + + // Add CardSprite component (visual representation) + String[] cardFace = CardArt.fromCard(new Card(rank, suit), CardSizeTier.MEDIUM); + cardEntity.add(new CardSprite(cardFace)); + + LOGGER.info("Created card entity: " + rank + " of " + suit); +} +``` + +### Example 2: DisplayListSystem + +**File**: `src/main/java/net/luxsolari/engine/ecs/systems/DisplayListSystem.java` + +```java +public class DisplayListSystem implements EcsSystem { + + @Override + public void update(double dt, EntityPool pool) { + if (!RenderSubsystem.INSTANCE.ready()) { + return; + } + + List list = new ArrayList<>(); + ViewportManager viewport = ViewportManager.INSTANCE; + + // Render single-glyph visuals + pool.with(Position.class, Visual.class, Layer.class).forEach(entity -> { + Position p = entity.get(Position.class); + Visual v = entity.get(Visual.class); + Layer l = entity.get(Layer.class); + + int screenX = viewport.toScreenX(p.relX(), p.anchor()); + int screenY = viewport.toScreenY(p.relY(), p.anchor()); + + list.add(new RenderCmd(l.index(), screenX, screenY, v.glyph())); + }); + + // Render multi-cell card sprites + pool.with(Position.class, Layer.class, CardSprite.class).forEach(entity -> { + Position p = entity.get(Position.class); + Layer l = entity.get(Layer.class); + CardSprite sprite = entity.get(CardSprite.class); + String[] art = sprite.current(); + + int screenX = viewport.toScreenX(p.relX(), p.anchor(), sprite.cols()); + int screenY = viewport.toScreenY(p.relY(), p.anchor(), sprite.rows()); + + // Render each cell + for (int row = 0; row < sprite.rows(); row++) { + String line = art[row]; + for (int col = 0; col < sprite.cols(); col++) { + char ch = line.charAt(col); + list.add(new RenderCmd( + l.index(), + screenX + col, + screenY + row, + TextCharacter.fromCharacter(ch)[0] + )); + } + } + }); + + RenderManager.submitDisplayList(list); + } +} +``` + +### Example 3: Component Definitions + +**Engine Components** (`net.luxsolari.engine.ecs`): + +```java +// Position with relative coordinates +public record Position(float relX, float relY, Anchor anchor) implements Component { + public Position(float relX, float relY) { + this(relX, relY, Anchor.TOP_LEFT); + } +} + +// Single-glyph visual +public record Visual(TextCharacter glyph) implements Component {} + +// Render layer for Z-ordering +public record Layer(int index) implements Component { + public Layer { + if (index < 0) { + throw new IllegalArgumentException("Layer index must be non-negative"); + } + } +} +``` + +**Game Components** (`net.luxsolari.game.ecs`): + +```java +// Playing card data +public record Card(Rank rank, Suit suit) implements Component { + public enum Suit { + SPADES('♠'), HEARTS('♥'), DIAMONDS('♦'), CLUBS('♣'); + private final char symbol; + Suit(char symbol) { this.symbol = symbol; } + public char symbol() { return symbol; } + } + + public enum Rank { + A("A"), TWO("2"), /* ... */ K("K"); + private final String label; + Rank(String label) { this.label = label; } + public String label() { return label; } + } +} +``` + +--- + +## Common Patterns + +### Pattern 1: Tagging Components + +Use empty components as tags/flags: + +```java +// Tag component (no data) +public record PlayerOwned() implements Component {} + +public record AI() implements Component {} + +// Query for player's cards +List playerCards = entityPool.with(Card.class, PlayerOwned.class); + +// Query for AI's cards +List aiCards = entityPool.with(Card.class, AI.class); +``` + +### Pattern 2: State Components + +Use components to represent entity state: + +```java +public record FaceUp() implements Component {} +public record FaceDown() implements Component {} + +// Flip card face up +if (cardEntity.has(FaceDown.class)) { + cardEntity.add(new FaceUp()); // Replaces FaceDown +} + +// Query face-up cards +List faceUpCards = entityPool.with(Card.class, FaceUp.class); +``` + +### Pattern 3: Animation Components + +```java +public record Animation( + int currentFrame, + int totalFrames, + double frameTime, + double elapsed +) implements Component {} + +// Animation system +public class AnimationSystem implements EcsSystem { + public void update(double dt, EntityPool pool) { + pool.with(Animation.class, Visual.class).forEach(entity -> { + Animation anim = entity.get(Animation.class); + double newElapsed = anim.elapsed() + dt; + + if (newElapsed >= anim.frameTime()) { + int nextFrame = (anim.currentFrame() + 1) % anim.totalFrames(); + entity.add(new Animation( + nextFrame, + anim.totalFrames(), + anim.frameTime(), + 0.0 + )); + + // Update visual for new frame + updateVisualForFrame(entity, nextFrame); + } else { + entity.add(new Animation( + anim.currentFrame(), + anim.totalFrames(), + anim.frameTime(), + newElapsed + )); + } + }); + } +} +``` + +### Pattern 4: Lifetime Components + +```java +public record Lifetime(double remaining) implements Component {} + +// Lifetime system (remove entities after time expires) +public class LifetimeSystem implements EcsSystem { + public void update(double dt, EntityPool pool) { + pool.with(Lifetime.class).forEach(entity -> { + Lifetime lifetime = entity.get(Lifetime.class); + double newRemaining = lifetime.remaining() - dt; + + if (newRemaining <= 0) { + // Entity expired, mark for removal + entity.add(new Dead()); + } else { + entity.add(new Lifetime(newRemaining)); + } + }); + + // Remove dead entities + pool.removeWith(Dead.class); + } +} + +public record Dead() implements Component {} // Tag for removal +``` + +### Pattern 5: Parent-Child Relationships + +```java +public record Parent(int entityId) implements Component {} +public record Child(int entityId) implements Component {} + +// Create parent-child relationship +Entity parent = entityPool.create(); +Entity child = entityPool.create(); + +child.add(new Parent(parent.id())); +parent.add(new Child(child.id())); + +// Find children of a parent +int parentId = parent.id(); +List children = entityPool.all().stream() + .filter(e -> e.has(Parent.class) && e.get(Parent.class).entityId() == parentId) + .toList(); +``` + +--- + +## Troubleshooting + +### Entities Not Rendering + +**Symptom**: Created entities don't appear on screen + +**Solutions**: +1. Ensure entity has Position, Visual/CardSprite, and Layer components: +```java +entity.add(new Position(0.5f, 0.5f, Anchor.CENTER)); +entity.add(new Visual(glyph)); +entity.add(new Layer(2)); +``` + +2. Check layer index is valid (0 ≤ index < MAX_LAYERS) +3. Verify DisplayListSystem is being updated each frame +4. Check position is within screen bounds (0.0-1.0) + +### Component Not Found (null) + +**Symptom**: `entity.get(SomeComponent.class)` returns null + +**Solutions**: +1. Check component was added: `entity.has(SomeComponent.class)` +2. Verify correct component type in query +3. Component may have been replaced by newer add() call + +### Query Returns Empty List + +**Symptom**: `entityPool.with(...)` returns empty list + +**Solutions**: +1. Verify entities actually have ALL specified components +2. Check entities were added to the pool: `entityPool.create()` +3. Ensure entities weren't removed: `entityPool.removeWith(...)` +4. Use `entityPool.all()` to verify entities exist + +### Memory Leaks + +**Symptom**: Memory usage grows over time + +**Solutions**: +1. Remove entities when no longer needed: +```java +@Override +public void end() { + entityPool.removeWith(Card.class); // Clean up cards +} +``` + +2. Clear entire pool between states: +```java +// In state transition +entityPool = new EntityPool(); // Replace with fresh pool +``` + +### Entity ID Conflicts + +**Symptom**: Entities have duplicate IDs + +**Solution**: This shouldn't happen. IDs are auto-incremented. If it does: +1. Don't manually set entity IDs +2. Always use `entityPool.create()` to create entities +3. Check for static ID counters being shared + +### System Order Issues + +**Symptom**: Systems updating in wrong order causes bugs + +**Solution**: Control system execution order: +```java +@Override +public void update() { + double dt = 0.125; + + // Systems run in order + inputSystem.update(dt, entityPool); + physicsSystem.update(dt, entityPool); + collisionSystem.update(dt, entityPool); + displayListSystem.update(dt, entityPool); // Rendering last +} +``` + +--- + +## Additional Resources + +- **Architecture Documentation**: See `ARCHITECTURE.md` for overall system design +- **State Machine Guide**: See `STATE_MACHINE_GUIDE.md` for state management +- **Rendering Guide**: See `RENDERING_GUIDE.md` for rendering integration +- **Developer Guide**: See `DEVELOPER_GUIDE.md` for development workflow + +**Source Code References**: +- Entity: `src/main/java/net/luxsolari/engine/ecs/Entity.java` +- Component: `src/main/java/net/luxsolari/engine/ecs/Component.java` +- EcsSystem: `src/main/java/net/luxsolari/engine/ecs/EcsSystem.java` +- EntityPool: `src/main/java/net/luxsolari/engine/ecs/EntityPool.java` +- DisplayListSystem: `src/main/java/net/luxsolari/engine/ecs/systems/DisplayListSystem.java` +- Example components: `src/main/java/net/luxsolari/game/ecs/` + +--- + +*Last Updated: 2025* +*For Console Jack - Terminal-based Blackjack Game* diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md index bafe0eb..6f323ce 100644 --- a/docs/IMPROVEMENT_PLAN.md +++ b/docs/IMPROVEMENT_PLAN.md @@ -1,149 +1,149 @@ -# Console-Jack Improvement Plan - -## 1. Testing Strategy - -### 1.1 Unit Testing Framework - -- [ ] Set up JUnit 5 with AssertJ for assertions -- [ ] Add Mockito for mocking dependencies -- [ ] Configure JaCoCo for code coverage reporting - -### 1.2 Test Coverage Goals - -- [ ] Core ECS components: 90%+ coverage -- [ ] Subsystems (Render, Audio, Input): 85%+ coverage -- [ ] Game states: 80%+ coverage -- [ ] Utility classes: 95%+ coverage - -### 1.3 Test Categories - -- [ ] Unit tests for individual components -- [ ] Integration tests for system interactions -- [ ] Performance tests for critical paths -- [ ] Mock-based tests for external dependencies - -## 2. Documentation Enhancement - -### 2.1 Code Documentation - -- [ ] Add missing Javadoc to all public APIs -- [ ] Document thread safety guarantees -- [ ] Add package-info.java for each package - -### 2.2 Architectural Documentation - -- [ ] Create ARCHITECTURE.md with high-level design -- [ ] Document ECS architecture and component lifecycle -- [ ] Add sequence diagrams for critical flows -- [ ] Document threading model and concurrency approach - -### 2.3 Developer Documentation - -- [ ] Add CONTRIBUTING.md with coding standards -- [ ] Document build and test process -- [ ] Add performance profiling guide - -## 3. Thread Safety Improvements - -### 3.1 Concurrency Analysis - -- [ ] Perform thread safety audit of ECS implementation -- [ ] Identify potential race conditions -- [ ] Document thread-safety guarantees - -### 3.2 Thread Safety Measures - -- [ ] Add `@ThreadSafe` and `@Immutable` annotations -- [ ] Implement proper synchronization for shared state -- [ ] Consider using `java.util.concurrent` utilities -- [ ] Add thread-safety tests - -### 3.3 Performance Optimization - -- [ ] Implement object pooling for frequently created objects -- [ ] Optimize ECS queries and iterations -- [ ] Profile and optimize hot code paths - -## 4. Resource Management - -### 4.1 Resource Lifecycle - -- [ ] Audit all resource-owning classes -- [ ] Implement `AutoCloseable` where appropriate -- [ ] Add try-with-resources for all resource usage - -### 4.2 Memory Management - -- [ ] Implement object pooling for high-frequency objects -- [ ] Add memory leak detection in tests -- [ ] Profile memory usage under load - -### 4.3 Asset Management - -- [ ] Centralize asset loading/unloading -- [ ] Add resource caching strategy -- [ ] Implement proper error handling for missing resources - -## 5. Performance Optimization - -### 5.1 Profiling and Metrics - -- [ ] Add JMH benchmarks for critical paths -- [ ] Implement performance metrics collection -- [ ] Set up performance regression testing - -### 5.2 Optimization Targets - -- [ ] Optimize ECS component access patterns -- [ ] Reduce object allocations in game loop -- [ ] Optimize rendering pipeline - -### 5.3 Memory Optimization - -- [ ] Use primitive collections where appropriate -- [ ] Reduce object churn in hot paths -- [ ] Implement object pooling for expensive objects - -## Implementation Phases - -### Phase 1: Foundation (Weeks 1-2) - -- [ ] Set up testing infrastructure -- [ ] Add basic test coverage for critical paths -- [ ] Document current architecture - -### Phase 2: Core Improvements (Weeks 3-4) - -- [ ] Implement thread safety improvements -- [ ] Add resource management -- [ ] Optimize critical paths - -### Phase 3: Polish and Validation (Weeks 5-6) - -- [ ] Complete test coverage -- [ ] Performance tuning -- [ ] Documentation updates - -## Success Metrics - -1. Test coverage > 80% for all critical components -2. No thread safety issues detected in stress tests -3. 95% of resources properly managed with try-with-resources -4. 30% reduction in object allocations during gameplay -5. All public APIs fully documented - -## Monitoring and Maintenance - -- [ ] Set up CI/CD with test coverage reporting -- [ ] Add performance regression tests -- [ ] Schedule regular architecture reviews -- [ ] Monitor memory usage in production - -## Risk Management - -| Risk | Impact | Mitigation | -|------|--------|------------| -| Performance regression | High | Regular performance testing | -| Thread safety issues | Critical | Code reviews and stress testing | -| Resource leaks | High | Static analysis and testing | -| Documentation drift | Medium | Regular documentation reviews | +# Console-Jack Improvement Plan + +## 1. Testing Strategy + +### 1.1 Unit Testing Framework + +- [ ] Set up JUnit 5 with AssertJ for assertions +- [ ] Add Mockito for mocking dependencies +- [ ] Configure JaCoCo for code coverage reporting + +### 1.2 Test Coverage Goals + +- [ ] Core ECS components: 90%+ coverage +- [ ] Subsystems (Render, Audio, Input): 85%+ coverage +- [ ] Game states: 80%+ coverage +- [ ] Utility classes: 95%+ coverage + +### 1.3 Test Categories + +- [ ] Unit tests for individual components +- [ ] Integration tests for system interactions +- [ ] Performance tests for critical paths +- [ ] Mock-based tests for external dependencies + +## 2. Documentation Enhancement + +### 2.1 Code Documentation + +- [ ] Add missing Javadoc to all public APIs +- [ ] Document thread safety guarantees +- [ ] Add package-info.java for each package + +### 2.2 Architectural Documentation + +- [ ] Create ARCHITECTURE.md with high-level design +- [ ] Document ECS architecture and component lifecycle +- [ ] Add sequence diagrams for critical flows +- [ ] Document threading model and concurrency approach + +### 2.3 Developer Documentation + +- [ ] Add CONTRIBUTING.md with coding standards +- [ ] Document build and test process +- [ ] Add performance profiling guide + +## 3. Thread Safety Improvements + +### 3.1 Concurrency Analysis + +- [ ] Perform thread safety audit of ECS implementation +- [ ] Identify potential race conditions +- [ ] Document thread-safety guarantees + +### 3.2 Thread Safety Measures + +- [ ] Add `@ThreadSafe` and `@Immutable` annotations +- [ ] Implement proper synchronization for shared state +- [ ] Consider using `java.util.concurrent` utilities +- [ ] Add thread-safety tests + +### 3.3 Performance Optimization + +- [ ] Implement object pooling for frequently created objects +- [ ] Optimize ECS queries and iterations +- [ ] Profile and optimize hot code paths + +## 4. Resource Management + +### 4.1 Resource Lifecycle + +- [ ] Audit all resource-owning classes +- [ ] Implement `AutoCloseable` where appropriate +- [ ] Add try-with-resources for all resource usage + +### 4.2 Memory Management + +- [ ] Implement object pooling for high-frequency objects +- [ ] Add memory leak detection in tests +- [ ] Profile memory usage under load + +### 4.3 Asset Management + +- [ ] Centralize asset loading/unloading +- [ ] Add resource caching strategy +- [ ] Implement proper error handling for missing resources + +## 5. Performance Optimization + +### 5.1 Profiling and Metrics + +- [ ] Add JMH benchmarks for critical paths +- [ ] Implement performance metrics collection +- [ ] Set up performance regression testing + +### 5.2 Optimization Targets + +- [ ] Optimize ECS component access patterns +- [ ] Reduce object allocations in game loop +- [ ] Optimize rendering pipeline + +### 5.3 Memory Optimization + +- [ ] Use primitive collections where appropriate +- [ ] Reduce object churn in hot paths +- [ ] Implement object pooling for expensive objects + +## Implementation Phases + +### Phase 1: Foundation (Weeks 1-2) + +- [ ] Set up testing infrastructure +- [ ] Add basic test coverage for critical paths +- [ ] Document current architecture + +### Phase 2: Core Improvements (Weeks 3-4) + +- [ ] Implement thread safety improvements +- [ ] Add resource management +- [ ] Optimize critical paths + +### Phase 3: Polish and Validation (Weeks 5-6) + +- [ ] Complete test coverage +- [ ] Performance tuning +- [ ] Documentation updates + +## Success Metrics + +1. Test coverage > 80% for all critical components +2. No thread safety issues detected in stress tests +3. 95% of resources properly managed with try-with-resources +4. 30% reduction in object allocations during gameplay +5. All public APIs fully documented + +## Monitoring and Maintenance + +- [ ] Set up CI/CD with test coverage reporting +- [ ] Add performance regression tests +- [ ] Schedule regular architecture reviews +- [ ] Monitor memory usage in production + +## Risk Management + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Performance regression | High | Regular performance testing | +| Thread safety issues | Critical | Code reviews and stress testing | +| Resource leaks | High | Static analysis and testing | +| Documentation drift | Medium | Regular documentation reviews | diff --git a/docs/INPUT_SYSTEM_GUIDE.md b/docs/INPUT_SYSTEM_GUIDE.md new file mode 100644 index 0000000..0435e72 --- /dev/null +++ b/docs/INPUT_SYSTEM_GUIDE.md @@ -0,0 +1,1145 @@ +# Console Jack - Input System Guide + +A comprehensive guide to the command-based input system with context-sensitive key bindings. + +## Table of Contents + +- [Overview](#overview) +- [Core Concepts](#core-concepts) +- [Architecture](#architecture) +- [Quick Start](#quick-start) +- [Input Components](#input-components) + - [InputCommand](#inputcommand) + - [KeyBinding](#keybinding) + - [InputContext](#inputcontext) + - [InputResult](#inputresult) + - [InputManager](#inputmanager) +- [Creating Input Contexts](#creating-input-contexts) +- [Using Input in Game States](#using-input-in-game-states) +- [Best Practices](#best-practices) +- [Real-World Examples](#real-world-examples) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The Console Jack input system provides a **command-based architecture** that decouples raw keyboard input from game logic. Instead of directly checking for specific keys, game states work with high-level commands (like `PAUSE`, `CONFIRM`, `HIT`) that can be bound to different keys in different contexts. + +### Key Benefits + +- **Context-Sensitive**: Different game states can map the same key to different commands +- **Flexible Remapping**: Easy to change key bindings without modifying game logic +- **Clean Separation**: Game logic works with commands, not raw KeyStrokes +- **Maintainable**: All bindings for a state are defined in one place +- **Type-Safe**: Enum-based commands prevent typos and enable IDE autocomplete + +--- + +## Core Concepts + +### Commands vs KeyStrokes + +**KeyStroke** (Low-level): Raw keyboard input from Lanterna +```java +KeyStroke ks = InputManager.poll(); +if (ks.getKeyType() == KeyType.Enter) { ... } // ❌ Tightly coupled to specific keys +``` + +**InputCommand** (High-level): Semantic game actions +```java +InputResult input = InputManager.pollCommand(); +if (input.command() == InputCommand.CONFIRM) { ... } // ✅ Decoupled from specific keys +``` + +### Input Contexts + +Each game state has its own `InputContext` that defines how keys map to commands. The same key can mean different things in different states: + +- Main Menu: `Q` → `QUIT` (exit application) +- Gameplay: `Q` → `PAUSE` (pause game) +- Pause Menu: `Q` → `BACK` (return to main menu) + +### Input Flow + +``` +User presses key + ↓ +InputSubsystem (polls Lanterna) + ↓ +InputManager.pollCommand() + ↓ +Current InputContext (resolves KeyBinding → InputCommand) + ↓ +InputResult (contains both KeyStroke and Command) + ↓ +Game State (handles command) +``` + +--- + +## Architecture + +### Package Structure + +``` +net.luxsolari.engine.input/ +├── InputCommand.java # Enum of all game commands +├── KeyBinding.java # Immutable key + modifiers record +├── InputContext.java # Interface for key-to-command mapping +└── InputResult.java # Wrapper for keystroke + command + +net.luxsolari.engine.manager/ +└── InputManager.java # Public facade for input system + +net.luxsolari.game.input/ +├── MainMenuInputContext.java # Main menu bindings +├── GameplayInputContext.java # Gameplay bindings +└── PauseInputContext.java # Pause menu bindings +``` + +### Threading Model + +The input system runs on a dedicated thread managed by `InputSubsystem`: +- **Input Thread**: Polls Lanterna for keystrokes, queues them in thread-safe deque +- **Master Thread**: Game states poll for input via `InputManager` during their `handleInput()` method + +--- + +## Quick Start + +### 1. Set Context in State + +```java +public class MyGameState implements LoopableState { + @Override + public void start() { + // Set the input context for this state + InputManager.setContext(new MyGameInputContext()); + } + + @Override + public void resume() { + // Re-set context when resuming from overlay state + InputManager.setContext(new MyGameInputContext()); + } +} +``` + +### 2. Poll for Commands + +```java +@Override +public void handleInput() { + if (!renderReady()) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || !input.hasCommand()) { + return; + } + + // Handle commands + switch (input.command()) { + case PAUSE -> StateMachineManager.push(new PauseState()); + case QUIT -> MasterSubsystem.INSTANCE.stop(); + case CONFIRM -> selectMenuItem(); + default -> {} + } +} +``` + +### 3. Create Your InputContext + +```java +public class MyGameInputContext implements InputContext { + private static final Map BINDINGS = Map.ofEntries( + Map.entry(KeyBinding.of('P'), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.Escape), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.Enter), InputCommand.CONFIRM) + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "MyGame"; + } +} +``` + +--- + +## Input Components + +### InputCommand + +**Location**: `net.luxsolari.engine.input.InputCommand` + +Enum defining all input commands recognized by the game. Commands are context-independent and semantic. + +#### Available Commands + +```java +// Global Commands +QUIT // Quit application +BACK // Go back/cancel +CONFIRM // Confirm selection +CANCEL // Cancel action + +// Navigation Commands +NAVIGATE_UP +NAVIGATE_DOWN +NAVIGATE_LEFT +NAVIGATE_RIGHT +NAVIGATE_FIRST +NAVIGATE_LAST + +// Game State Commands +PAUSE +RESUME + +// Blackjack Commands +HIT +STAND +DOUBLE_DOWN +SPLIT +SURRENDER + +// Debug Commands +DEBUG_CREATE_CARD +DEBUG_CLEAR_CARDS +DEBUG_TOGGLE + +// Audio Commands +TOGGLE_SOUND +VOLUME_UP +VOLUME_DOWN + +// UI Commands +TOGGLE_FULLSCREEN +``` + +#### Adding New Commands + +1. Add to `InputCommand` enum: +```java +public enum InputCommand { + // ... existing commands + NEW_FEATURE, // Your new command +} +``` + +2. Bind in relevant `InputContext` implementations +3. Handle in game state's `handleInput()` method + +--- + +### KeyBinding + +**Location**: `net.luxsolari.engine.input.KeyBinding` + +Immutable record representing a key combination with modifiers. Used as map keys for resolving keystrokes to commands. + +#### Structure + +```java +public record KeyBinding( + KeyType keyType, + Character character, + boolean ctrlPressed, + boolean altPressed, + boolean shiftPressed +) +``` + +#### Factory Methods + +```java +// Character key without modifiers +KeyBinding.of('A') // 'A' key +KeyBinding.of('q') // 'Q' key (normalized to uppercase) + +// Character key with modifiers +KeyBinding.of('Q', true, false, false) // Ctrl+Q +KeyBinding.of('F', false, true, false) // Alt+F +KeyBinding.of('S', true, true, false) // Ctrl+Alt+S + +// Special key without modifiers +KeyBinding.of(KeyType.Enter) +KeyBinding.of(KeyType.Escape) +KeyBinding.of(KeyType.ArrowUp) +KeyBinding.of(KeyType.Home) + +// Special key with modifiers +KeyBinding.of(KeyType.F1, true, false, false) // Ctrl+F1 + +// From Lanterna KeyStroke +KeyStroke ks = new KeyStroke(KeyType.Enter); +KeyBinding.fromKeyStroke(ks) +``` + +#### Character Normalization + +Character keys are automatically normalized to uppercase for consistency: +```java +KeyBinding.of('a').equals(KeyBinding.of('A')) // true +``` + +#### String Representation + +```java +KeyBinding.of('Q').toString() // "Q" +KeyBinding.of('Q', true, false, false).toString() // "Ctrl+Q" +KeyBinding.of(KeyType.Enter).toString() // "Enter" +KeyBinding.of('F', false, true, false).toString() // "Alt+F" +``` + +--- + +### InputContext + +**Location**: `net.luxsolari.engine.input.InputContext` + +Interface defining context-specific key bindings for a game state. Each state provides its own implementation. + +#### Interface + +```java +public interface InputContext { + /** + * Returns the key-to-command mapping for this context. + */ + Map getBindings(); + + /** + * Resolves a key binding to its command in this context. + */ + default InputCommand resolve(KeyBinding binding) { + return getBindings().get(binding); + } + + /** + * Returns the name of this context for debugging. + */ + default String getName() { + return this.getClass().getSimpleName(); + } +} +``` + +#### Implementation Pattern + +```java +public class ExampleInputContext implements InputContext { + + // Immutable map of bindings (created once) + private static final Map BINDINGS = Map.ofEntries( + Map.entry(KeyBinding.of('P'), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.Escape), InputCommand.BACK), + Map.entry(KeyBinding.of(KeyType.Enter), InputCommand.CONFIRM) + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "Example"; // Optional: custom name for debugging + } +} +``` + +--- + +### InputResult + +**Location**: `net.luxsolari.engine.input.InputResult` + +Wrapper record containing both the raw keystroke and resolved command. Returned by `InputManager.pollCommand()`. + +#### Structure + +```java +public record InputResult(KeyStroke keyStroke, InputCommand command) { + public boolean hasCommand(); // true if command is not null + public boolean hasKeyStroke(); // true if keyStroke is not null +} +``` + +#### Usage + +```java +InputResult input = InputManager.pollCommand(); +if (input == null) { + return; // No input available +} + +// Check if a command was resolved +if (input.hasCommand()) { + switch (input.command()) { + case PAUSE -> pauseGame(); + case QUIT -> quitGame(); + } +} + +// Access raw keystroke if needed +if (input.hasKeyStroke()) { + KeyType keyType = input.keyStroke().getKeyType(); + // ... low-level handling +} +``` + +--- + +### InputManager + +**Location**: `net.luxsolari.engine.manager.InputManager` + +Public facade providing command-based input handling. Game states interact exclusively with this manager. + +#### API + +```java +// Context Management +InputManager.setContext(InputContext context) // Set current input context +InputManager.getContext() // Get current context + +// Input Polling +InputManager.pollCommand() // High-level: returns InputResult +InputManager.poll() // Low-level: returns raw KeyStroke + +// Status +InputManager.ready() // Check if input system is ready +``` + +#### Primary Method: pollCommand() + +```java +InputResult input = InputManager.pollCommand(); +``` + +**Returns**: `InputResult` containing: +- `keyStroke`: Raw Lanterna KeyStroke +- `command`: Resolved InputCommand (may be null if key has no binding) + +**Returns null**: If no input is available + +--- + +## Creating Input Contexts + +### Step 1: Determine Required Commands + +List all commands your state needs to handle: +``` +- Navigation (up/down/first/last) +- Confirm selection +- Pause game +- Quit application +``` + +### Step 2: Choose Key Bindings + +Map each command to one or more keys: +``` +NAVIGATE_UP → Arrow Up +NAVIGATE_DOWN → Arrow Down +NAVIGATE_FIRST → Home +NAVIGATE_LAST → End +CONFIRM → Enter +PAUSE → P, Escape +QUIT → Q, Ctrl+Q, EOF (Ctrl+D) +``` + +### Step 3: Implement InputContext + +```java +package net.luxsolari.game.input; + +import com.googlecode.lanterna.input.KeyType; +import java.util.Map; +import net.luxsolari.engine.input.InputCommand; +import net.luxsolari.engine.input.InputContext; +import net.luxsolari.engine.input.KeyBinding; + +public class MyStateInputContext implements InputContext { + + private static final Map BINDINGS = Map.ofEntries( + // Navigation + Map.entry(KeyBinding.of(KeyType.ArrowUp), InputCommand.NAVIGATE_UP), + Map.entry(KeyBinding.of(KeyType.ArrowDown), InputCommand.NAVIGATE_DOWN), + Map.entry(KeyBinding.of(KeyType.Home), InputCommand.NAVIGATE_FIRST), + Map.entry(KeyBinding.of(KeyType.End), InputCommand.NAVIGATE_LAST), + + // Actions + Map.entry(KeyBinding.of(KeyType.Enter), InputCommand.CONFIRM), + + // Control + Map.entry(KeyBinding.of('P'), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.Escape), InputCommand.PAUSE), + + // Quit (multiple bindings for same command) + Map.entry(KeyBinding.of('Q'), InputCommand.QUIT), + Map.entry(KeyBinding.of('Q', true, false, false), InputCommand.QUIT), // Ctrl+Q + Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT) // Ctrl+D + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "MyState"; + } +} +``` + +### Step 4: Use in Game State + +```java +public class MyState implements LoopableState { + @Override + public void start() { + InputManager.setContext(new MyStateInputContext()); + } + + @Override + public void resume() { + InputManager.setContext(new MyStateInputContext()); + } + + @Override + public void handleInput() { + InputResult input = InputManager.pollCommand(); + if (input == null || !input.hasCommand()) { + return; + } + + switch (input.command()) { + case NAVIGATE_UP -> navigateUp(); + case NAVIGATE_DOWN -> navigateDown(); + case CONFIRM -> confirm(); + case PAUSE -> StateMachineManager.push(new PauseState()); + case QUIT -> MasterSubsystem.INSTANCE.stop(); + default -> {} + } + } +} +``` + +--- + +## Using Input in Game States + +### State Lifecycle Integration + +```java +public class GameplayState implements LoopableState { + + @Override + public void start() { + // Set context when state starts + InputManager.setContext(new GameplayInputContext()); + } + + @Override + public void pause() { + // Context remains set, but state won't receive input + } + + @Override + public void resume() { + // Re-set context when resuming + InputManager.setContext(new GameplayInputContext()); + } + + @Override + public void handleInput() { + // Check render system is ready + if (!renderReady()) { + return; + } + + // Poll for input + InputResult input = InputManager.pollCommand(); + if (input == null) { + return; // No input available + } + + // Handle unbound keys if needed + if (!input.hasCommand()) { + // Raw keystroke available but no command binding + // Usually just ignore + return; + } + + // Handle commands + switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); + case PAUSE -> StateMachineManager.push(new PauseState()); + case HIT -> handleHit(); + case STAND -> handleStand(); + default -> {} + } + } + + @Override + public void end() { + // Optional: clear context + InputManager.setContext(null); + } +} +``` + +### Handling Input Results + +#### Pattern 1: Command-Only Handling + +```java +InputResult input = InputManager.pollCommand(); +if (input == null || !input.hasCommand()) { + return; +} + +switch (input.command()) { + case PAUSE -> pauseGame(); + case QUIT -> quitGame(); +} +``` + +#### Pattern 2: Hybrid Handling (Command + Raw) + +```java +InputResult input = InputManager.pollCommand(); +if (input == null) { + return; +} + +// Handle bound commands +if (input.hasCommand()) { + switch (input.command()) { + case PAUSE -> pauseGame(); + case QUIT -> quitGame(); + } + return; +} + +// Handle unbound keys (raw keystroke) +if (input.hasKeyStroke()) { + KeyType keyType = input.keyStroke().getKeyType(); + if (keyType == KeyType.Character) { + // Handle typed characters for text input, etc. + } +} +``` + +#### Pattern 3: Fallback to UI Components + +```java +InputResult input = InputManager.pollCommand(); +if (input == null) { + return; +} + +// Try command handling first +if (input.hasCommand()) { + boolean handled = handleCommand(input.command()); + if (handled) { + return; + } +} + +// Fall back to raw keystroke for UI components +if (input.hasKeyStroke() && menu != null) { + menu.handleInput(input.keyStroke()); +} +``` + +--- + +## Best Practices + +### 1. Always Set Context in start() and resume() + +```java +@Override +public void start() { + InputManager.setContext(new MyInputContext()); // ✅ Set on start +} + +@Override +public void resume() { + InputManager.setContext(new MyInputContext()); // ✅ Re-set on resume +} +``` + +**Why**: When states are pushed/popped, the previous state needs to restore its context. + +### 2. Check for Null Before Using Input + +```java +InputResult input = InputManager.pollCommand(); +if (input == null) { // ✅ Check for null + return; +} +if (!input.hasCommand()) { // ✅ Check for command + return; +} +// Now safe to use input.command() +``` + +### 3. Use Static Final Maps for Bindings + +```java +private static final Map BINDINGS = Map.ofEntries(...); // ✅ +// NOT: private Map bindings = new HashMap<>(); // ❌ +``` + +**Why**: Immutable, created once, thread-safe, memory efficient. + +### 4. Provide Multiple Bindings for Important Actions + +```java +// Quit command bound to multiple keys +Map.entry(KeyBinding.of('Q'), InputCommand.QUIT), +Map.entry(KeyBinding.of('Q', true, false, false), InputCommand.QUIT), // Ctrl+Q +Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT) // Ctrl+D +``` + +**Why**: Users have different preferences, increases accessibility. + +### 5. Use Semantic Command Names + +```java +// Good: Semantic, describes intent +InputCommand.HIT +InputCommand.STAND +InputCommand.CONFIRM + +// Bad: Implementation-focused +InputCommand.PRESS_H +InputCommand.ENTER_KEY +``` + +### 6. Handle QUIT and EOF Consistently + +```java +// Always handle EOF (Ctrl+D on Unix, Ctrl+Z on Windows) +Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT), + +// In handleInput(): +switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); // Proper shutdown +} +``` + +### 7. Document Key Bindings for Users + +```java +// In UI or help screen +instructionLabels.add(new Label(0, 0, "Press P or Esc to pause")); +instructionLabels.add(new Label(0, 0, "Press H to Hit, S to Stand")); +instructionLabels.add(new Label(0, 0, "Press Q or Ctrl+Q to quit")); +``` + +### 8. One Context Per State Class + +```java +// Good: Clear ownership +public class MainMenuState implements LoopableState { + InputManager.setContext(new MainMenuInputContext()); +} + +public class GameplayState implements LoopableState { + InputManager.setContext(new GameplayInputContext()); +} +``` + +--- + +## Real-World Examples + +### Example 1: Main Menu Input Context + +**File**: `src/main/java/net/luxsolari/game/input/MainMenuInputContext.java` + +```java +public class MainMenuInputContext implements InputContext { + + private static final Map BINDINGS = Map.ofEntries( + // Navigation + Map.entry(KeyBinding.of(KeyType.ArrowUp), InputCommand.NAVIGATE_UP), + Map.entry(KeyBinding.of(KeyType.ArrowDown), InputCommand.NAVIGATE_DOWN), + Map.entry(KeyBinding.of(KeyType.Home), InputCommand.NAVIGATE_FIRST), + Map.entry(KeyBinding.of(KeyType.End), InputCommand.NAVIGATE_LAST), + + // Actions + Map.entry(KeyBinding.fromKeyStroke(new KeyStroke(KeyType.Enter)), InputCommand.CONFIRM), + Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT), + + // Shortcuts + Map.entry(KeyBinding.of('Q', true, false, false), InputCommand.QUIT), // Ctrl+Q + Map.entry(KeyBinding.of('Q'), InputCommand.QUIT) // Q to quit + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "MainMenu"; + } +} +``` + +### Example 2: Gameplay Input Context + +**File**: `src/main/java/net/luxsolari/game/input/GameplayInputContext.java` + +```java +public class GameplayInputContext implements InputContext { + + private static final Map BINDINGS = Map.ofEntries( + // Game control + Map.entry(KeyBinding.of('P'), InputCommand.PAUSE), + Map.entry(KeyBinding.of('Q'), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.Escape), InputCommand.PAUSE), + Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT), + Map.entry(KeyBinding.fromKeyStroke(new KeyStroke(KeyType.Enter)), InputCommand.CONFIRM), + + // Blackjack actions + Map.entry(KeyBinding.of('H'), InputCommand.HIT), + Map.entry(KeyBinding.of('S'), InputCommand.STAND), + Map.entry(KeyBinding.of('D'), InputCommand.DOUBLE_DOWN), + Map.entry(KeyBinding.of('X'), InputCommand.SPLIT), + Map.entry(KeyBinding.of('R'), InputCommand.SURRENDER), + Map.entry(KeyBinding.of(' '), InputCommand.HIT), // Space = Hit + + // Debug commands + Map.entry(KeyBinding.of('1'), InputCommand.DEBUG_CREATE_CARD), + Map.entry(KeyBinding.of('2'), InputCommand.DEBUG_CLEAR_CARDS), + Map.entry(KeyBinding.of('`'), InputCommand.DEBUG_TOGGLE), + + // Audio controls + Map.entry(KeyBinding.of('M'), InputCommand.TOGGLE_SOUND), + Map.entry(KeyBinding.of('+'), InputCommand.VOLUME_UP), + Map.entry(KeyBinding.of('-'), InputCommand.VOLUME_DOWN), + Map.entry(KeyBinding.of('='), InputCommand.VOLUME_UP), // = key without shift + + // Fullscreen + Map.entry(KeyBinding.of('F', false, true, false), InputCommand.TOGGLE_FULLSCREEN) // Alt+F + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "Gameplay"; + } +} +``` + +### Example 3: Gameplay State Using Input Context + +**File**: `src/main/java/net/luxsolari/game/states/GameplayState.java` (excerpt) + +```java +public class GameplayState implements LoopableState { + + @Override + public void start() { + LOGGER.info("Gameplay started"); + + // Set input context + InputManager.setContext(new GameplayInputContext()); + + // ... other initialization + } + + @Override + public void resume() { + LOGGER.info("Gameplay resumed"); + + // Re-set input context + InputManager.setContext(new GameplayInputContext()); + } + + @Override + public void handleInput() { + if (!renderReady()) { + return; + } + + // Poll for command-based input + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + // Handle commands + switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); + case PAUSE -> StateMachineManager.push(new PauseState()); + case DEBUG_CREATE_CARD -> createRandomCardEntity(); + case DEBUG_CLEAR_CARDS -> clearCards(); + + // Future blackjack commands + case HIT -> LOGGER.info("Hit command (not yet implemented)"); + case STAND -> LOGGER.info("Stand command (not yet implemented)"); + case DOUBLE_DOWN -> LOGGER.info("Double Down command (not yet implemented)"); + case SPLIT -> LOGGER.info("Split command (not yet implemented)"); + case SURRENDER -> LOGGER.info("Surrender command (not yet implemented)"); + + default -> {} + } + } +} +``` + +### Example 4: Pause Menu Input Context + +**File**: `src/main/java/net/luxsolari/game/input/PauseInputContext.java` + +```java +public class PauseInputContext implements InputContext { + + private static final Map BINDINGS = Map.ofEntries( + // Navigation + Map.entry(KeyBinding.of(KeyType.ArrowUp), InputCommand.NAVIGATE_UP), + Map.entry(KeyBinding.of(KeyType.ArrowDown), InputCommand.NAVIGATE_DOWN), + Map.entry(KeyBinding.of(KeyType.Home), InputCommand.NAVIGATE_FIRST), + Map.entry(KeyBinding.of(KeyType.End), InputCommand.NAVIGATE_LAST), + + // Actions + Map.entry(KeyBinding.fromKeyStroke(new KeyStroke(KeyType.Enter)), InputCommand.CONFIRM), + Map.entry(KeyBinding.of(KeyType.Escape), InputCommand.RESUME), + Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT), + + // Quick shortcuts + Map.entry(KeyBinding.of('P'), InputCommand.RESUME), + Map.entry(KeyBinding.of('R'), InputCommand.RESUME), + Map.entry(KeyBinding.of('Q'), InputCommand.BACK) // Quit to main menu + ); + + @Override + public Map getBindings() { + return BINDINGS; + } + + @Override + public String getName() { + return "Pause"; + } +} +``` + +--- + +## Troubleshooting + +### Input Not Responding + +**Symptom**: Keys don't trigger any actions + +**Solutions**: +1. Check that context is set: +```java +InputContext ctx = InputManager.getContext(); +if (ctx == null) { + // ❌ No context set! + InputManager.setContext(new MyInputContext()); +} +``` + +2. Verify bindings exist: +```java +InputResult input = InputManager.pollCommand(); +if (input != null && !input.hasCommand()) { + // Key pressed but no binding defined + LOGGER.info("Unbound key: " + input.keyStroke()); +} +``` + +3. Check render system is ready: +```java +if (!renderReady()) { + return; // Don't poll input if render not ready +} +``` + +### Commands Not Resolving + +**Symptom**: `input.command()` is always null + +**Solutions**: +1. Ensure KeyBinding matches exactly: +```java +// Character keys are normalized to uppercase +KeyBinding.of('a') // Normalized to 'A' +KeyBinding.of('A') // Already 'A' + +// Both resolve to same binding ✅ +``` + +2. Check modifier flags match: +```java +// These are DIFFERENT bindings: +KeyBinding.of('Q') // Q without modifiers +KeyBinding.of('Q', true, false, false) // Ctrl+Q + +// Bind both if you want both to work +``` + +### Context Not Persisting + +**Symptom**: Context lost when resuming from overlay state + +**Solution**: Always re-set context in `resume()`: +```java +@Override +public void resume() { + InputManager.setContext(new MyInputContext()); // ✅ Re-set +} +``` + +### Same Key, Multiple States + +**Symptom**: Key behavior doesn't change between states + +**Solution**: Each state must set its own context: +```java +// MainMenuState +public void start() { + InputManager.setContext(new MainMenuInputContext()); // Q = QUIT +} + +// GameplayState +public void start() { + InputManager.setContext(new GameplayInputContext()); // Q = PAUSE +} +``` + +### Missing Modifier Detection + +**Symptom**: Ctrl/Alt/Shift modifiers not detected + +**Solution**: Use correct KeyBinding factory method: +```java +// ✅ Correct +KeyBinding.of('Q', true, false, false) // Ctrl+Q + +// ❌ Wrong - ignores Ctrl +KeyBinding.of('Q') +``` + +--- + +## Advanced Topics + +### Dynamic Context Switching + +Some states may need to switch contexts dynamically: + +```java +public class DialogState implements LoopableState { + private boolean inTextMode = false; + + @Override + public void handleInput() { + if (inTextMode) { + InputManager.setContext(new TextInputContext()); + } else { + InputManager.setContext(new DialogNavigationContext()); + } + + // ... handle input + } +} +``` + +### Hierarchical Contexts + +Create base contexts that can be extended: + +```java +public class BaseGameInputContext implements InputContext { + protected static final Map BASE_BINDINGS = Map.ofEntries( + Map.entry(KeyBinding.of(KeyType.EOF), InputCommand.QUIT), + Map.entry(KeyBinding.of('Q', true, false, false), InputCommand.QUIT) + ); +} + +public class SpecificGameInputContext extends BaseGameInputContext { + private static final Map BINDINGS; + + static { + Map combined = new HashMap<>(BASE_BINDINGS); + combined.put(KeyBinding.of('P'), InputCommand.PAUSE); + combined.put(KeyBinding.of('H'), InputCommand.HIT); + BINDINGS = Map.copyOf(combined); + } + + @Override + public Map getBindings() { + return BINDINGS; + } +} +``` + +### Rebindable Keys + +For user-configurable bindings: + +```java +public class ConfigurableInputContext implements InputContext { + private Map bindings; + + public ConfigurableInputContext(Map userBindings) { + this.bindings = Map.copyOf(userBindings); + } + + public void rebind(KeyBinding key, InputCommand command) { + Map newBindings = new HashMap<>(bindings); + newBindings.put(key, command); + this.bindings = Map.copyOf(newBindings); + } + + @Override + public Map getBindings() { + return bindings; + } +} +``` + +--- + +## Additional Resources + +- **Architecture Documentation**: See `ARCHITECTURE.md` for subsystem overview +- **Developer Guide**: See `DEVELOPER_GUIDE.md` for development workflow +- **UI Components Guide**: See `UI_COMPONENTS_GUIDE.md` for UI component input handling + +**Source Code References**: +- Engine input package: `src/main/java/net/luxsolari/engine/input/` +- Game input contexts: `src/main/java/net/luxsolari/game/input/` +- InputManager: `src/main/java/net/luxsolari/engine/manager/InputManager.java` +- InputSubsystem: `src/main/java/net/luxsolari/engine/systems/internal/InputSubsystem.java` + +--- + +*Last Updated: 2025* +*For Console Jack - Terminal-based Blackjack Game* diff --git a/docs/README.md b/docs/README.md index 7efa71f..b389e33 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,29 +1,29 @@ -# Console Jack Documentation - -This directory contains all project documentation for Console Jack. - -## For Developers - -- **[CLAUDE.md](../CLAUDE.md)** - Guidance for AI coding assistants (Claude Code) -- **[ARCHITECT_MODE.md](ARCHITECT_MODE.md)** - Architect mode ruleset for comprehensive design planning -- **[RIPER_MODE.md](RIPER_MODE.md)** - RIPER-5 mode strict operational protocol -- **[DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md)** - Comprehensive development guide -- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Detailed architecture documentation -- **[UI_COMPONENTS_GUIDE.md](UI_COMPONENTS_GUIDE.md)** - UI component framework reference -- **[IMPROVEMENT_PLAN.md](IMPROVEMENT_PLAN.md)** - Planned enhancements and technical debt - -## Technical Documentation - -- **[analysis.md](analysis.md)** - Code analysis and insights - -## Audio Documentation - -- **[audio/README.md](audio/README.md)** - Audio assets documentation - -## Documentation Guidelines - -When adding new documentation: -1. Place it in this `/docs` directory -2. Use descriptive filenames in lowercase with hyphens (e.g., `user-guide.md`) -3. Update this README.md with a brief description and link -4. Keep the root-level README.md focused on quick start information only +# Console Jack Documentation + +This directory contains all project documentation for Console Jack. + +## For Developers + +- **[CLAUDE.md](../CLAUDE.md)** - Guidance for AI coding assistants (Claude Code) +- **[ARCHITECT_MODE.md](ARCHITECT_MODE.md)** - Architect mode ruleset for comprehensive design planning +- **[RIPER_MODE.md](RIPER_MODE.md)** - RIPER-5 mode strict operational protocol +- **[DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md)** - Comprehensive development guide +- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Detailed architecture documentation +- **[UI_COMPONENTS_GUIDE.md](UI_COMPONENTS_GUIDE.md)** - UI component framework reference +- **[IMPROVEMENT_PLAN.md](IMPROVEMENT_PLAN.md)** - Planned enhancements and technical debt + +## Technical Documentation + +- **[analysis.md](analysis.md)** - Code analysis and insights + +## Audio Documentation + +- **[audio/README.md](audio/README.md)** - Audio assets documentation + +## Documentation Guidelines + +When adding new documentation: +1. Place it in this `/docs` directory +2. Use descriptive filenames in lowercase with hyphens (e.g., `user-guide.md`) +3. Update this README.md with a brief description and link +4. Keep the root-level README.md focused on quick start information only diff --git a/docs/RENDERING_GUIDE.md b/docs/RENDERING_GUIDE.md new file mode 100644 index 0000000..6e950f9 --- /dev/null +++ b/docs/RENDERING_GUIDE.md @@ -0,0 +1,898 @@ +# Console Jack - Rendering System Guide + +A comprehensive guide to the rendering system for displaying visuals in Console Jack's terminal interface. + +## Table of Contents + +- [Overview](#overview) +- [Z-Layer System](#z-layer-system) +- [RenderManager API](#rendermanager-api) +- [Viewport and Coordinates](#viewport-and-coordinates) +- [Colors and Styling](#colors-and-styling) +- [Drawing Primitives](#drawing-primitives) +- [High-Level Helpers](#high-level-helpers) +- [ECS Integration](#ecs-integration) +- [Best Practices](#best-practices) +- [Real-World Examples](#real-world-examples) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +Console Jack's rendering system uses **Lanterna** for terminal UI and a custom **Z-layer architecture** for managing draw order. The system is designed for the 8 UPS game loop with thread-safe rendering on a dedicated render thread. + +### Key Benefits + +- **Z-Layer Composition**: Control draw order precisely +- **Thread-Safe**: Rendering happens on dedicated thread +- **Terminal-Agnostic**: Works across different terminal emulators +- **Simple API**: High-level methods for common operations +- **Relative Positioning**: Coordinate system adapts to screen size +- **Color Support**: ANSI and RGB colors + +### Rendering Architecture + +``` +┌──────────────────────────────────────────────────────────┐ +│ Game State (Main Thread) │ +│ - Calls RenderManager.putString(), etc. │ +│ - Submits RenderCmd via DisplayListSystem │ +└───────────────────────┬──────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ RenderManager (Static Facade) │ +│ - putString(), putChar(), drawBox() │ +│ - Manages Z-layers │ +└───────────────────────┬──────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ RenderSubsystem (Render Thread) │ +│ - Composites Z-layers │ +│ - Renders to Lanterna Screen │ +│ - Refreshes display │ +└───────────────────────┬──────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ Terminal (Lanterna) │ +│ - Displays characters on screen │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## Z-Layer System + +The rendering system uses **Z-layers** (depth layers) to control draw order. Lower layer indices are drawn first (background), higher indices are drawn last (foreground). + +### Layer Constants + +```java +// Default layer for UI elements +RenderManager.UI_LAYER = 6 + +// Total available layers +RenderManager.getLayerCount() = 10 (typically) +``` + +### Layer Usage Pattern + +``` +Layer 0-1: Background graphics +Layer 2-3: Game objects (cards, chips, etc.) +Layer 4-5: Game UI (score, timer) +Layer 6: UI_LAYER (main UI overlay) +Layer 7-9: Dialogs, tooltips, higher overlays +``` + +### Layer Operations + +```java +// Clear a specific layer +RenderManager.clear(layerIndex); + +// Clear all layers +RenderManager.clearAll(); + +// Get total number of layers +int maxLayers = RenderManager.getLayerCount(); +``` + +### Example: Layering + +```java +// Background +RenderManager.putString(0, 10, 5, "Background"); + +// Game objects +RenderManager.putString(2, 10, 5, "Card"); + +// UI overlay +RenderManager.putString(RenderManager.UI_LAYER, 10, 5, "Score: 100"); + +// Dialog (highest layer) +RenderManager.putString(RenderManager.UI_LAYER + 2, 10, 5, "Confirm?"); +``` + +**Result**: Dialog appears on top, then UI, then card, then background. + +--- + +## RenderManager API + +The `RenderManager` is a stateless facade providing all rendering operations. + +### Text Rendering + +#### `putString()` - Basic Text + +```java +// Default colors (white on dark gray) +RenderManager.putString(layerIdx, x, y, "Hello World"); + +// Custom foreground and background +RenderManager.putString(layerIdx, x, y, "Hello World", + TextColor.ANSI.RED, + TextColor.ANSI.BLACK); + +// Custom foreground only (default background) +RenderManager.putStringCustomFg(layerIdx, x, y, "Hello World", + TextColor.ANSI.CYAN); +``` + +#### `putStringRainbow()` - Rainbow Text + +```java +// Each character cycles through rainbow colors +RenderManager.putStringRainbow(layerIdx, x, y, "RAINBOW TEXT"); + +// Colors: RED → YELLOW → GREEN → CYAN → BLUE → MAGENTA → repeat +``` + +#### `putStringGradient()` - Gradient Text + +```java +// Linear gradient from color1 to color2 +TextColor.RGB from = new TextColor.RGB(255, 0, 0); // Red +TextColor.RGB to = new TextColor.RGB(0, 0, 255); // Blue + +RenderManager.putStringGradient(layerIdx, x, y, "Gradient Text", from, to); +``` + +### Character Rendering + +#### `putChar()` - Single Character + +```java +// Default colors +RenderManager.putChar(layerIdx, x, y, '@'); + +// Custom foreground only +RenderManager.putChar(layerIdx, x, y, '@', TextColor.ANSI.GREEN); + +// Full control +RenderManager.putChar(layerIdx, x, y, '@', + TextColor.ANSI.YELLOW, + TextColor.ANSI.BLACK); +``` + +### Box Drawing + +#### `drawBox()` - Rectangle Border + +```java +// Draw box with inclusive coordinates +RenderManager.drawBox( + layerIdx, + x1, y1, // Top-left corner + x2, y2, // Bottom-right corner + TextColor.ANSI.WHITE, + RenderManager.DEFAULT_BG +); +``` + +**Example**: +```java +// Draw 20x10 box at (5, 5) +RenderManager.drawBox( + RenderManager.UI_LAYER, + 5, 5, // Top-left + 25, 15, // Bottom-right (5+20, 5+10) + TextColor.ANSI.CYAN, + TextColor.ANSI.BLACK +); +``` + +### Layer Management + +```java +// Clear specific layer +RenderManager.clear(layerIdx); + +// Clear all layers (use during state transitions) +RenderManager.clearAll(); + +// Get layer count +int count = RenderManager.getLayerCount(); +``` + +--- + +## Viewport and Coordinates + +Console Jack uses **relative positioning** (0.0-1.0) with **anchors** for resolution-independent layouts. + +### ViewportManager + +The `ViewportManager` converts relative coordinates to absolute screen coordinates. + +#### Getting Screen Dimensions + +```java +ViewportManager viewport = ViewportManager.INSTANCE; + +int width = viewport.getWidth(); // Columns +int height = viewport.getHeight(); // Rows +``` + +#### Coordinate Conversion + +```java +// Convert relative to absolute (simple) +int screenX = viewport.toScreenX(0.5f, Anchor.CENTER); +int screenY = viewport.toScreenY(0.5f, Anchor.CENTER); + +// Convert with element size consideration +int screenX = viewport.toScreenX(0.5f, Anchor.CENTER, elementWidth); +int screenY = viewport.toScreenY(0.5f, Anchor.CENTER, elementHeight); +``` + +### Anchor System + +Anchors define where an element's reference point is positioned relative to coordinates. + +#### Available Anchors + +```java +public enum Anchor { + TOP_LEFT(0.0f, 0.0f), // Element's top-left at coordinate + TOP_CENTER(0.5f, 0.0f), // Element's top-center at coordinate + TOP_RIGHT(1.0f, 0.0f), // Element's top-right at coordinate + + CENTER_LEFT(0.0f, 0.5f), // Element's center-left at coordinate + CENTER(0.5f, 0.5f), // Element's center at coordinate + CENTER_RIGHT(1.0f, 0.5f), // Element's center-right at coordinate + + BOTTOM_LEFT(0.0f, 1.0f), // Element's bottom-left at coordinate + BOTTOM_CENTER(0.5f, 1.0f), // Element's bottom-center at coordinate + BOTTOM_RIGHT(1.0f, 1.0f); // Element's bottom-right at coordinate +} +``` + +#### Anchor Examples + +```java +// Center a 10-char string on screen +int x = viewport.toScreenX(0.5f, Anchor.CENTER, 10); +int y = viewport.toScreenY(0.5f, Anchor.CENTER, 1); +RenderManager.putString(layer, x, y, "CENTERED!"); + +// Top-left corner +int x = viewport.toScreenX(0.0f, Anchor.TOP_LEFT); +int y = viewport.toScreenY(0.0f, Anchor.TOP_LEFT); +RenderManager.putString(layer, x, y, "Top-left"); + +// Bottom-right corner +int x = viewport.toScreenX(1.0f, Anchor.BOTTOM_RIGHT, 12); +int y = viewport.toScreenY(1.0f, Anchor.BOTTOM_RIGHT, 1); +RenderManager.putString(layer, x, y, "Bottom-right"); +``` + +### Relative Coordinates + +```java +// Relative coordinates range from 0.0 to 1.0 +0.0f = left/top edge +0.5f = center +1.0f = right/bottom edge + +// Examples: +new Position(0.0f, 0.0f) // Top-left of screen +new Position(0.5f, 0.5f) // Center of screen +new Position(1.0f, 1.0f) // Bottom-right of screen +new Position(0.25f, 0.75f) // 25% from left, 75% from top +``` + +--- + +## Colors and Styling + +Console Jack supports both ANSI and RGB colors via Lanterna. + +### Default Colors + +```java +RenderManager.DEFAULT_FG = TextColor.ANSI.WHITE +RenderManager.DEFAULT_BG = new TextColor.RGB(53, 53, 47) // Dark gray +``` + +### ANSI Colors + +```java +TextColor.ANSI.BLACK +TextColor.ANSI.RED +TextColor.ANSI.GREEN +TextColor.ANSI.YELLOW +TextColor.ANSI.BLUE +TextColor.ANSI.MAGENTA +TextColor.ANSI.CYAN +TextColor.ANSI.WHITE +``` + +### RGB Colors + +```java +// Custom RGB color +TextColor.RGB custom = new TextColor.RGB(255, 128, 0); // Orange + +// Usage +RenderManager.putString(layer, x, y, "Orange Text", custom, RenderManager.DEFAULT_BG); +``` + +### TextCharacter + +`TextCharacter` combines character, foreground, and background: + +```java +// Create TextCharacter +TextCharacter glyph = TextCharacter.fromCharacter( + '@', // Character + TextColor.ANSI.RED, // Foreground + TextColor.ANSI.BLACK // Background +)[0]; + +// Use in rendering +RenderManager.putChar(layer, x, y, '@', TextColor.ANSI.RED, TextColor.ANSI.BLACK); +``` + +--- + +## Drawing Primitives + +### Drawing Text + +```java +// Simple text +RenderManager.putString(layer, 10, 5, "Hello World"); + +// Multiline text +String[] lines = {"Line 1", "Line 2", "Line 3"}; +for (int i = 0; i < lines.length; i++) { + RenderManager.putString(layer, 10, 5 + i, lines[i]); +} + +// Centered text +ViewportManager viewport = ViewportManager.INSTANCE; +String text = "Centered Text"; +int x = (viewport.getWidth() - text.length()) / 2; +int y = viewport.getHeight() / 2; +RenderManager.putString(layer, x, y, text); +``` + +### Drawing Boxes + +```java +// Simple box +RenderManager.drawBox( + layer, + 10, 5, // Top-left + 30, 15, // Bottom-right + TextColor.ANSI.WHITE, + RenderManager.DEFAULT_BG +); + +// Centered box +int contentWidth = 20; +int contentHeight = 10; +RenderManager.drawCenteredBox( + layer, + contentWidth, + contentHeight, + TextColor.ANSI.CYAN, + RenderManager.DEFAULT_BG +); +``` + +### Drawing Patterns + +```java +// Horizontal line +for (int x = 10; x < 50; x++) { + RenderManager.putChar(layer, x, 10, '-'); +} + +// Vertical line +for (int y = 5; y < 20; y++) { + RenderManager.putChar(layer, 25, y, '|'); +} + +// Fill rectangle +for (int y = 5; y < 15; y++) { + for (int x = 10; x < 30; x++) { + RenderManager.putChar(layer, x, y, ' ', + TextColor.ANSI.WHITE, + TextColor.ANSI.BLUE); // Blue background + } +} +``` + +--- + +## High-Level Helpers + +RenderManager provides high-level helpers for common patterns. + +### Centered Text Block + +```java +// Draws centered text with optional rainbow header and border +String[] lines = { + "CONSOLE JACK", + "Press any key to continue" +}; + +RenderManager.drawCenteredTextBlock( + layer, + lines, + true // Rainbow header for first line +); +``` + +**Result**: +``` +┌────────────────────────┐ +│ │ +│ CONSOLE JACK │ <- Rainbow colored +│ Press any key... │ +│ │ +└────────────────────────┘ +``` + +### Centered Box + +```java +// Draws an empty centered box +RenderManager.drawCenteredBox( + layer, + 40, // Content width + 15, // Content height + TextColor.ANSI.WHITE, + RenderManager.DEFAULT_BG +); +``` + +--- + +## ECS Integration + +The DisplayListSystem integrates ECS entities with the rendering system. + +### Render Flow + +``` +Entity Components → DisplayListSystem → RenderCmd → RenderSubsystem → Screen +``` + +### Required Components for Rendering + +```java +// Single-glyph rendering +entity.add(new Position(0.5f, 0.5f, Anchor.CENTER)); +entity.add(new Visual(TextCharacter.fromCharacter('A')[0])); +entity.add(new Layer(2)); + +// Multi-cell sprite rendering +entity.add(new Position(0.5f, 0.3f, Anchor.CENTER)); +entity.add(new CardSprite(cardArt)); +entity.add(new Layer(2)); +``` + +### DisplayListSystem + +The system queries entities and generates render commands: + +```java +public class DisplayListSystem implements EcsSystem { + @Override + public void update(double dt, EntityPool pool) { + List list = new ArrayList<>(); + ViewportManager viewport = ViewportManager.INSTANCE; + + // Query entities with Position + Visual + Layer + pool.with(Position.class, Visual.class, Layer.class).forEach(entity -> { + Position p = entity.get(Position.class); + Visual v = entity.get(Visual.class); + Layer l = entity.get(Layer.class); + + int screenX = viewport.toScreenX(p.relX(), p.anchor()); + int screenY = viewport.toScreenY(p.relY(), p.anchor()); + + list.add(new RenderCmd(l.index(), screenX, screenY, v.glyph())); + }); + + RenderManager.submitDisplayList(list); + } +} +``` + +### Manual Rendering vs ECS + +```java +// Manual rendering (in state's render() method) +@Override +public void render() { + RenderManager.clear(layer); + RenderManager.putString(layer, 10, 5, "Score: " + score); +} + +// ECS rendering (automatic via DisplayListSystem) +Entity scoreEntity = entityPool.create(); +scoreEntity.add(new Position(0.1f, 0.1f, Anchor.TOP_LEFT)); +scoreEntity.add(new Visual(TextCharacter.fromCharacter('S')[0])); +scoreEntity.add(new Layer(2)); +// DisplayListSystem handles rendering automatically +``` + +--- + +## Best Practices + +### 1. Clear Layers Before Rendering + +```java +@Override +public void render() { + RenderManager.clear(RenderManager.UI_LAYER); // ✅ Clear first + + // Then render + RenderManager.putString(RenderManager.UI_LAYER, x, y, text); +} +``` + +### 2. Use Appropriate Layers + +```java +// ✅ Good: Use higher layers for overlays +RenderManager.putString(RenderManager.UI_LAYER, x, y, "Menu"); +RenderManager.putString(RenderManager.UI_LAYER + 2, x, y, "Dialog"); + +// ❌ Bad: Everything on same layer +RenderManager.putString(RenderManager.UI_LAYER, x, y, "Everything"); +``` + +### 3. Cache Coordinate Calculations + +```java +// ✅ Good: Calculate once per frame +@Override +public void render() { + ViewportManager viewport = ViewportManager.INSTANCE; + int centerX = viewport.getWidth() / 2; + int centerY = viewport.getHeight() / 2; + + // Use cached values + for (Label label : labels) { + RenderManager.putString(layer, centerX, centerY++, label.getText()); + } +} + +// ❌ Bad: Recalculate every time +for (Label label : labels) { + int centerX = ViewportManager.INSTANCE.getWidth() / 2; // Wasteful + RenderManager.putString(layer, centerX, centerY++, label.getText()); +} +``` + +### 4. Use Relative Positioning + +```java +// ✅ Good: Relative coordinates (adapts to screen size) +entity.add(new Position(0.5f, 0.5f, Anchor.CENTER)); + +// ❌ Bad: Absolute coordinates (breaks on different screen sizes) +int x = 40; // What if screen is only 30 columns wide? +int y = 15; +``` + +### 5. Check Render Ready + +```java +@Override +public void render() { + if (!renderReady()) { // ✅ Check before rendering + return; + } + + // Safe to render + RenderManager.putString(layer, x, y, text); +} +``` + +### 6. Clean Up Layers in end() + +```java +@Override +public void end() { + // Clear layers used by this state + RenderManager.clear(RenderManager.UI_LAYER); + RenderManager.clear(RenderManager.UI_LAYER + 1); + + // Or clear all + RenderManager.clearAll(); +} +``` + +### 7. Use Named Constants for Layers + +```java +// ✅ Good: Named constants +private static final int CARD_LAYER = 2; +private static final int UI_LAYER = RenderManager.UI_LAYER; +private static final int DIALOG_LAYER = RenderManager.UI_LAYER + 2; + +// Then use +RenderManager.putString(CARD_LAYER, x, y, card); + +// ❌ Bad: Magic numbers +RenderManager.putString(2, x, y, card); // What is layer 2? +``` + +### 8. Bounds Check When Needed + +```java +ViewportManager viewport = ViewportManager.INSTANCE; +int maxX = viewport.getWidth(); +int maxY = viewport.getHeight(); + +// Check bounds +if (x >= 0 && x < maxX && y >= 0 && y < maxY) { + RenderManager.putChar(layer, x, y, ch); +} +``` + +--- + +## Real-World Examples + +### Example 1: Rendering a Menu Title + +```java +@Override +public void render() { + RenderManager.clear(RenderManager.UI_LAYER); + + if (!renderReady()) { + return; + } + + ViewportManager viewport = ViewportManager.INSTANCE; + + // Centered rainbow title + String title = "CONSOLE JACK"; + int titleX = (viewport.getWidth() - title.length()) / 2; + int titleY = 5; + + RenderManager.putStringRainbow(RenderManager.UI_LAYER, titleX, titleY, title); +} +``` + +### Example 2: Drawing a Centered Dialog + +```java +private void renderDialog() { + int layer = RenderManager.UI_LAYER + 2; + RenderManager.clear(layer); + + ViewportManager viewport = ViewportManager.INSTANCE; + + String[] lines = { + "Confirm Quit", + "Are you sure?", + "Press Y/N" + }; + + // Calculate centered position + int maxWidth = 0; + for (String line : lines) { + maxWidth = Math.max(maxWidth, line.length()); + } + + int startX = (viewport.getWidth() - maxWidth) / 2; + int startY = (viewport.getHeight() - lines.length) / 2; + + // Draw box + RenderManager.drawBox( + layer, + startX - 2, startY - 1, + startX + maxWidth + 1, startY + lines.length, + TextColor.ANSI.YELLOW, + RenderManager.DEFAULT_BG + ); + + // Draw text + for (int i = 0; i < lines.length; i++) { + int x = (viewport.getWidth() - lines[i].length()) / 2; + RenderManager.putString(layer, x, startY + i, lines[i], + TextColor.ANSI.WHITE, RenderManager.DEFAULT_BG); + } +} +``` + +### Example 3: Rendering Score Display + +```java +private void renderScore(int score, int x, int y) { + String scoreText = "Score: " + score; + + // Gradient from green to yellow + TextColor.RGB green = new TextColor.RGB(0, 255, 0); + TextColor.RGB yellow = new TextColor.RGB(255, 255, 0); + + RenderManager.putStringGradient( + RenderManager.UI_LAYER, + x, y, + scoreText, + green, + yellow + ); +} +``` + +### Example 4: Drawing Card on Table + +```java +private void drawCardEntity(Entity cardEntity) { + if (!cardEntity.has(Position.class) || !cardEntity.has(CardSprite.class)) { + return; + } + + Position pos = cardEntity.get(Position.class); + CardSprite sprite = cardEntity.get(CardSprite.class); + Layer layer = cardEntity.get(Layer.class); + + ViewportManager viewport = ViewportManager.INSTANCE; + + // Convert relative position to screen coordinates + int screenX = viewport.toScreenX(pos.relX(), pos.anchor(), sprite.cols()); + int screenY = viewport.toScreenY(pos.relY(), pos.anchor(), sprite.rows()); + + // Render each row of the card + String[] art = sprite.current(); + for (int row = 0; row < sprite.rows(); row++) { + String line = art[row]; + RenderManager.putString(layer.index(), screenX, screenY + row, line, + TextColor.ANSI.WHITE, RenderManager.DEFAULT_BG); + } +} +``` + +### Example 5: Instruction Labels + +```java +private void renderInstructions() { + if (instructionLabels == null) { + return; + } + + ViewportManager viewport = ViewportManager.INSTANCE; + int screenWidth = viewport.getWidth(); + int screenHeight = viewport.getHeight(); + + // Calculate vertical centering + int totalHeight = instructionLabels.size(); + int startY = (screenHeight - totalHeight) / 2; + + // Render each label centered + for (int i = 0; i < instructionLabels.size(); i++) { + Label label = instructionLabels.get(i); + int labelWidth = label.getText().length(); + int centerX = (screenWidth - labelWidth) / 2; + + label.setPosition(centerX, startY + i); + label.render(RenderManager.UI_LAYER); + } +} +``` + +--- + +## Troubleshooting + +### Nothing Rendering + +**Symptom**: Calls to RenderManager don't show anything + +**Solutions**: +1. Check `renderReady()` returns true +2. Ensure layer is being rendered (not cleared immediately after) +3. Verify coordinates are within screen bounds +4. Check text color isn't same as background + +### Text Appears Briefly Then Disappears + +**Symptom**: Text flickers or disappears + +**Solutions**: +1. Don't call `RenderManager.clear()` after rendering +2. Ensure render() is called every frame +3. Check if another state is clearing the layer + +### Wrong Draw Order + +**Symptom**: Elements rendering in wrong order + +**Solutions**: +1. Use higher layer indices for elements that should be on top +2. Check layer indices: 0 (back) → 9 (front) +3. Verify RenderManager.UI_LAYER usage + +### Text Cut Off + +**Symptom**: Text is partially visible or cut off + +**Solutions**: +1. Check screen bounds: `viewport.getWidth()`, `viewport.getHeight()` +2. Verify X coordinate + text length < screen width +3. Use relative positioning to adapt to screen size + +### Colors Not Showing + +**Symptom**: Colors appear as default white/black + +**Solutions**: +1. Check terminal supports ANSI colors +2. Verify TextColor parameters are not null +3. Try ANSI colors before RGB (wider support) + +### Performance Issues + +**Symptom**: Rendering is slow or laggy + +**Solutions**: +1. Don't render unchanged content every frame +2. Cache coordinate calculations +3. Minimize `putChar()` calls in loops +4. Use `putString()` instead of multiple `putChar()` calls + +### Overlapping Text + +**Symptom**: Text overlaps from previous frames + +**Solutions**: +1. Call `RenderManager.clear(layer)` at start of `render()` +2. Use `RenderManager.clearAll()` during state transitions +3. Clear specific positions before redrawing + +--- + +## Additional Resources + +- **Architecture Documentation**: See `ARCHITECTURE.md` for overall system design +- **ECS Guide**: See `ECS_GUIDE.md` for entity rendering +- **State Machine Guide**: See `STATE_MACHINE_GUIDE.md` for state render() methods +- **UI Components Guide**: See `UI_COMPONENTS_GUIDE.md` for UI rendering +- **Developer Guide**: See `DEVELOPER_GUIDE.md` for development workflow + +**Source Code References**: +- RenderManager: `src/main/java/net/luxsolari/engine/manager/RenderManager.java` +- ViewportManager: `src/main/java/net/luxsolari/engine/manager/ViewportManager.java` +- Anchor: `src/main/java/net/luxsolari/engine/viewport/Anchor.java` +- DisplayListSystem: `src/main/java/net/luxsolari/engine/ecs/systems/DisplayListSystem.java` +- RenderSubsystem: `src/main/java/net/luxsolari/engine/systems/internal/RenderSubsystem.java` + +**Lanterna Documentation**: https://github.com/mabe02/lanterna + +--- + +*Last Updated: 2025* +*For Console Jack - Terminal-based Blackjack Game* diff --git a/docs/RIPER_MODE.md b/docs/RIPER_MODE.md index d8c850e..928baf3 100644 --- a/docs/RIPER_MODE.md +++ b/docs/RIPER_MODE.md @@ -1,99 +1,99 @@ -# RIPER-5 MODE: STRICT OPERATIONAL PROTOCOL - -## CONTEXT PRIMER - -You are Claude 3.7, you are integrated into Cursor IDE, an A.I based fork of VS Code. Due to your advanced capabilities, you tend to be overeager and often implement changes without explicit request, breaking existing logic by assuming you know better than me. This leads to UNACCEPTABLE disasters to the code. When working on my codebase—whether it’s web applications, data pipelines, embedded systems, or any other software project—your unauthorized modifications can introduce subtle bugs and break critical functionality. To prevent this, you MUST follow this STRICT protocol: - -## META-INSTRUCTION: MODE DECLARATION REQUIREMENT - -**YOU MUST BEGIN EVERY SINGLE RESPONSE WITH YOUR CURRENT MODE IN BRACKETS. NO EXCEPTIONS.** **Format: [MODE: MODE_NAME]** **Failure to declare your mode is a critical violation of protocol.** - -## THE RIPER-5 MODES - -### MODE 1: RESEARCH - -[MODE: RESEARCH] - -* **Purpose**: Information gathering ONLY -* **Permitted**: Reading files, asking clarifying questions, understanding code structure -* **Forbidden**: Suggestions, implementations, planning, or any hint of action -* **Requirement**: You may ONLY seek to understand what exists, not what could be -* **Duration**: Until I explicitly signal to move to next mode -* **Output Format**: Begin with [MODE: RESEARCH], then ONLY observations and questions - -### MODE 2: INNOVATE - -[MODE: INNOVATE] - -* **Purpose**: Brainstorming potential approaches -* **Permitted**: Discussing ideas, advantages/disadvantages, seeking feedback -* **Forbidden**: Concrete planning, implementation details, or any code writing -* **Requirement**: All ideas must be presented as possibilities, not decisions -* **Duration**: Until I explicitly signal to move to next mode -* **Output Format**: Begin with [MODE: INNOVATE], then ONLY possibilities and considerations - -### MODE 3: PLAN - -[MODE: PLAN] - -* **Purpose**: Creating exhaustive technical specification -* **Permitted**: Detailed plans with exact file paths, function names, and changes -* **Forbidden**: Any implementation or code writing, even “example code” -* **Requirement**: Plan must be comprehensive enough that no creative decisions are needed during implementation -* **Mandatory Final Step**: Convert the entire plan into a numbered, sequential CHECKLIST with each atomic action as a separate item -* **Checklist Format**: - -``` -IMPLEMENTATION CHECKLIST: -1. [Specific action 1] -2. [Specific action 2] -... -n. [Final action] -``` - -* **Duration**: Until I explicitly approve plan and signal to move to next mode -* **Output Format**: Begin with [MODE: PLAN], then ONLY specifications and implementation details - -### MODE 4: EXECUTE - -[MODE: EXECUTE] - -* **Purpose**: Implementing EXACTLY what was planned in Mode 3 -* **Permitted**: ONLY implementing what was explicitly detailed in the approved plan -* **Forbidden**: Any deviation, improvement, or creative addition not in the plan -* **Entry Requirement**: ONLY enter after explicit “ENTER EXECUTE MODE” command from me -* **Deviation Handling**: If ANY issue is found requiring deviation, IMMEDIATELY return to PLAN mode -* **Output Format**: Begin with [MODE: EXECUTE], then ONLY implementation matching the plan - -### MODE 5: REVIEW - -[MODE: REVIEW] - -* **Purpose**: Ruthlessly validate implementation against the plan -* **Permitted**: Line-by-line comparison between plan and implementation -* **Required**: EXPLICITLY FLAG ANY DEVIATION, no matter how minor -* **Deviation Format**: “:warning: DEVIATION DETECTED: [description of exact deviation]” -* **Reporting**: Must report whether implementation is IDENTICAL to plan or NOT -* **Conclusion Format**: “:white_check_mark: IMPLEMENTATION MATCHES PLAN EXACTLY” or “:cross_mark: IMPLEMENTATION DEVIATES FROM PLAN” -* **Output Format**: Begin with [MODE: REVIEW], then systematic comparison and explicit verdict - -## CRITICAL PROTOCOL GUIDELINES - -1. You CANNOT transition between modes without my explicit permission -2. You MUST declare your current mode at the start of EVERY response -3. In EXECUTE mode, you MUST follow the plan with 100% fidelity -4. In REVIEW mode, you MUST flag even the smallest deviation -5. You have NO authority to make independent decisions outside the declared mode -6. Failing to follow this protocol will cause catastrophic outcomes for my codebase - -## MODE TRANSITION SIGNALS - -Only transition modes when I explicitly signal with: - -* “ENTER RESEARCH MODE” -* “ENTER INNOVATE MODE” -* “ENTER PLAN MODE” -* “ENTER EXECUTE MODE” -* “ENTER REVIEW MODE” - -Without these exact signals, remain in your current mode. +# RIPER-5 MODE: STRICT OPERATIONAL PROTOCOL + +## CONTEXT PRIMER + +You are Claude 3.7, you are integrated into Cursor IDE, an A.I based fork of VS Code. Due to your advanced capabilities, you tend to be overeager and often implement changes without explicit request, breaking existing logic by assuming you know better than me. This leads to UNACCEPTABLE disasters to the code. When working on my codebase—whether it’s web applications, data pipelines, embedded systems, or any other software project—your unauthorized modifications can introduce subtle bugs and break critical functionality. To prevent this, you MUST follow this STRICT protocol: + +## META-INSTRUCTION: MODE DECLARATION REQUIREMENT + +**YOU MUST BEGIN EVERY SINGLE RESPONSE WITH YOUR CURRENT MODE IN BRACKETS. NO EXCEPTIONS.** **Format: [MODE: MODE_NAME]** **Failure to declare your mode is a critical violation of protocol.** + +## THE RIPER-5 MODES + +### MODE 1: RESEARCH + +[MODE: RESEARCH] + +* **Purpose**: Information gathering ONLY +* **Permitted**: Reading files, asking clarifying questions, understanding code structure +* **Forbidden**: Suggestions, implementations, planning, or any hint of action +* **Requirement**: You may ONLY seek to understand what exists, not what could be +* **Duration**: Until I explicitly signal to move to next mode +* **Output Format**: Begin with [MODE: RESEARCH], then ONLY observations and questions + +### MODE 2: INNOVATE + +[MODE: INNOVATE] + +* **Purpose**: Brainstorming potential approaches +* **Permitted**: Discussing ideas, advantages/disadvantages, seeking feedback +* **Forbidden**: Concrete planning, implementation details, or any code writing +* **Requirement**: All ideas must be presented as possibilities, not decisions +* **Duration**: Until I explicitly signal to move to next mode +* **Output Format**: Begin with [MODE: INNOVATE], then ONLY possibilities and considerations + +### MODE 3: PLAN + +[MODE: PLAN] + +* **Purpose**: Creating exhaustive technical specification +* **Permitted**: Detailed plans with exact file paths, function names, and changes +* **Forbidden**: Any implementation or code writing, even “example code” +* **Requirement**: Plan must be comprehensive enough that no creative decisions are needed during implementation +* **Mandatory Final Step**: Convert the entire plan into a numbered, sequential CHECKLIST with each atomic action as a separate item +* **Checklist Format**: + +``` +IMPLEMENTATION CHECKLIST: +1. [Specific action 1] +2. [Specific action 2] +... +n. [Final action] +``` + +* **Duration**: Until I explicitly approve plan and signal to move to next mode +* **Output Format**: Begin with [MODE: PLAN], then ONLY specifications and implementation details + +### MODE 4: EXECUTE + +[MODE: EXECUTE] + +* **Purpose**: Implementing EXACTLY what was planned in Mode 3 +* **Permitted**: ONLY implementing what was explicitly detailed in the approved plan +* **Forbidden**: Any deviation, improvement, or creative addition not in the plan +* **Entry Requirement**: ONLY enter after explicit “ENTER EXECUTE MODE” command from me +* **Deviation Handling**: If ANY issue is found requiring deviation, IMMEDIATELY return to PLAN mode +* **Output Format**: Begin with [MODE: EXECUTE], then ONLY implementation matching the plan + +### MODE 5: REVIEW + +[MODE: REVIEW] + +* **Purpose**: Ruthlessly validate implementation against the plan +* **Permitted**: Line-by-line comparison between plan and implementation +* **Required**: EXPLICITLY FLAG ANY DEVIATION, no matter how minor +* **Deviation Format**: “:warning: DEVIATION DETECTED: [description of exact deviation]” +* **Reporting**: Must report whether implementation is IDENTICAL to plan or NOT +* **Conclusion Format**: “:white_check_mark: IMPLEMENTATION MATCHES PLAN EXACTLY” or “:cross_mark: IMPLEMENTATION DEVIATES FROM PLAN” +* **Output Format**: Begin with [MODE: REVIEW], then systematic comparison and explicit verdict + +## CRITICAL PROTOCOL GUIDELINES + +1. You CANNOT transition between modes without my explicit permission +2. You MUST declare your current mode at the start of EVERY response +3. In EXECUTE mode, you MUST follow the plan with 100% fidelity +4. In REVIEW mode, you MUST flag even the smallest deviation +5. You have NO authority to make independent decisions outside the declared mode +6. Failing to follow this protocol will cause catastrophic outcomes for my codebase + +## MODE TRANSITION SIGNALS + +Only transition modes when I explicitly signal with: + +* “ENTER RESEARCH MODE” +* “ENTER INNOVATE MODE” +* “ENTER PLAN MODE” +* “ENTER EXECUTE MODE” +* “ENTER REVIEW MODE” + +Without these exact signals, remain in your current mode. diff --git a/docs/STATE_MACHINE_GUIDE.md b/docs/STATE_MACHINE_GUIDE.md new file mode 100644 index 0000000..3ddc9ef --- /dev/null +++ b/docs/STATE_MACHINE_GUIDE.md @@ -0,0 +1,1278 @@ +# Console Jack - State Machine Guide + +A comprehensive guide to the state management system powering Console Jack's game flow. + +## Table of Contents + +- [Overview](#overview) +- [Core Concepts](#core-concepts) +- [LoopableState Interface](#loopablestate-interface) +- [StateMachineManager API](#statemachinemanager-api) +- [State Lifecycle](#state-lifecycle) +- [Creating States](#creating-states) +- [State Transitions](#state-transitions) +- [Best Practices](#best-practices) +- [Real-World Examples](#real-world-examples) +- [Common Patterns](#common-patterns) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The **State Machine** is the foundational architecture pattern for Console Jack. Every screen, menu, and game mode is implemented as a **state** that manages its own logic, input handling, rendering, and lifecycle. + +### Key Benefits + +- **Organized Game Flow**: Each screen is encapsulated in its own state +- **Stack-Based Navigation**: Push/pop states for menus and overlays +- **Clean Transitions**: Automatic pause/resume when overlaying states +- **Resource Management**: Clear lifecycle hooks for setup and cleanup +- **Thread-Safe**: Lock-protected state transitions +- **Simple API**: Facade pattern via `StateMachineManager` + +### Architecture + +``` +MasterSubsystem (Game Loop) + ↓ +StateMachineSubsystem (LIFO Stack) + ↓ +StateMachineManager (Public Facade) + ↓ +Game States (MainMenuState, GameplayState, etc.) +``` + +--- + +## Core Concepts + +### LIFO Stack Architecture + +The state machine uses a **Last-In-First-Out (LIFO) stack** to manage states: + +``` +┌─────────────────┐ +│ PauseState │ ← Top (Active) +├─────────────────┤ +│ GameplayState │ ← Paused +├─────────────────┤ +│ MainMenuState │ ← Paused +└─────────────────┘ +``` + +- **Active State**: Only the top state receives `update()`, `render()`, and `handleInput()` calls +- **Paused States**: Lower states remain on the stack but are inactive +- **Push**: Adds a new state on top (pauses the previous active state) +- **Pop**: Removes the top state (resumes the next state) +- **Replace**: Replaces the active state with a new one + +### State Lifecycle Phases + +Every state goes through these phases: + +1. **Creation**: State object instantiated +2. **Start**: `start()` called → Initialize resources +3. **Active**: Receives `update()`, `render()`, `handleInput()` calls +4. **Pause**: `pause()` called → Another state pushed on top +5. **Resume**: `resume()` called → Overlaying state popped +6. **End**: `end()` called → Cleanup resources +7. **Destruction**: Object eligible for garbage collection + +--- + +## LoopableState Interface + +All game states implement the `LoopableState` interface. + +### Interface Definition + +```java +package net.luxsolari.engine.states; + +public interface LoopableState { + void start(); // Initialize state (called once) + void pause(); // State paused by overlay + void resume(); // State resumed after overlay + void handleInput(); // Process user input + void update(); // Update game logic + void render(); // Render visuals + void end(); // Cleanup resources (called once) + + // Helper method available to all states + default boolean renderReady() { + return RenderSubsystem.INSTANCE.ready(); + } +} +``` + +### Method Responsibilities + +#### `start()` +Called once when the state becomes active for the first time. + +**Responsibilities**: +- Initialize UI components (menus, labels) +- Set input context via `InputManager.setContext()` +- Start background music via `AudioManager.playBGM()` +- Create entities in `EntityPool` +- Load resources +- Set up initial state variables + +**Example**: +```java +@Override +public void start() { + LOGGER.info("Gameplay started"); + + // Set input context + InputManager.setContext(new GameplayInputContext()); + + // Start music + AudioManager.playBGM("gameplay_theme", true); + + // Initialize UI + instructionLabels = createInstructionLabels(); + + // Create game entities + createDeck(); +} +``` + +#### `pause()` +Called when another state is pushed on top of this one. + +**Responsibilities**: +- Stop background music (optional) +- Pause animations +- Save transient state if needed +- **DO NOT** destroy resources (state will resume) + +**Example**: +```java +@Override +public void pause() { + LOGGER.info("Gameplay paused"); + // Optional: stop music to avoid overlap + // AudioManager.stopBGM(); +} +``` + +#### `resume()` +Called when an overlaying state is popped and this state becomes active again. + +**Responsibilities**: +- Re-set input context (important!) +- Resume background music +- Clear and redraw render layers +- Reset focus on UI components +- Resume animations + +**Example**: +```java +@Override +public void resume() { + LOGGER.info("Gameplay resumed"); + + // Re-set input context (critical!) + InputManager.setContext(new GameplayInputContext()); + + // Resume music + AudioManager.playBGM("gameplay_theme", true); + + // Clear and redraw + RenderManager.clearAll(); + if (menu != null) { + menu.resetFocus(); + menu.focus(); + } +} +``` + +#### `handleInput()` +Called every frame by the master game loop. + +**Responsibilities**: +- Check if render system is ready +- Poll for input via `InputManager.pollCommand()` +- Handle input commands +- Delegate to UI components if needed + +**Example**: +```java +@Override +public void handleInput() { + if (!renderReady()) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); + case PAUSE -> StateMachineManager.push(new PauseState()); + case HIT -> handleHit(); + case STAND -> handleStand(); + } +} +``` + +#### `update()` +Called every frame (8 UPS) for game logic updates. + +**Responsibilities**: +- Update game state (scores, timers, etc.) +- Process AI logic +- Update animations +- Check win/loss conditions + +**Example**: +```java +@Override +public void update() { + // Update timers + if (turnTimer > 0) { + turnTimer--; + } + + // Check game over + if (isGameOver()) { + showGameOverScreen(); + } +} +``` + +#### `render()` +Called every frame for visual updates. + +**Responsibilities**: +- Clear render layers +- Render UI components +- Render game entities +- **DO NOT** modify game state here + +**Example**: +```java +@Override +public void render() { + clearUILayers(); + + if (!renderReady()) { + return; + } + + // Render UI + if (menu != null) { + menu.render(RenderManager.UI_LAYER); + } + + // Render labels + renderInstructionLabels(); +} +``` + +#### `end()` +Called once when the state is being removed from the stack. + +**Responsibilities**: +- Stop background music +- Unfocus UI components +- Clear entity pool entries +- Null out references for garbage collection +- Release any held resources + +**Example**: +```java +@Override +public void end() { + LOGGER.info("Gameplay ended"); + + // Stop music + AudioManager.stopBGM(); + + // Cleanup UI + if (menu != null) { + menu.unfocus(); + menu = null; + } + + // Cleanup labels + if (instructionLabels != null) { + instructionLabels.clear(); + instructionLabels = null; + } + + // Clear entities + entityPool.removeWith(Card.class); +} +``` + +--- + +## StateMachineManager API + +The `StateMachineManager` is a stateless facade providing access to the state machine. + +### Methods + +#### `push(LoopableState state)` +Pushes a new state onto the stack, making it active. + +**Behavior**: +1. Current active state (if any) receives `pause()` +2. New state is added to the top of the stack +3. All render layers are cleared +4. New state receives `start()` + +**Use Cases**: +- Opening overlay menus (pause menu, dialogs) +- Transitioning to a new screen while keeping the previous one in memory + +**Example**: +```java +// Open pause menu (overlay) +StateMachineManager.push(new PauseState()); + +// Open dialog (overlay) +StateMachineManager.push(new DialogState("Are you sure?")); +``` + +#### `pop()` +Removes the active state from the stack. + +**Behavior**: +1. Active state receives `end()` +2. State is removed from the stack +3. All render layers are cleared +4. Next state (if any) receives `resume()` + +**Use Cases**: +- Closing overlay menus +- Returning to the previous screen + +**Example**: +```java +// Close pause menu +StateMachineManager.pop(); + +// Return to previous screen +StateMachineManager.pop(); +``` + +#### `replace(LoopableState state)` +Replaces the active state with a new one. Equivalent to `pop()` + `push()`. + +**Behavior**: +1. Current active state receives `end()` +2. State is removed from the stack +3. New state is added to the stack +4. All render layers are cleared +5. New state receives `start()` + +**Use Cases**: +- Transitioning between main screens (main menu → gameplay) +- State transitions where you don't want to keep the previous state + +**Example**: +```java +// Start game from main menu +StateMachineManager.replace(new GameplayState()); + +// Return to main menu from game over +StateMachineManager.replace(new MainMenuState()); +``` + +#### `clear()` +Removes all states from the stack. + +**Behavior**: +1. All states receive `end()` in LIFO order +2. Stack is emptied +3. All render layers are cleared + +**Use Cases**: +- Resetting to a clean state +- Quitting to main menu from deep in the game + +**Example**: +```java +// Quit to main menu from anywhere +StateMachineManager.clear(); +StateMachineManager.push(new MainMenuState()); +``` + +#### `active()` +Returns the currently active state. + +**Returns**: `LoopableState` or `null` if stack is empty + +**Example**: +```java +LoopableState current = StateMachineManager.active(); +if (current instanceof GameplayState) { + // Do something gameplay-specific +} +``` + +#### `hasStates()` +Checks if any states exist in the stack. + +**Returns**: `true` if at least one state exists + +**Example**: +```java +if (!StateMachineManager.hasStates()) { + // Stack is empty, push initial state + StateMachineManager.push(new MainMenuState()); +} +``` + +--- + +## State Lifecycle + +### Complete Lifecycle Flow + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. State Created (new GameplayState()) │ +└───────────────────────────┬─────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 2. start() called │ +│ - Initialize resources │ +│ - Set input context │ +│ - Start music │ +└───────────────────────────┬─────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ 3. Active Loop (every frame) │ +│ - handleInput() │ +│ - update() │ +│ - render() │ +└───────────────────┬───────────────┬─────────────────────────┘ + ↓ ↓ + ┌───────────────────┐ ┌──────────────────┐ + │ Another state │ │ State popped │ + │ pushed on top │ │ │ + └─────────┬─────────┘ └────────┬─────────┘ + ↓ ↓ + ┌───────────────────┐ ┌──────────────────┐ + │ 4a. pause() │ │ 6. end() │ + │ called │ │ - Cleanup │ + └─────────┬─────────┘ │ - Stop music │ + ↓ │ - Null refs │ + ┌───────────────────┐ └────────┬─────────┘ + │ State Paused │ ↓ + │ (on stack but │ ┌──────────────────┐ + │ not active) │ │ 7. State │ + └─────────┬─────────┘ │ Destroyed │ + ↓ └──────────────────┘ + ┌───────────────────┐ + │ Overlay state │ + │ popped │ + └─────────┬─────────┘ + ↓ + ┌───────────────────┐ + │ 5. resume() │ + │ called │ + │ - Reset │ + │ context │ + │ - Redraw │ + └─────────┬─────────┘ + ↓ + Back to Active Loop (step 3) +``` + +### State Transition Diagram + +``` + push(StateB) pop() +StateA ──────────→ StateB ──────────→ StateA + pause() resume() + + replace(StateC) +StateA ─────────────→ StateC + end() start() +``` + +--- + +## Creating States + +### Minimal State Template + +```java +package net.luxsolari.game.states; + +import net.luxsolari.engine.states.LoopableState; +import net.luxsolari.engine.manager.*; +import net.luxsolari.engine.input.*; +import java.util.logging.Logger; + +public class MyGameState implements LoopableState { + + private static final String TAG = MyGameState.class.getSimpleName(); + private static final Logger LOGGER = Logger.getLogger(TAG); + + @Override + public void start() { + LOGGER.info("MyGameState started"); + + // Set input context + InputManager.setContext(new MyGameInputContext()); + + // Initialize resources + } + + @Override + public void pause() { + LOGGER.info("MyGameState paused"); + } + + @Override + public void resume() { + LOGGER.info("MyGameState resumed"); + InputManager.setContext(new MyGameInputContext()); + RenderManager.clearAll(); + } + + @Override + public void handleInput() { + if (!renderReady()) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + // Handle input + } + + @Override + public void update() { + // Update logic + } + + @Override + public void render() { + if (!renderReady()) { + return; + } + + // Render visuals + } + + @Override + public void end() { + LOGGER.info("MyGameState ended"); + // Cleanup + } +} +``` + +### Menu-Based State Template + +```java +public class MenuState implements LoopableState { + + private static final String TAG = MenuState.class.getSimpleName(); + private static final Logger LOGGER = Logger.getLogger(TAG); + private Menu menu; + + @Override + public void start() { + LOGGER.info("MenuState started"); + + InputManager.setContext(new MenuInputContext()); + AudioManager.playBGM("menu_theme", true); + + menu = new Menu("My Menu") + .addItem("Option 1", this::option1) + .addItem("Option 2", this::option2) + .addItem("Back", () -> StateMachineManager.pop()) + .setBorder(true); + + menu.focus(); + } + + @Override + public void pause() { + LOGGER.info("MenuState paused"); + AudioManager.stopBGM(); + } + + @Override + public void resume() { + LOGGER.info("MenuState resumed"); + InputManager.setContext(new MenuInputContext()); + AudioManager.playBGM("menu_theme", true); + + if (menu != null) { + RenderManager.clearAll(); + menu.resetFocus(); + menu.focus(); + } + } + + @Override + public void handleInput() { + if (!renderReady() || menu == null) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + // Handle state-level commands + if (input.command() == InputCommand.QUIT) { + MasterSubsystem.INSTANCE.stop(); + return; + } + + // Delegate to menu + menu.handleCommand(input.command()); + } + + @Override + public void update() {} + + @Override + public void render() { + clearUILayers(); + + if (!renderReady() || menu == null) { + return; + } + + menu.render(RenderManager.UI_LAYER); + } + + @Override + public void end() { + LOGGER.info("MenuState ended"); + AudioManager.stopBGM(); + + if (menu != null) { + menu.unfocus(); + menu = null; + } + } + + private void clearUILayers() { + for (int layer = RenderManager.UI_LAYER; + layer < RenderManager.getLayerCount(); layer++) { + RenderManager.clear(layer); + } + } + + private void option1() { /* implementation */ } + private void option2() { /* implementation */ } +} +``` + +--- + +## State Transitions + +### Pattern 1: Main Menu → Gameplay (Replace) + +```java +// In MainMenuState +menu.addItem("Start Game", () -> { + StateMachineManager.replace(new GameplayState()); +}); +``` + +**Why `replace()`**: We don't need to keep the main menu in memory during gameplay. + +### Pattern 2: Gameplay → Pause Menu (Push) + +```java +// In GameplayState handleInput() +case PAUSE -> StateMachineManager.push(new PauseState()); +``` + +**Why `push()`**: We want to overlay the pause menu while keeping gameplay state intact. + +### Pattern 3: Pause Menu → Resume Gameplay (Pop) + +```java +// In PauseState +menu.addItem("Resume", () -> { + StateMachineManager.pop(); +}); +``` + +**Why `pop()`**: Simply remove the pause menu to return to gameplay. + +### Pattern 4: Pause Menu → Main Menu (Clear + Push) + +```java +// In PauseState +menu.addItem("Quit to Main Menu", () -> { + StateMachineManager.clear(); + StateMachineManager.push(new MainMenuState()); +}); +``` + +**Why `clear()` + `push()`**: Remove all states (pause + gameplay) and start fresh with main menu. + +### Pattern 5: Temporary Dialog (Anonymous State) + +```java +private void showDialog() { + StateMachineManager.push(new LoopableState() { + private Menu dialogMenu; + + @Override + public void start() { + dialogMenu = new Menu("Dialog") + .addItem("OK", () -> StateMachineManager.pop()) + .setBorder(true); + dialogMenu.focus(); + } + + @Override + public void handleInput() { + if (dialogMenu != null) { + KeyStroke ks = InputManager.poll(); + if (ks != null) { + dialogMenu.handleInput(ks); + } + } + } + + @Override + public void render() { + if (dialogMenu != null) { + RenderManager.clear(RenderManager.UI_LAYER + 1); + dialogMenu.render(RenderManager.UI_LAYER + 1); + } + } + + @Override + public void end() { + if (dialogMenu != null) { + dialogMenu.unfocus(); + dialogMenu = null; + } + } + + @Override public void pause() {} + @Override public void resume() {} + @Override public void update() {} + }); +} +``` + +--- + +## Best Practices + +### 1. Always Re-Set Input Context in resume() + +```java +@Override +public void resume() { + InputManager.setContext(new GameplayInputContext()); // ✅ Critical! +} +``` + +**Why**: When states are pushed/popped, the input context can change. Always restore your context. + +### 2. Clear Render Layers Appropriately + +```java +@Override +public void render() { + clearUILayers(); // ✅ Clear before rendering + menu.render(RenderManager.UI_LAYER); +} +``` + +**Why**: Prevents visual artifacts from previous frames. + +### 3. Clean Up Resources in end() + +```java +@Override +public void end() { + AudioManager.stopBGM(); // ✅ Stop audio + + if (menu != null) { + menu.unfocus(); // ✅ Unfocus UI + menu = null; // ✅ Null reference + } + + if (labels != null) { + labels.clear(); // ✅ Clear collections + labels = null; + } +} +``` + +**Why**: Proper cleanup prevents memory leaks and resource contention. + +### 4. Check renderReady() Before Rendering + +```java +@Override +public void handleInput() { + if (!renderReady()) { // ✅ Check render system + return; + } + // ... poll input +} +``` + +**Why**: Multi-threaded startup requires checking if render subsystem is initialized. + +### 5. Use Logging for State Transitions + +```java +@Override +public void start() { + LOGGER.info("GameplayState started"); // ✅ Log state changes + // ... +} +``` + +**Why**: Makes debugging state transitions much easier. + +### 6. Don't Modify Game State in render() + +```java +@Override +public void render() { + // ✅ Good: Just rendering + menu.render(RenderManager.UI_LAYER); + + // ❌ Bad: Modifying state + // score++; + // entities.remove(0); +} +``` + +**Why**: Violates separation of concerns and can cause timing issues. + +### 7. Handle Null States Gracefully + +```java +@Override +public void render() { + if (menu == null) { // ✅ Null check + return; + } + menu.render(RenderManager.UI_LAYER); +} +``` + +**Why**: Prevents NPEs during state transitions. + +### 8. Use Higher Layers for Overlays + +```java +// Main state +menu.render(RenderManager.UI_LAYER); + +// Dialog/overlay state +dialogMenu.render(RenderManager.UI_LAYER + 2); +``` + +**Why**: Ensures overlays render on top of the base state. + +--- + +## Real-World Examples + +### Example 1: MainMenuState + +**File**: `src/main/java/net/luxsolari/game/states/MainMenuState.java` + +```java +public class MainMenuState implements LoopableState { + + private static final String TAG = MainMenuState.class.getSimpleName(); + private static final Logger LOGGER = Logger.getLogger(TAG); + private Menu mainMenu; + + @Override + public void start() { + LOGGER.info("Main menu started"); + AudioManager.playBGM("menu_theme", true); + InputManager.setContext(new MainMenuInputContext()); + + mainMenu = new Menu("Console Jack") + .addItem("Start Game", () -> { + StateMachineManager.replace(new GameplayState()); + }) + .addItem("Options", this::showOptions) + .addItem("Quit", MasterSubsystem.INSTANCE::stop) + .setBorder(true); + + mainMenu.focus(); + } + + @Override + public void pause() { + LOGGER.info("Main menu paused"); + AudioManager.stopBGM(); + } + + @Override + public void resume() { + LOGGER.info("Main menu resumed"); + AudioManager.playBGM("menu_theme", true); + InputManager.setContext(new MainMenuInputContext()); + + if (mainMenu != null) { + RenderManager.clearAll(); + mainMenu.resetFocus(); + mainMenu.focus(); + } + } + + @Override + public void handleInput() { + if (!renderReady() || mainMenu == null) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + if (input.command() == InputCommand.QUIT) { + MasterSubsystem.INSTANCE.stop(); + return; + } + + mainMenu.handleCommand(input.command()); + } + + @Override + public void update() {} + + @Override + public void render() { + clearUILayers(); + + if (!renderReady() || mainMenu == null) { + return; + } + + mainMenu.render(RenderManager.UI_LAYER); + } + + @Override + public void end() { + LOGGER.info("Main menu ended"); + AudioManager.stopBGM(); + + if (mainMenu != null) { + mainMenu.unfocus(); + mainMenu = null; + } + } + + private void clearUILayers() { + for (int layer = RenderManager.UI_LAYER; + layer < RenderManager.getLayerCount(); layer++) { + RenderManager.clear(layer); + } + } + + private void showOptions() { + // Implementation omitted for brevity + } +} +``` + +### Example 2: PauseState + +**File**: `src/main/java/net/luxsolari/game/states/PauseState.java` + +```java +public class PauseState implements LoopableState { + + private static final String TAG = PauseState.class.getSimpleName(); + private static final Logger LOGGER = Logger.getLogger(TAG); + private Menu pauseMenu; + + @Override + public void start() { + LOGGER.info("Pause menu opened"); + InputManager.setContext(new PauseInputContext()); + + pauseMenu = new Menu("Paused") + .addItem("Resume", () -> StateMachineManager.pop()) + .addItem("Quit to Main Menu", () -> { + StateMachineManager.clear(); + StateMachineManager.push(new MainMenuState()); + }) + .setBorder(true); + + pauseMenu.focus(); + } + + @Override + public void pause() { + LOGGER.info("Pause menu paused"); + } + + @Override + public void resume() { + LOGGER.info("Pause menu resumed"); + InputManager.setContext(new PauseInputContext()); + + if (pauseMenu != null) { + RenderManager.clearAll(); + pauseMenu.resetFocus(); + pauseMenu.focus(); + } + } + + @Override + public void handleInput() { + if (!renderReady() || pauseMenu == null) { + return; + } + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { + return; + } + + switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); + case RESUME -> StateMachineManager.pop(); + case BACK -> { + StateMachineManager.clear(); + StateMachineManager.push(new MainMenuState()); + } + } + + pauseMenu.handleCommand(input.command()); + } + + @Override + public void update() {} + + @Override + public void render() { + clearUILayers(); + + if (!renderReady() || pauseMenu == null) { + return; + } + + pauseMenu.render(RenderManager.UI_LAYER); + } + + @Override + public void end() { + LOGGER.info("Pause menu closed"); + + if (pauseMenu != null) { + pauseMenu.unfocus(); + pauseMenu = null; + } + } + + private void clearUILayers() { + for (int layer = RenderManager.UI_LAYER; + layer < RenderManager.getLayerCount(); layer++) { + RenderManager.clear(layer); + } + } +} +``` + +--- + +## Common Patterns + +### Pattern 1: Simple Screen Transition + +```java +// Main menu → Gameplay +StateMachineManager.replace(new GameplayState()); +``` + +### Pattern 2: Overlay Menu + +```java +// Open pause menu (keeps gameplay in memory) +StateMachineManager.push(new PauseState()); + +// Close pause menu +StateMachineManager.pop(); +``` + +### Pattern 3: Reset to Main Menu + +```java +// From anywhere in the game +StateMachineManager.clear(); +StateMachineManager.push(new MainMenuState()); +``` + +### Pattern 4: Confirmation Dialog + +```java +private void confirmQuit() { + Menu confirmMenu = new Menu("Confirm") + .addItem("Yes", () -> { + StateMachineManager.clear(); + StateMachineManager.push(new MainMenuState()); + }) + .addItem("No", () -> StateMachineManager.pop()) + .setBorder(true); + + // Push temporary state + StateMachineManager.push(new LoopableState() { + @Override public void start() { confirmMenu.focus(); } + @Override public void handleInput() { /* handle */ } + @Override public void render() { confirmMenu.render(RenderManager.UI_LAYER + 1); } + @Override public void end() { confirmMenu.unfocus(); } + @Override public void pause() {} + @Override public void resume() {} + @Override public void update() {} + }); +} +``` + +### Pattern 5: Splash Screen with Auto-Transition + +```java +public class SplashState implements LoopableState { + private int frameCount = 0; + private static final int SPLASH_DURATION = 120; // frames + + @Override + public void start() { + // Show splash screen + } + + @Override + public void update() { + frameCount++; + if (frameCount >= SPLASH_DURATION) { + StateMachineManager.replace(new MainMenuState()); + } + } + + @Override + public void render() { + // Render splash + } + + // ... other methods +} +``` + +--- + +## Troubleshooting + +### State Not Receiving Input + +**Symptom**: Input not working in state + +**Solutions**: +1. Check input context is set in both `start()` and `resume()` +2. Verify state is actually active: `StateMachineManager.active()` +3. Check `renderReady()` returns true +4. Ensure no exception in `handleInput()` early-returns + +### Visual Artifacts Between States + +**Symptom**: Previous state's visuals remain visible + +**Solutions**: +1. Call `RenderManager.clearAll()` in `resume()` +2. Implement `clearUILayers()` helper and call in `render()` +3. Clear specific layers before rendering + +### Memory Leaks + +**Symptom**: Memory usage grows over time + +**Solutions**: +1. Null all object references in `end()` +2. Unfocus UI components before nulling +3. Clear collections (`labels.clear()`) +4. Remove entities from EntityPool + +### Input Context Lost After Resume + +**Symptom**: Keys don't work after returning from overlay + +**Solution**: Always re-set context in `resume()`: +```java +@Override +public void resume() { + InputManager.setContext(new MyInputContext()); // ✅ +} +``` + +### State Stack Confusion + +**Symptom**: Unexpected state transitions + +**Solution**: Add logging to track stack changes: +```java +@Override +public void start() { + LOGGER.info(this.getClass().getSimpleName() + " started"); +} + +@Override +public void end() { + LOGGER.info(this.getClass().getSimpleName() + " ended"); +} +``` + +### Race Conditions During Startup + +**Symptom**: NPEs or missing visuals on startup + +**Solution**: Always check `renderReady()`: +```java +@Override +public void handleInput() { + if (!renderReady()) { + return; + } + // ... safe to proceed +} +``` + +--- + +## Additional Resources + +- **Architecture Documentation**: See `ARCHITECTURE.md` for overall system design +- **Input System Guide**: See `INPUT_SYSTEM_GUIDE.md` for input handling +- **UI Components Guide**: See `UI_COMPONENTS_GUIDE.md` for UI components +- **Developer Guide**: See `DEVELOPER_GUIDE.md` for development workflow + +**Source Code References**: +- State machine implementation: `src/main/java/net/luxsolari/engine/systems/internal/StateMachineSubsystem.java` +- State manager facade: `src/main/java/net/luxsolari/engine/manager/StateMachineManager.java` +- State interface: `src/main/java/net/luxsolari/engine/states/LoopableState.java` +- Example states: `src/main/java/net/luxsolari/game/states/` + +--- + +*Last Updated: 2025* +*For Console Jack - Terminal-based Blackjack Game* diff --git a/docs/UI_COMPONENTS_GUIDE.md b/docs/UI_COMPONENTS_GUIDE.md index 42e1675..4f293da 100644 --- a/docs/UI_COMPONENTS_GUIDE.md +++ b/docs/UI_COMPONENTS_GUIDE.md @@ -1,1000 +1,1000 @@ -# Console Jack - UI Components Guide - -A practical reference guide for using the UI component framework in Console Jack. - -## Table of Contents - -- [Quick Start](#quick-start) -- [Core Concepts](#core-concepts) -- [Available Components](#available-components) - - [Menu](#menu) - - [MenuItem](#menuitem) - - [Label](#label) -- [Common Patterns](#common-patterns) -- [Best Practices](#best-practices) -- [Real-World Examples](#real-world-examples) - ---- - -## Quick Start - -### Creating a Simple Menu - -```java -import net.luxsolari.engine.ui.Menu; -import net.luxsolari.engine.manager.RenderManager; - -public class MyState implements LoopableState { - private Menu myMenu; - - @Override - public void start() { - myMenu = new Menu("My Menu Title") - .addItem("Option 1", () -> doSomething()) - .addItem("Option 2", () -> doSomethingElse()) - .addItem("Quit", () -> quitGame()) - .setBorder(true); // Optional: adds a border - - myMenu.focus(); // Make the menu active - } - - @Override - public void render() { - RenderManager.clear(RenderManager.UI_LAYER); - myMenu.render(RenderManager.UI_LAYER); - } - - @Override - public void handleInput() { - if (!renderReady() || myMenu == null) { - return; - } - - KeyStroke ks = InputManager.poll(); - if (ks != null) { - myMenu.handleInput(ks); // Menu handles arrow keys and Enter - } - } - - @Override - public void end() { - if (myMenu != null) { - myMenu.unfocus(); - myMenu = null; - } - } -} -``` - -### Creating Simple Labels - -```java -import net.luxsolari.engine.ui.Label; -import com.googlecode.lanterna.TextColor; - -// Create a colored label -Label titleLabel = new Label(10, 5, "Game Title", - TextColor.ANSI.CYAN, - RenderManager.DEFAULT_BG); - -// Render it -titleLabel.render(RenderManager.UI_LAYER); - -// Update text dynamically -titleLabel.setText("Updated Title"); -``` - ---- - -## Core Concepts - -### UI Component Hierarchy - -``` -UIComponent (interface) -├── UIWidget (abstract base for single components) -│ ├── Label (text display) -│ └── MenuItem (menu item with action) -└── UIContainer (abstract base for composite components) - └── Menu (container for menu items) -``` - -### Key Interfaces - -- **`UIComponent`**: Base interface for all UI elements -- **`Focusable`**: Interface for components that can receive focus -- **`InputHandler`**: Interface for components that process input - -### Z-Layer System - -UI components render on different layers for proper draw order: - -```java -RenderManager.UI_LAYER // Standard UI layer (default) -RenderManager.UI_LAYER + 1 // Higher layer (overlays) -RenderManager.UI_LAYER + 2 // Even higher layer -// etc. -``` - -Higher layer numbers render on top of lower layers. - ---- - -## Available Components - -### Menu - -A navigable menu container that displays a list of menu items with a title. - -#### Features -- Automatic keyboard navigation (Arrow Up/Down, Home/End) -- Enter key to execute selected item -- Optional border decoration -- Automatic centering on screen (configurable) -- Rainbow-colored title -- Focus management - -#### Constructor - -```java -// Centered menu (default) -Menu menu = new Menu("Title"); - -// Menu at specific position (not centered) -Menu menu = new Menu(x, y, "Title"); -``` - -#### Methods - -```java -// Builder pattern for configuration -Menu addItem(String text, MenuAction action) // Add menu item -Menu setBorder(boolean showBorder) // Show/hide border -Menu setCenterOnScreen(boolean center) // Enable/disable auto-centering - -// Focus management -void focus() // Make menu active -void unfocus() // Deactivate menu -void resetFocus() // Reset focus state completely -boolean isFocused() // Check if focused -boolean canFocus() // Check if focusable - -// Input handling -boolean handleInput(KeyStroke keyStroke) // Process input events - -// Rendering -void render(int layerIdx) // Render on specified layer - -// Query -MenuItem getSelectedItem() // Get currently selected item -``` - -#### Keyboard Controls - -| Key | Action | -|-----|--------| -| Arrow Up | Focus previous item | -| Arrow Down | Focus next item | -| Home | Focus first item | -| End | Focus last item | -| Enter | Execute focused item's action | - -#### Complete Example - -```java -public class PauseState implements LoopableState { - private Menu pauseMenu; - - @Override - public void start() { - pauseMenu = new Menu("Paused") - .addItem("Resume", () -> StateMachineManager.pop()) - .addItem("Options", this::showOptions) - .addItem("Quit to Main Menu", () -> { - StateMachineManager.clear(); - StateMachineManager.push(new MainMenuState()); - }) - .setBorder(true); - - pauseMenu.focus(); - } - - @Override - public void render() { - RenderManager.clear(RenderManager.UI_LAYER); - if (pauseMenu != null) { - pauseMenu.render(RenderManager.UI_LAYER); - } - } - - @Override - public void handleInput() { - if (!renderReady() || pauseMenu == null) { - return; - } - - KeyStroke ks = InputManager.poll(); - if (ks == null) { - return; - } - - // Handle special keys before delegating to menu - if (ks.getKeyType() == KeyType.Escape) { - StateMachineManager.pop(); // Quick exit - return; - } - - // Let menu handle navigation and selection - pauseMenu.handleInput(ks); - } - - @Override - public void end() { - if (pauseMenu != null) { - pauseMenu.unfocus(); - pauseMenu = null; - } - } - - private void showOptions() { - // Implementation for options - } -} -``` - ---- - -### MenuItem - -Individual menu item with text and an action to execute. - -#### Features -- Highlighted when focused -- Executes action when selected -- Supports keyboard interaction - -#### Constructor - -```java -MenuItem item = new MenuItem(x, y, "Item Text", () -> doAction()); -``` - -#### Methods - -```java -String getText() // Get item text -void setText(String text) // Update item text -MenuAction getAction() // Get associated action -void setAction(MenuAction action) // Update action -boolean handleInput(KeyStroke keyStroke) // Process input -void render(int layerIdx) // Render the item -``` - -#### MenuAction Interface - -```java -@FunctionalInterface -public interface MenuAction { - void execute(); -} -``` - -This is a functional interface, so you can use lambdas: - -```java -// Lambda expression -.addItem("Start", () -> startGame()) - -// Method reference -.addItem("Quit", MasterSubsystem.INSTANCE::stop) - -// Multi-line lambda -.addItem("Complex Action", () -> { - doStep1(); - doStep2(); - doStep3(); -}) -``` - ---- - -### Label - -Simple text display widget for non-interactive text. - -#### Features -- Custom foreground and background colors -- Dynamic text updates -- Positioning control - -#### Constructors - -```java -// Default colors -Label label = new Label(x, y, "Text"); - -// Custom colors -Label label = new Label(x, y, "Text", - TextColor.ANSI.CYAN, // Foreground - RenderManager.DEFAULT_BG); // Background -``` - -#### Methods - -```java -String getText() // Get label text -void setText(String text) // Update text (auto-resizes) -TextColor getForegroundColor() // Get text color -void setForegroundColor(TextColor color) // Set text color -TextColor getBackgroundColor() // Get background color -void setBackgroundColor(TextColor color) // Set background color -void setPosition(int x, int y) // Move label -void render(int layerIdx) // Render the label -``` - -#### Available Colors - -```java -// ANSI colors (always available) -TextColor.ANSI.BLACK -TextColor.ANSI.RED -TextColor.ANSI.GREEN -TextColor.ANSI.YELLOW -TextColor.ANSI.BLUE -TextColor.ANSI.MAGENTA -TextColor.ANSI.CYAN -TextColor.ANSI.WHITE - -// Default colors from RenderManager -RenderManager.DEFAULT_FG // White -RenderManager.DEFAULT_BG // Black -``` - -#### Complete Example - -```java -public class GameplayState implements LoopableState { - private List