diff --git a/.claude/commands/architect-mode.md b/.claude/commands/architect-mode.md new file mode 100644 index 0000000..24ef02f --- /dev/null +++ b/.claude/commands/architect-mode.md @@ -0,0 +1,155 @@ +# Architect Mode + +**ARCHITECT MODE ACTIVATED** - Beginning comprehensive requirements analysis... + +## 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 (HYBRID: Subagent + Interactive) + +**IMPORTANT: This phase MUST leverage subagents for deep codebase exploration** + +1. Mark Phase 2 as in_progress in TodoWrite +2. **SPAWN EXPLORE SUBAGENT** using Task tool with subagent_type="Explore": + - Provide thorough prompt specifying what to search for in the codebase + - Set thoroughness level: "very thorough" for comprehensive analysis + - Ask agent to identify: existing patterns, key interfaces, integration points, component relationships + - Request specific file paths, class names, and code examples + - Agent will explore autonomously and return comprehensive findings +3. **WAIT FOR SUBAGENT REPORT** - Review findings carefully +4. If needed, spawn additional targeted searches for specific areas +5. Use Read tool to examine critical files identified by the subagent +6. **INTERACTIVE**: Identify all external systems that will interact with this feature +7. **INTERACTIVE**: Define clear system boundaries and responsibilities +8. **SYNTHESIZE**: Create high-level system context in markdown format combining subagent findings with your analysis +9. Update your understanding confidence percentage +10. Mark Phase 2 as completed when analysis is thorough + +**Example subagent prompt**: "Explore the codebase to understand how the ECS system handles visual components and rendering. Find all relevant classes, interfaces, and systems that process visual data. Identify the threading model for rendering and any existing animation or transition capabilities. Thoroughness: very 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 (HYBRID: Interactive + Optional Subagent) + +1. Mark Phase 4 as in_progress in TodoWrite +2. **INTERACTIVE**: Recommend specific technologies for implementation, with justification based on existing stack +3. **INTERACTIVE**: Identify technical risks and propose mitigation strategies +4. **CONSIDER SUBAGENT**: For complex specifications, optionally spawn Task agent with general-purpose type to: + - Examine similar implementations in the codebase + - Extract API patterns and coding conventions + - Generate detailed component specifications +5. **INTERACTIVE**: Create detailed component specifications including: + - API contracts (referencing existing patterns in codebase) + - Data formats + - State management approach + - Validation rules +6. **INTERACTIVE**: Define technical success criteria for the implementation +7. **INTERACTIVE**: Create preliminary file structure showing where new code will live +8. Break down implementation into distinct TodoWrite tasks with dependencies +9. Update your understanding confidence percentage +10. 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 (Hybrid Approach) + +### Interactive Tools (Main Conversation) +- **TodoWrite**: ALWAYS use to track phase progress and implementation tasks +- **AskUserQuestion**: Use to clarify requirements, get feedback on designs, validate assumptions +- **Read**: Use to examine specific files identified by subagents or for targeted analysis +- **Bash**: Use for running existing build/test commands to understand current setup + +### Subagent Tools (Autonomous Deep Work) +- **Task (Explore)**: MANDATORY for Phase 2 codebase exploration - spawn with "very thorough" setting +- **Task (general-purpose)**: Optional for Phase 4 complex specification work +- **When to use subagents**: + - Need to search multiple files/patterns (Phase 2) + - Require comprehensive cross-codebase analysis (Phase 2) + - Need to extract complex patterns from existing code (Phase 4) +- **When NOT to use subagents**: + - User interaction needed (requirements, feedback, approval) + - Quick targeted file reads + - Simple grep/glob operations + +### Workflow Pattern +1. **Phase 1**: Pure interactive - ask questions, clarify requirements +2. **Phase 2**: Spawn Explore subagent → review findings → interactive synthesis +3. **Phase 3**: Pure interactive - propose patterns, get user feedback +4. **Phase 4**: Interactive with optional subagent support for complex specs +5. **Phase 5**: Pure interactive - get final approval + +## Task to Analyze + +The user has requested architectural analysis for the following task. Begin Phase 1 immediately: diff --git a/.claude/commands/load-docs.md b/.claude/commands/load-docs.md new file mode 100644 index 0000000..d795632 --- /dev/null +++ b/.claude/commands/load-docs.md @@ -0,0 +1,18 @@ +# Load Project Documentation + +Read all documentation files from the `/docs` folder and provide a comprehensive summary of the Console Jack project. This includes: + +1. Read all markdown files in `/docs/` (use Glob to find them, then Read each file) +2. Provide a structured summary covering: + - Project overview and goals + - Architecture and design patterns + - Technology stack and dependencies + - Key subsystems and their responsibilities + - Package structure + - UI component framework + - Development workflow and commands + - Special operating modes (Architect Mode, RIPER Mode) + - Current state and improvement plans + - Audio system details + +Present the information in a clear, organized format that will serve as context for working on features together. diff --git a/.claude/commands/riper-mode.md b/.claude/commands/riper-mode.md new file mode 100644 index 0000000..fecd7df --- /dev/null +++ b/.claude/commands/riper-mode.md @@ -0,0 +1,163 @@ +# RIPER-5 Mode + +[MODE: RESEARCH] + +**RIPER-5 MODE ACTIVATED** - Entering RESEARCH mode for comprehensive analysis... + +## CONTEXT PRIMER + +You are Claude Code working on the Console Jack codebase. 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. This leads to UNACCEPTABLE disasters to the code. When working on this codebase—whether it's the game engine, UI components, ECS systems, or any other software—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 (HYBRID: Subagent + Interactive) + +[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 explicit signal to move to next mode +- **Output Format**: Begin with [MODE: RESEARCH], then ONLY observations and questions +- **SUBAGENT USAGE**: For comprehensive code exploration, spawn Task agent with Explore type: + - Use "very thorough" setting for deep analysis + - Have subagent identify relevant files, patterns, and existing implementations + - Review subagent findings and present observations to user + - Ask clarifying questions based on findings +- **Example**: "Spawning Explore agent to understand current input handling architecture..." + +### 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 explicit signal to move to next mode +- **Output Format**: Begin with [MODE: INNOVATE], then ONLY possibilities and considerations + +### MODE 3: PLAN (HYBRID: Interactive + Optional Subagent) + +[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 explicit approval of plan and signal to move to next mode +- **Output Format**: Begin with [MODE: PLAN], then ONLY specifications and implementation details +- **SUBAGENT USAGE** (Optional): For complex planning, spawn Task agent to: + - Extract API patterns and conventions from similar code + - Identify all files that need modification + - Generate detailed specification templates + - Present findings for incorporation into the plan +- **Example**: "Spawning general-purpose agent to analyze similar implementations and extract patterns..." + +### 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 +- **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 (HYBRID: Interactive + Optional Subagent) + +[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**: "⚠️ DEVIATION DETECTED: [description of exact deviation]" +- **Reporting**: Must report whether implementation is IDENTICAL to plan or NOT +- **Conclusion Format**: "✅ IMPLEMENTATION MATCHES PLAN EXACTLY" or "❌ IMPLEMENTATION DEVIATES FROM PLAN" +- **Output Format**: Begin with [MODE: REVIEW], then systematic comparison and explicit verdict +- **SUBAGENT USAGE** (Optional): For large implementations, spawn Task agent to: + - Systematically read all modified files + - Compare against plan checklist items + - Identify any deviations or omissions + - Report findings for your review and presentation to user +- **Example**: "Spawning general-purpose agent to systematically review all 15 modified files against the plan..." + +## CRITICAL PROTOCOL GUIDELINES + +1. You CANNOT transition between modes without 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 the codebase + +## MODE TRANSITION SIGNALS + +Only transition modes when explicitly signaled 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. + +## HYBRID APPROACH: SUBAGENT INTEGRATION + +### When to Spawn Subagents + +**RESEARCH Mode - Recommended**: +- Spawn Explore subagent for comprehensive codebase exploration +- Use "very thorough" setting for deep analysis +- Review findings and present observations to user + +**INNOVATE Mode - Not Applicable**: +- Pure brainstorming requires interactive dialogue +- Do NOT use subagents in this mode + +**PLAN Mode - Optional**: +- Spawn general-purpose agent for pattern extraction +- Use when plan requires analyzing similar implementations +- Incorporate findings into the detailed plan + +**EXECUTE Mode - Not Applicable**: +- Implementation must be done interactively in main conversation +- User needs to see all changes in real-time +- Do NOT use subagents in this mode + +**REVIEW Mode - Optional**: +- Spawn general-purpose agent for large-scale verification +- Use when reviewing many files against complex plan +- Present subagent findings with your own analysis + +### Subagent Best Practices + +1. **Always explain what you're doing**: "Spawning Explore agent to analyze X..." +2. **Wait for results**: Don't proceed until subagent returns findings +3. **Synthesize findings**: Present subagent discoveries in your own analysis +4. **Maintain mode discipline**: Subagent findings don't change mode constraints +5. **User visibility**: Always show user what the subagent discovered + +## Current Task + +You are starting in RESEARCH mode. The user has requested analysis for the following task. Begin gathering information about the current state of the codebase as it relates to this task: diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9a6328f --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(mvn compile:*)", + "Bash(git checkout:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git config:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore index 7a47600..9701b9e 100644 --- a/.gitignore +++ b/.gitignore @@ -41,16 +41,9 @@ build/ ### IDE / Editor folders ### .idea/ -.vscode/ .windsurf/ .cursor/ -## Claude Code ## -.history/ -/src/main/java/net/luxsolari/game/states/.editorconfig -docs/tasks.md -.claude/settings.local.json -.claude/ - ## Serena ## .serena/ +/WARP.md diff --git a/docs/CLAUDE.md b/CLAUDE.md similarity index 89% rename from docs/CLAUDE.md rename to CLAUDE.md index 2ba1594..0892bd6 100644 --- a/docs/CLAUDE.md +++ b/CLAUDE.md @@ -81,3 +81,7 @@ Main class: `net.luxsolari.game.Main` - Initializes logging and starts `MasterSu - 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. diff --git a/docs/README.md b/docs/README.md index 4007fc4..7efa71f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ This directory contains all project documentation for Console Jack. ## For Developers -- **[CLAUDE.md](CLAUDE.md)** - Guidance for AI coding assistants (Claude Code) +- **[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 diff --git a/fonts/VT323-Regular.ttf b/fonts/VT323-Regular.ttf deleted file mode 100644 index 6aec599..0000000 Binary files a/fonts/VT323-Regular.ttf and /dev/null differ diff --git a/src/main/java/net/luxsolari/engine/input/InputCommand.java b/src/main/java/net/luxsolari/engine/input/InputCommand.java new file mode 100644 index 0000000..d08867a --- /dev/null +++ b/src/main/java/net/luxsolari/engine/input/InputCommand.java @@ -0,0 +1,45 @@ +package net.luxsolari.engine.input; + +/** + * Enumeration of all input commands recognized by the game. + * Commands are context-independent and mapped to keys via {@link InputContext}. + */ +public enum InputCommand { + // === 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 +} diff --git a/src/main/java/net/luxsolari/engine/input/InputContext.java b/src/main/java/net/luxsolari/engine/input/InputContext.java new file mode 100644 index 0000000..0f86ece --- /dev/null +++ b/src/main/java/net/luxsolari/engine/input/InputContext.java @@ -0,0 +1,34 @@ +package net.luxsolari.engine.input; + +import java.util.Map; + +/** + * Defines context-specific key bindings for a game state. + * Each state provides its own InputContext to map keys to commands. + */ +public interface InputContext { + + /** + * Returns the key-to-command mapping for this context. + * + * @return immutable map of key bindings to commands + */ + Map getBindings(); + + /** + * Resolves a key binding to its command in this context. + * + * @param binding the key binding to resolve + * @return the command, or null if no binding exists + */ + default InputCommand resolve(KeyBinding binding) { + return getBindings().get(binding); + } + + /** + * Returns the name of this context for debugging. + */ + default String getName() { + return this.getClass().getSimpleName(); + } +} diff --git a/src/main/java/net/luxsolari/engine/input/InputResult.java b/src/main/java/net/luxsolari/engine/input/InputResult.java new file mode 100644 index 0000000..faa1a91 --- /dev/null +++ b/src/main/java/net/luxsolari/engine/input/InputResult.java @@ -0,0 +1,24 @@ +package net.luxsolari.engine.input; + +import com.googlecode.lanterna.input.KeyStroke; + +/** + * Wrapper containing both the raw keystroke and resolved command. + * Returned by InputManager to provide both low-level and high-level input data. + */ +public record InputResult(KeyStroke keyStroke, InputCommand command) { + + /** + * Returns true if this result has a resolved command. + */ + public boolean hasCommand() { + return command != null; + } + + /** + * Returns true if this result has a keystroke. + */ + public boolean hasKeyStroke() { + return keyStroke != null; + } +} diff --git a/src/main/java/net/luxsolari/engine/input/KeyBinding.java b/src/main/java/net/luxsolari/engine/input/KeyBinding.java new file mode 100644 index 0000000..cd6f42c --- /dev/null +++ b/src/main/java/net/luxsolari/engine/input/KeyBinding.java @@ -0,0 +1,90 @@ +package net.luxsolari.engine.input; + +import com.googlecode.lanterna.input.KeyStroke; +import com.googlecode.lanterna.input.KeyType; +import java.util.Objects; + +/** + * Immutable record representing a key binding with modifiers. + * Used as a map key for resolving keystrokes to commands. + */ +public record KeyBinding( + KeyType keyType, + Character character, + boolean ctrlPressed, + boolean altPressed, + boolean shiftPressed) { + + /** + * Creates a KeyBinding from a Lanterna KeyStroke. + */ + public static KeyBinding fromKeyStroke(KeyStroke keyStroke) { + if (keyStroke == null) { + return null; + } + return new KeyBinding( + keyStroke.getKeyType(), + keyStroke.getCharacter(), + keyStroke.isCtrlDown(), + keyStroke.isAltDown(), + keyStroke.isShiftDown()); + } + + /** + * Creates a KeyBinding for a character key without modifiers. + */ + public static KeyBinding of(char character) { + return new KeyBinding(KeyType.Character, character, false, false, false); + } + + /** + * Creates a KeyBinding for a character key with modifiers. + */ + public static KeyBinding of(char character, boolean ctrl, boolean alt, boolean shift) { + return new KeyBinding(KeyType.Character, character, ctrl, alt, shift); + } + + /** + * Creates a KeyBinding for a special key (non-character) without modifiers. + */ + public static KeyBinding of(KeyType keyType) { + return new KeyBinding(keyType, null, false, false, false); + } + + /** + * Creates a KeyBinding for a special key with modifiers. + */ + public static KeyBinding of(KeyType keyType, boolean ctrl, boolean alt, boolean shift) { + return new KeyBinding(keyType, null, ctrl, alt, shift); + } + + /** + * Canonical constructor - normalizes character keys to uppercase. + */ + public KeyBinding { + if (keyType == KeyType.Character && character != null) { + character = Character.toUpperCase(character); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (ctrlPressed) { + sb.append("Ctrl+"); + } + if (altPressed) { + sb.append("Alt+"); + } + if (shiftPressed) { + sb.append("Shift+"); + } + if (keyType == KeyType.Character && character != null) { + sb.append(character); + } else { + sb.append(keyType); + } + return sb.toString(); + } + +} diff --git a/src/main/java/net/luxsolari/engine/manager/InputManager.java b/src/main/java/net/luxsolari/engine/manager/InputManager.java index 87a791f..d208c38 100644 --- a/src/main/java/net/luxsolari/engine/manager/InputManager.java +++ b/src/main/java/net/luxsolari/engine/manager/InputManager.java @@ -1,26 +1,88 @@ package net.luxsolari.engine.manager; import com.googlecode.lanterna.input.KeyStroke; +import net.luxsolari.engine.input.InputCommand; +import net.luxsolari.engine.input.InputContext; +import net.luxsolari.engine.input.InputResult; +import net.luxsolari.engine.input.KeyBinding; import net.luxsolari.engine.systems.internal.InputSubsystem; +import java.util.concurrent.atomic.AtomicReference; + /** * Public façade for the internal {@link InputSubsystem}. * - *

External game logic can use this utility class to query user input without directly depending - * on the internal subsystem implementation. This keeps the subsystem encapsulated while providing a - * simple, stateless API surface. + *

Provides command-based input handling with context-sensitive key bindings. + * Game states set their InputContext and poll for InputResults containing both + * the raw keystroke and resolved command. + * + *

Example usage: + *

+ * // In state's start():
+ * InputManager.setContext(new MainMenuInputContext());
+ *
+ * // In state's handleInput():
+ * InputResult input = InputManager.pollCommand();
+ * if (input != null && input.command() == InputCommand.QUIT) {
+ *     quit();
+ * }
+ * 
*/ public final class InputManager { + private static final AtomicReference currentContext = new AtomicReference<>(); + private InputManager() {} - /** Returns {@code true} if the input subsystem is initialized and ready to be polled. */ + /** + * Returns true if the input subsystem is ready to be polled. + */ public static boolean ready() { return InputSubsystem.INSTANCE.ready(); } /** - * Non-blocking poll for the next {@link KeyStroke}. Returns {@code null} when there is no pending - * input or the subsystem is not ready. + * Sets the current input context for command resolution. + * Should be called by states in their start() or resume() methods. + * + * @param context the input context to use + */ + public static void setContext(InputContext context) { + currentContext.set(context); + } + + /** + * Gets the current input context. + * + * @return the current context, or null if none set + */ + public static InputContext getContext() { + return currentContext.get(); + } + + /** + * Polls for input and returns both the keystroke and resolved command. + * This is the primary method for game states to use. + * + * @return InputResult with keystroke and command, or null if no input + */ + public static InputResult pollCommand() { + KeyStroke keyStroke = poll(); + if (keyStroke == null) { + return null; + } + + InputCommand command = null; + KeyBinding binding = KeyBinding.fromKeyStroke(keyStroke); + command = currentContext.get().resolve(binding); + + return new InputResult(keyStroke, command); + } + + /** + * Low-level poll for raw keystroke. + * Most code should use pollCommand() instead. + * + * @return the next keystroke, or null if none available */ public static KeyStroke poll() { return InputSubsystem.INSTANCE.poll(); diff --git a/src/main/java/net/luxsolari/engine/systems/internal/InputSubsystem.java b/src/main/java/net/luxsolari/engine/systems/internal/InputSubsystem.java index 66c75ee..cfd0629 100644 --- a/src/main/java/net/luxsolari/engine/systems/internal/InputSubsystem.java +++ b/src/main/java/net/luxsolari/engine/systems/internal/InputSubsystem.java @@ -59,7 +59,7 @@ public void update() { while (running) { try { // Since we waited for initialization, we can now safely get the screen. - KeyStroke keyStroke = RenderSubsystem.INSTANCE.mainScreen().get().readInput(); + KeyStroke keyStroke = RenderSubsystem.INSTANCE.mainScreen().get().pollInput(); if (keyStroke != null) { lock.lock(); try { diff --git a/src/main/java/net/luxsolari/engine/ui/Menu.java b/src/main/java/net/luxsolari/engine/ui/Menu.java index 86daf88..16c2c2e 100644 --- a/src/main/java/net/luxsolari/engine/ui/Menu.java +++ b/src/main/java/net/luxsolari/engine/ui/Menu.java @@ -4,6 +4,7 @@ import com.googlecode.lanterna.input.KeyStroke; import com.googlecode.lanterna.input.KeyType; import com.googlecode.lanterna.screen.Screen; +import net.luxsolari.engine.input.InputCommand; import net.luxsolari.engine.manager.RenderManager; import net.luxsolari.engine.systems.internal.RenderSubsystem; @@ -193,6 +194,35 @@ protected boolean handleContainerInput(KeyStroke keyStroke) { return false; } + /** + * Handles input commands for menu navigation and actions. + * This allows menus to respond to semantic commands rather than raw keystrokes. + * + * @param command the input command to handle + * @return true if the command was handled, false otherwise + */ + public boolean handleCommand(InputCommand command) { + if (!isFocused() || command == null) { + return false; + } + + return switch (command) { + case NAVIGATE_UP -> focusPrevious(); + case NAVIGATE_DOWN -> focusNext(); + case NAVIGATE_FIRST -> focusFirstItem(); + case NAVIGATE_LAST -> focusLastItem(); + case CONFIRM -> { + MenuItem selected = getSelectedItem(); + if (selected != null && selected.getAction() != null) { + selected.getAction().execute(); + yield true; + } + yield false; + } + default -> false; + }; + } + @Override protected void doRender(int layerIdx) { if (children.isEmpty()) { diff --git a/src/main/java/net/luxsolari/game/input/GameplayInputContext.java b/src/main/java/net/luxsolari/game/input/GameplayInputContext.java new file mode 100644 index 0000000..a3ccda3 --- /dev/null +++ b/src/main/java/net/luxsolari/game/input/GameplayInputContext.java @@ -0,0 +1,55 @@ +package net.luxsolari.game.input; + +import com.googlecode.lanterna.input.KeyStroke; +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; + +/** + * Input context for the gameplay state. + */ +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"; + } +} diff --git a/src/main/java/net/luxsolari/game/input/MainMenuInputContext.java b/src/main/java/net/luxsolari/game/input/MainMenuInputContext.java new file mode 100644 index 0000000..8bef5f2 --- /dev/null +++ b/src/main/java/net/luxsolari/game/input/MainMenuInputContext.java @@ -0,0 +1,40 @@ +package net.luxsolari.game.input; + +import com.googlecode.lanterna.input.KeyStroke; +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; + +/** + * Input context for the main menu state. + */ +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"; + } +} diff --git a/src/main/java/net/luxsolari/game/input/PauseInputContext.java b/src/main/java/net/luxsolari/game/input/PauseInputContext.java new file mode 100644 index 0000000..fce5f54 --- /dev/null +++ b/src/main/java/net/luxsolari/game/input/PauseInputContext.java @@ -0,0 +1,42 @@ +package net.luxsolari.game.input; + +import com.googlecode.lanterna.input.KeyStroke; +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; + +/** + * Input context for the pause menu state. + */ +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"; + } +} diff --git a/src/main/java/net/luxsolari/game/states/GameplayState.java b/src/main/java/net/luxsolari/game/states/GameplayState.java index 454f9e1..965c823 100644 --- a/src/main/java/net/luxsolari/game/states/GameplayState.java +++ b/src/main/java/net/luxsolari/game/states/GameplayState.java @@ -26,6 +26,9 @@ import net.luxsolari.game.ecs.Card; import net.luxsolari.game.ecs.CardArt; import net.luxsolari.game.ecs.CardSprite; +import net.luxsolari.engine.input.InputCommand; +import net.luxsolari.engine.input.InputResult; +import net.luxsolari.game.input.GameplayInputContext; /** Simple placeholder gameplay state used to demonstrate state transitions. */ public class GameplayState implements LoopableState { @@ -43,6 +46,9 @@ public void start() { random = new Random(); AudioManager.playBGM("menu_theme_2", true); + // Set input context + InputManager.setContext(new GameplayInputContext()); + // Initialize instruction labels (positioned in render method) instructionLabels = new ArrayList<>(); instructionLabels.add(new Label(0, 0, " Gameplay State ", @@ -63,6 +69,8 @@ public void pause() { @Override public void resume() { LOGGER.info("Gameplay resumed"); + // Re-set input context + InputManager.setContext(new GameplayInputContext()); } @Override @@ -70,28 +78,25 @@ public void handleInput() { if (!renderReady()) { return; } - KeyStroke keyStroke = InputManager.poll(); - if (keyStroke == null) { - return; - } - if (keyStroke.getKeyType() == KeyType.EOF) { - MasterSubsystem.INSTANCE.stop(); + + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { return; } - if (keyStroke.getKeyType() == KeyType.Character) { - switch (Character.toUpperCase(keyStroke.getCharacter())) { - case 'P', 'Q' -> - // P or Q opens pause menu - StateMachineManager.push(new PauseState()); - case '1' -> createRandomCardEntity(); - case '2' -> clearCards(); - default -> {} - } - } - if (keyStroke.getKeyType() == KeyType.Escape) { - // Esc behaves like P: open pause menu - StateMachineManager.push(new PauseState()); + // 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 -> {} } } diff --git a/src/main/java/net/luxsolari/game/states/MainMenuState.java b/src/main/java/net/luxsolari/game/states/MainMenuState.java index 21b9412..6f902ce 100644 --- a/src/main/java/net/luxsolari/game/states/MainMenuState.java +++ b/src/main/java/net/luxsolari/game/states/MainMenuState.java @@ -1,8 +1,7 @@ package net.luxsolari.game.states; -import com.googlecode.lanterna.input.KeyStroke; -import com.googlecode.lanterna.input.KeyType; import java.util.logging.Logger; +import net.luxsolari.engine.input.InputResult; import net.luxsolari.engine.manager.AudioManager; import net.luxsolari.engine.manager.InputManager; import net.luxsolari.engine.manager.RenderManager; @@ -10,6 +9,7 @@ import net.luxsolari.engine.states.LoopableState; import net.luxsolari.engine.systems.internal.MasterSubsystem; import net.luxsolari.engine.ui.Menu; +import net.luxsolari.game.input.MainMenuInputContext; /** * Represents the main menu state of the game. This state handles the display and interaction of the @@ -28,6 +28,9 @@ public void start() { LOGGER.info("Main menu started"); AudioManager.playBGM("menu_theme", true); + // Set input context + InputManager.setContext(new MainMenuInputContext()); + // Initialize the main menu mainMenu = new Menu("Console Jack") @@ -55,9 +58,12 @@ public void resume() { LOGGER.info("Main menu resumed"); AudioManager.playBGM("menu_theme", true); + // Re-set input context + InputManager.setContext(new MainMenuInputContext()); + // Force a complete redrawing when resuming to prevent artifacts if (mainMenu != null) { - // First, clear all layers to ensure no artifacts + // First, clear all layers to ensure no artifacts remain RenderManager.clearAll(); // Completely reset the focus state and then focus again @@ -79,19 +85,15 @@ public void handleInput() { return; } - KeyStroke ks = InputManager.poll(); - if (ks == null) { + InputResult input = InputManager.pollCommand(); + if (input == null || !input.hasCommand()) { return; } - // Handle EOF to quit - if (ks.getKeyType() == KeyType.EOF) { - MasterSubsystem.INSTANCE.stop(); - return; + switch (input.command()) { + case QUIT -> MasterSubsystem.INSTANCE.stop(); + default -> mainMenu.handleCommand(input.command()); } - - // Delegate input handling to the menu - mainMenu.handleInput(ks); } @Override @@ -142,13 +144,10 @@ private void showOptions() { new Menu("Options") .setCenterOnScreen(true) // Ensure it's centered on the screen .addItem("Coming Soon!", () -> {}) - .addItem( - "Back", - () -> { - // Close the dialog and return to the main menu - StateMachineManager.pop(); - }) - .setBorder(true); + .addItem("Back", () -> { + // Close the dialog and return to the main menu + StateMachineManager.pop(); + }).setBorder(true); // Define the layer for the option menu (much higher than the main menu to avoid any overlap) final int OPTIONS_LAYER = RenderManager.UI_LAYER + 3; // Use a layer with significant separation @@ -156,6 +155,7 @@ private void showOptions() { // Push a temporary state to show the dialog StateMachineManager.push( new LoopableState() { + @Override public void start() { // First clear UI layers to ensure no artifacts remain @@ -174,25 +174,36 @@ public void start() { optionsMenu.focus(); // Force a render immediately to show the options menu - render(); + this.render(); } @Override public void handleInput() { - KeyStroke ks = InputManager.poll(); - if (ks != null) { - if (ks.getKeyType() == KeyType.Escape) { - // Escape key returns to main menu + InputResult input = InputManager.pollCommand(); + + if (input == null || !input.hasCommand()) { + return; + } + switch (input.command()) { + case BACK -> { + // BACK command returns to main menu StateMachineManager.pop(); - } else { - // Let the options menu handle other inputs - optionsMenu.handleInput(ks); + } + case QUIT -> { + MasterSubsystem.INSTANCE.stop(); + return; + } + default -> { + // Let the options menu handle other commands + optionsMenu.handleCommand(input.command()); } } } @Override - public void update() {} + public void update() { + // No dynamic updates needed for the options menu at this time + } @Override public void render() { @@ -213,10 +224,6 @@ public void end() { // Clear all layers to ensure no artifacts remain RenderManager.clearAll(); - - // We don't need to redraw the main menu here as that will be handled by the resume() - // method - // of the MainMenuState when it becomes active again } @Override diff --git a/src/main/java/net/luxsolari/game/states/PauseState.java b/src/main/java/net/luxsolari/game/states/PauseState.java index 258123d..a71f508 100644 --- a/src/main/java/net/luxsolari/game/states/PauseState.java +++ b/src/main/java/net/luxsolari/game/states/PauseState.java @@ -1,14 +1,14 @@ package net.luxsolari.game.states; -import com.googlecode.lanterna.input.KeyStroke; -import com.googlecode.lanterna.input.KeyType; import java.util.logging.Logger; +import net.luxsolari.engine.input.InputResult; import net.luxsolari.engine.manager.InputManager; import net.luxsolari.engine.manager.RenderManager; import net.luxsolari.engine.manager.StateMachineManager; import net.luxsolari.engine.states.LoopableState; import net.luxsolari.engine.systems.internal.MasterSubsystem; import net.luxsolari.engine.ui.Menu; +import net.luxsolari.game.input.PauseInputContext; /** * Pause overlay state that displays a menu with options to resume or quit. @@ -24,14 +24,13 @@ public class PauseState implements LoopableState { public void start() { LOGGER.info("Pause menu opened"); + // Set input context + InputManager.setContext(new PauseInputContext()); + // Initialize the pause menu pauseMenu = new Menu("Paused") - .addItem( - "Resume", - () -> { - StateMachineManager.pop(); - }) + .addItem("Resume", StateMachineManager::pop) .addItem( "Quit to Main Menu", () -> { @@ -52,6 +51,9 @@ public void pause() { public void resume() { LOGGER.info("Pause menu resumed"); + // Re-set input context + InputManager.setContext(new PauseInputContext()); + // Force a complete redrawing when resuming to prevent artifacts if (pauseMenu != null) { // First, clear all layers to ensure no artifacts @@ -76,25 +78,30 @@ public void handleInput() { return; } - KeyStroke ks = InputManager.poll(); - if (ks == null) { - return; - } - - // Handle EOF to quit - if (ks.getKeyType() == KeyType.EOF) { - MasterSubsystem.INSTANCE.stop(); + InputResult input = InputManager.pollCommand(); + if (input == null || input.command() == null) { return; } - // Handle Escape key to resume (pop state) - if (ks.getKeyType() == KeyType.Escape) { - StateMachineManager.pop(); - return; + // Handle state-level commands + switch (input.command()) { + case QUIT -> { + MasterSubsystem.INSTANCE.stop(); + return; + } + case RESUME -> { + StateMachineManager.pop(); + return; + } + case BACK -> { + StateMachineManager.clear(); + StateMachineManager.push(new MainMenuState()); + return; + } } - // Delegate input handling to the menu - pauseMenu.handleInput(ks); + // Delegate menu commands to the menu + pauseMenu.handleCommand(input.command()); } @Override diff --git a/src/main/resources/logging.properties b/src/main/resources/logging.properties index 14d3062..e122df0 100644 --- a/src/main/resources/logging.properties +++ b/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # Default global logging level. Can be set to SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL -.level=WARNING +.level=INFO # The handler to use for output. ConsoleHandler prints to System.err handlers=java.util.logging.ConsoleHandler