diff --git a/.cursor/rules/ui/RULE.md b/.cursor/rules/ui/RULE.md index bc6bd3a..029012b 100644 --- a/.cursor/rules/ui/RULE.md +++ b/.cursor/rules/ui/RULE.md @@ -9,6 +9,7 @@ alwaysApply: false ## **Framework** - Use **LitElement** for all UI components. +- Filename should match the component name (kebab-case) - Styles must be scoped within static get styles(). ## **Integration Logic** diff --git a/src/index.js b/src/index.js index aea3407..4d47199 100644 --- a/src/index.js +++ b/src/index.js @@ -26,12 +26,12 @@ let currentCharacterSheet = null; window.addEventListener("open-character-sheet", async (e) => { let { unit, unitId, readOnly = false, inventory = [] } = e.detail; - + // Resolve unit from ID if needed if (!unit && unitId && gameStateManager.gameLoop?.unitManager) { unit = gameStateManager.gameLoop.unitManager.getUnitById(unitId); } - + if (!unit) { console.warn("open-character-sheet event missing unit or unitId"); return; @@ -50,7 +50,10 @@ window.addEventListener("open-character-sheet", async (e) => { // Pause GameLoop if in combat let wasPaused = false; - if (gameStateManager.gameLoop && gameStateManager.currentState === "STATE_COMBAT") { + if ( + gameStateManager.gameLoop && + gameStateManager.currentState === "STATE_COMBAT" + ) { wasPaused = gameStateManager.gameLoop.isPaused; if (!wasPaused && gameStateManager.gameLoop.isRunning) { gameStateManager.gameLoop.pause(); @@ -58,62 +61,77 @@ window.addEventListener("open-character-sheet", async (e) => { } // Dynamically import CharacterSheet component - const { CharacterSheet } = await import("./ui/components/CharacterSheet.js"); - + const { CharacterSheet } = await import("./ui/components/character-sheet.js"); + // Generate skill tree using SkillTreeFactory if available let skillTree = null; if (gameStateManager.gameLoop?.classRegistry && unit.activeClassId) { try { - const { SkillTreeFactory } = await import("./factories/SkillTreeFactory.js"); - + const { SkillTreeFactory } = await import( + "./factories/SkillTreeFactory.js" + ); + // Load skill tree template - const templateResponse = await fetch("assets/data/skill_trees/template_standard_30.json"); + const templateResponse = await fetch( + "assets/data/skill_trees/template_standard_30.json" + ); if (templateResponse.ok) { const template = await templateResponse.json(); const templateRegistry = { [template.id]: template }; - + // Get class definition - const classDef = gameStateManager.gameLoop.classRegistry.get(unit.activeClassId); - + const classDef = gameStateManager.gameLoop.classRegistry.get( + unit.activeClassId + ); + if (classDef && classDef.skillTreeData) { // Get skill registry - import it directly const { skillRegistry } = await import("./managers/SkillRegistry.js"); - + // Convert Map to object for SkillTreeFactory const skillMap = Object.fromEntries(skillRegistry.skills); - + // Create factory and generate tree const factory = new SkillTreeFactory(templateRegistry, skillMap); skillTree = factory.createTree(classDef); } } } catch (error) { - console.warn("Failed to load skill tree template, using fallback:", error); + console.warn( + "Failed to load skill tree template, using fallback:", + error + ); } } - + // Create character sheet const characterSheet = document.createElement("character-sheet"); characterSheet.unit = unit; characterSheet.readOnly = readOnly; characterSheet.inventory = inventory; - characterSheet.gameMode = gameStateManager.currentState === "STATE_COMBAT" ? "DUNGEON" : "HUB"; + characterSheet.gameMode = + gameStateManager.currentState === "STATE_COMBAT" ? "DUNGEON" : "HUB"; characterSheet.treeDef = skillTree; // Pass generated tree // Pass inventoryManager from gameLoop if available if (gameStateManager.gameLoop?.inventoryManager) { - characterSheet.inventoryManager = gameStateManager.gameLoop.inventoryManager; + characterSheet.inventoryManager = + gameStateManager.gameLoop.inventoryManager; } - + // Handle close event const handleClose = () => { characterSheet.remove(); currentCharacterSheet = null; // Resume GameLoop if it was paused - if (!wasPaused && gameStateManager.gameLoop && gameStateManager.gameLoop.isPaused) { + if ( + !wasPaused && + gameStateManager.gameLoop && + gameStateManager.gameLoop.isPaused + ) { gameStateManager.gameLoop.resume(); } }; - + // Handle equip-item event (update unit equipment) const handleEquipItem = (event) => { const { unitId, slot, item, oldItem } = event.detail; @@ -121,22 +139,22 @@ window.addEventListener("open-character-sheet", async (e) => { // This event can be used for persistence or other side effects console.log(`Equipped ${item.name} to ${slot} slot for unit ${unitId}`); }; - + // Handle unlock-request event from SkillTreeUI const handleUnlockRequest = (event) => { const { nodeId, cost } = event.detail; const mastery = unit.classMastery?.[unit.activeClassId]; - + if (!mastery) { console.warn("No mastery data found for unit"); return; } - + if (mastery.skillPoints < cost) { console.warn("Insufficient skill points"); return; } - + // Deduct skill points and unlock node mastery.skillPoints -= cost; if (!mastery.unlockedNodes) { @@ -145,28 +163,31 @@ window.addEventListener("open-character-sheet", async (e) => { if (!mastery.unlockedNodes.includes(nodeId)) { mastery.unlockedNodes.push(nodeId); } - + // Trigger update in character sheet characterSheet.requestUpdate(); - + console.log(`Unlocked node ${nodeId} for ${cost} skill points`); }; - + // Handle skill-unlocked event to refresh combat HUD const handleSkillUnlocked = (event) => { const { unitId } = event.detail; // If we're in combat and the gameLoop exists, update combat state to refresh the HUD - if (gameStateManager?.gameLoop && gameStateManager.currentState === "STATE_COMBAT") { + if ( + gameStateManager?.gameLoop && + gameStateManager.currentState === "STATE_COMBAT" + ) { // Update combat state to refresh the HUD with newly unlocked skills gameStateManager.gameLoop.updateCombatState().catch(console.error); } }; - + characterSheet.addEventListener("close", handleClose); characterSheet.addEventListener("equip-item", handleEquipItem); characterSheet.addEventListener("unlock-request", handleUnlockRequest); characterSheet.addEventListener("skill-unlocked", handleSkillUnlocked); - + // Append to document body - dialog will handle its own display document.body.appendChild(characterSheet); currentCharacterSheet = characterSheet; @@ -179,9 +200,12 @@ window.addEventListener("gamestate-changed", async (e) => { case "STATE_MAIN_MENU": // Check if we should show hub or main menu const hasRoster = gameStateManager.rosterManager.roster.length > 0; - const hasCompletedMissions = gameStateManager.missionManager.completedMissions.size > 0; + const hasCompletedMissions = + gameStateManager.missionManager.completedMissions.size > 0; const shouldShowHub = hasRoster || hasCompletedMissions; - loadingMessage.textContent = shouldShowHub ? "INITIALIZING HUB..." : "INITIALIZING MAIN MENU..."; + loadingMessage.textContent = shouldShowHub + ? "INITIALIZING HUB..." + : "INITIALIZING MAIN MENU..."; break; case "STATE_TEAM_BUILDER": loadingMessage.textContent = "INITIALIZING TEAM BUILDER..."; @@ -204,13 +228,14 @@ window.addEventListener("gamestate-changed", async (e) => { case "STATE_MAIN_MENU": // Check if we should show hub or main menu const hasRoster = gameStateManager.rosterManager.roster.length > 0; - const hasCompletedMissions = gameStateManager.missionManager.completedMissions.size > 0; + const hasCompletedMissions = + gameStateManager.missionManager.completedMissions.size > 0; const shouldShowHub = hasRoster || hasCompletedMissions; - + if (shouldShowHub) { // Load HubScreen dynamically - await import("./ui/screens/HubScreen.js"); - await import("./ui/components/MissionBoard.js"); + await import("./ui/screens/hub-screen.js"); + await import("./ui/components/mission-board.js"); const hub = document.querySelector("hub-screen"); if (hub) { hub.toggleAttribute("hidden", false); diff --git a/src/ui/combat-hud.js b/src/ui/combat-hud.js index 72f14fa..ec63fb8 100644 --- a/src/ui/combat-hud.js +++ b/src/ui/combat-hud.js @@ -1,455 +1,431 @@ import { LitElement, html, css } from "lit"; +import { + theme, + progressBarStyles, + portraitStyles, + buttonStyles, + badgeStyles, +} from "./styles/theme.js"; export class CombatHUD extends LitElement { static get styles() { - return css` - :host { - display: block; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - font-family: "Courier New", monospace; - color: white; - z-index: 1000; - } - - /* Top Bar */ - .top-bar { - position: absolute; - top: 0; - left: 0; - right: 0; - height: 120px; - background: linear-gradient( - to bottom, - rgba(0, 0, 0, 0.9) 0%, - rgba(0, 0, 0, 0.7) 80%, - transparent 100% - ); - pointer-events: auto; - display: flex; - align-items: center; - justify-content: space-between; - padding: 20px 30px; - } - - .turn-queue { - display: flex; - align-items: center; - gap: 15px; - flex: 1; - } - - .queue-portrait { - width: 60px; - height: 60px; - border-radius: 50%; - border: 2px solid #666; - overflow: hidden; - background: rgba(0, 0, 0, 0.8); - position: relative; - pointer-events: auto; - } - - .queue-portrait.active { - width: 80px; - height: 80px; - border: 3px solid #ffd700; - box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); - } - - .queue-portrait img { - width: 100%; - height: 100%; - object-fit: cover; - } - - .queue-portrait.enemy { - border-color: #ff6666; - } - - .queue-portrait.player { - border-color: #66ff66; - } - - .enemy-intent { - position: absolute; - bottom: -5px; - right: -5px; - width: 20px; - height: 20px; - background: rgba(0, 0, 0, 0.9); - border: 1px solid #ff6666; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - } - - .global-info { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 8px; - background: rgba(0, 0, 0, 0.8); - border: 2px solid #555; - padding: 10px 20px; - pointer-events: auto; - } - - .round-counter { - font-size: 1.1rem; - font-weight: bold; - } - - .threat-level { - font-size: 0.9rem; - padding: 2px 8px; - border-radius: 3px; - } - - .threat-level.low { - background: rgba(0, 255, 0, 0.3); - color: #66ff66; - } - - .threat-level.medium { - background: rgba(255, 255, 0, 0.3); - color: #ffff66; - } - - .threat-level.high { - background: rgba(255, 0, 0, 0.3); - color: #ff6666; - } - - /* Bottom Bar */ - .bottom-bar { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 180px; - background: linear-gradient( - to top, - rgba(0, 0, 0, 0.9) 0%, - rgba(0, 0, 0, 0.7) 80%, - transparent 100% - ); - pointer-events: auto; - display: flex; - align-items: flex-end; - justify-content: space-between; - padding: 20px 30px; - } - - /* Unit Status (Bottom-Left) */ - .unit-status { - display: flex; - flex-direction: column; - gap: 10px; - background: rgba(0, 0, 0, 0.8); - border: 2px solid #555; - padding: 15px; - min-width: 200px; - pointer-events: auto; - } - - .unit-portrait { - width: 120px; - height: 120px; - border: 2px solid #666; - overflow: hidden; - background: rgba(0, 0, 0, 0.9); - margin: 0 auto; - } - - .unit-portrait img { - width: 100%; - height: 100%; - object-fit: cover; - } - - .unit-name { - text-align: center; - font-size: 1rem; - font-weight: bold; - margin-top: 5px; - } - - .status-icons { - display: flex; - gap: 5px; - justify-content: center; - flex-wrap: wrap; - margin-top: 5px; - } - - .status-icon { - width: 24px; - height: 24px; - border: 1px solid #555; - border-radius: 3px; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - background: rgba(0, 0, 0, 0.7); - cursor: help; - position: relative; - } - - .status-icon:hover::after { - content: attr(data-description); - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - background: rgba(0, 0, 0, 0.95); - border: 1px solid #555; - padding: 5px 10px; - white-space: nowrap; - font-size: 0.8rem; - margin-bottom: 5px; - pointer-events: none; - } - - .bar-container { - display: flex; - flex-direction: column; - gap: 5px; - margin-top: 10px; - } - - .bar-label { - font-size: 0.8rem; - display: flex; - justify-content: space-between; - } - - .bar { - height: 20px; - background: rgba(0, 0, 0, 0.7); - border: 1px solid #555; - position: relative; - overflow: hidden; - } - - .bar-fill { - height: 100%; - transition: width 0.3s ease; - } - - .bar-fill.hp { - background: #ff0000; - } - - .bar-fill.ap { - background: #ffaa00; - } - - .bar-fill.charge { - background: #0066ff; - } - - /* Action Bar (Bottom-Center) */ - .action-bar { - display: flex; - gap: 10px; - align-items: center; - pointer-events: auto; - } - - .skill-button { - width: 70px; - height: 70px; - background: rgba(0, 0, 0, 0.8); - border: 2px solid #666; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 5px; - position: relative; - transition: all 0.2s; - pointer-events: auto; - color: white; - } - - .skill-button:hover:not(:disabled) { - border-color: #ffd700; - box-shadow: 0 0 10px rgba(255, 215, 0, 0.5); - transform: translateY(-2px); - } - - .skill-button:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: #333; - } - - .skill-button.active { - background: rgba(255, 215, 0, 0.2); - border-color: #ffd700; - box-shadow: 0 0 15px rgba(255, 215, 0, 0.6); - } - - .movement-button { - width: 70px; - height: 70px; - background: rgba(0, 0, 0, 0.8); - border: 2px solid #666; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 5px; - position: relative; - transition: all 0.2s; - pointer-events: auto; - color: white; - } - - .movement-button:hover { - border-color: #66ff66; - box-shadow: 0 0 10px rgba(102, 255, 102, 0.5); - transform: translateY(-2px); - } - - .movement-button.active { - background: rgba(102, 255, 102, 0.2); - border-color: #66ff66; - box-shadow: 0 0 15px rgba(102, 255, 102, 0.6); - } - - .movement-button .icon { - font-size: 1.5rem; - margin-top: 8px; - } - - .movement-button .name { - font-size: 0.7rem; - text-align: center; - padding: 0 4px; - } - - .skill-button .hotkey { - position: absolute; - top: 2px; - left: 2px; - font-size: 0.7rem; - background: rgba(0, 0, 0, 0.8); - padding: 2px 4px; - border: 1px solid #555; - color: white; - } - - .skill-button .icon { - font-size: 1.5rem; - margin-top: 8px; - } - - .skill-button .name { - font-size: 0.7rem; - text-align: center; - padding: 0 4px; - } - - .skill-button .cost { - position: absolute; - bottom: 2px; - right: 2px; - font-size: 0.7rem; - color: #ffaa00; - } - - .skill-button .cooldown { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.7); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.2rem; - font-weight: bold; - } - - /* End Turn Button (Bottom-Right) */ - .end-turn-button { - background: rgba(0, 0, 0, 0.8); - border: 2px solid #ff6666; - padding: 15px 30px; - font-size: 1.1rem; - font-weight: bold; - color: white; - cursor: pointer; - transition: all 0.2s; - pointer-events: auto; - font-family: "Courier New", monospace; - } - - .end-turn-button:hover { - background: rgba(255, 102, 102, 0.2); - box-shadow: 0 0 15px rgba(255, 102, 102, 0.5); - transform: translateY(-2px); - } - - .end-turn-button:active { - transform: translateY(0); - } - - /* Responsive Design - Mobile (< 768px) */ - @media (max-width: 767px) { - .bottom-bar { - flex-direction: column; - align-items: stretch; - gap: 15px; - height: auto; - min-height: 180px; - } - - .unit-status { + return [ + theme, + progressBarStyles, + portraitStyles, + buttonStyles, + badgeStyles, + css` + :host { + display: block; + position: absolute; + top: 0; + left: 0; width: 100%; - min-width: auto; - } - - .action-bar { - justify-content: center; - flex-wrap: wrap; - } - - .end-turn-button { - width: 100%; - margin-top: 10px; + height: 100%; + pointer-events: none; + font-family: var(--font-family); + color: var(--color-text-primary); + z-index: var(--z-tooltip); } + /* Top Bar */ .top-bar { - flex-direction: column; - height: auto; - min-height: 120px; - gap: 10px; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 120px; + background: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.9) 0%, + rgba(0, 0, 0, 0.7) 80%, + transparent 100% + ); + pointer-events: auto; + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-lg) var(--spacing-xl); } .turn-queue { + display: flex; + align-items: center; + gap: var(--spacing-md); + flex: 1; + } + + .queue-portrait { + width: 60px; + height: 60px; + border-radius: 50%; + border: var(--border-width-medium) solid var(--color-border-light); + overflow: hidden; + background: var(--color-bg-primary); + position: relative; + pointer-events: auto; + } + + .queue-portrait.active { + width: 80px; + height: 80px; + border: var(--border-width-thick) solid var(--color-accent-gold); + box-shadow: var(--shadow-glow-gold); + } + + .queue-portrait img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .queue-portrait.enemy { + border-color: var(--color-accent-red); + } + + .queue-portrait.player { + border-color: var(--color-accent-green); + } + + .enemy-intent { + position: absolute; + bottom: -5px; + right: -5px; + width: 20px; + height: 20px; + background: rgba(0, 0, 0, 0.9); + border: var(--border-width-thin) solid var(--color-accent-red); + border-radius: 50%; + display: flex; + align-items: center; justify-content: center; - flex-wrap: wrap; + font-size: var(--font-size-xs); } .global-info { - align-items: center; - width: 100%; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: var(--spacing-sm); + background: var(--color-bg-primary); + border: var(--border-width-medium) solid var(--color-border-default); + padding: var(--spacing-sm) var(--spacing-lg); + pointer-events: auto; } - } - `; + + .round-counter { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + } + + .threat-level { + font-size: var(--font-size-sm); + padding: 2px var(--spacing-sm); + border-radius: var(--border-radius-sm); + } + + .threat-level.low { + background: rgba(0, 255, 0, 0.3); + color: var(--color-accent-green); + } + + .threat-level.medium { + background: rgba(255, 255, 0, 0.3); + color: #ffff66; + } + + .threat-level.high { + background: rgba(255, 0, 0, 0.3); + color: var(--color-accent-red); + } + + /* Bottom Bar */ + .bottom-bar { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 180px; + background: linear-gradient( + to top, + rgba(0, 0, 0, 0.9) 0%, + rgba(0, 0, 0, 0.7) 80%, + transparent 100% + ); + pointer-events: auto; + display: flex; + align-items: flex-end; + justify-content: space-between; + padding: var(--spacing-lg) var(--spacing-xl); + } + + /* Unit Status (Bottom-Left) */ + .unit-status { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + background: var(--color-bg-primary); + border: var(--border-width-medium) solid var(--color-border-default); + padding: var(--spacing-md); + min-width: 200px; + pointer-events: auto; + } + + .unit-portrait { + width: 120px; + height: 120px; + border: var(--border-width-medium) solid var(--color-border-light); + overflow: hidden; + background: rgba(0, 0, 0, 0.9); + margin: 0 auto; + } + + .unit-portrait img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .unit-name { + text-align: center; + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); + margin-top: var(--spacing-xs); + } + + .status-icons { + display: flex; + gap: var(--spacing-xs); + justify-content: center; + flex-wrap: wrap; + margin-top: var(--spacing-xs); + } + + .status-icon { + width: 24px; + height: 24px; + border: var(--border-width-thin) solid var(--color-border-default); + border-radius: var(--border-radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-sm); + background: rgba(0, 0, 0, 0.7); + cursor: help; + position: relative; + } + + .status-icon:hover::after { + content: attr(data-description); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.95); + border: var(--border-width-thin) solid var(--color-border-default); + padding: var(--spacing-xs) var(--spacing-sm); + white-space: nowrap; + font-size: var(--font-size-sm); + margin-bottom: var(--spacing-xs); + pointer-events: none; + } + + /* Action Bar (Bottom-Center) */ + .action-bar { + display: flex; + gap: var(--spacing-sm); + align-items: center; + pointer-events: auto; + } + + .skill-button { + width: 70px; + height: 70px; + background: var(--color-bg-primary); + border: var(--border-width-medium) solid var(--color-border-light); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + position: relative; + transition: all var(--transition-normal); + pointer-events: auto; + color: var(--color-text-primary); + } + + .skill-button:hover:not(:disabled) { + border-color: var(--color-accent-gold); + box-shadow: var(--shadow-glow-gold); + transform: translateY(-2px); + } + + .skill-button:disabled { + opacity: 0.5; + cursor: not-allowed; + border-color: var(--color-border-dark); + } + + .skill-button.active { + background: rgba(255, 215, 0, 0.2); + border-color: var(--color-accent-gold); + box-shadow: var(--shadow-glow-gold); + } + + .movement-button { + width: 70px; + height: 70px; + background: var(--color-bg-primary); + border: var(--border-width-medium) solid var(--color-border-light); + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-xs); + position: relative; + transition: all var(--transition-normal); + pointer-events: auto; + color: var(--color-text-primary); + } + + .movement-button:hover { + border-color: var(--color-accent-green); + box-shadow: var(--shadow-glow-green); + transform: translateY(-2px); + } + + .movement-button.active { + background: rgba(102, 255, 102, 0.2); + border-color: var(--color-accent-green); + box-shadow: var(--shadow-glow-green); + } + + .movement-button .icon { + font-size: var(--font-size-2xl); + margin-top: var(--spacing-sm); + } + + .movement-button .name { + font-size: var(--font-size-xs); + text-align: center; + padding: 0 4px; + } + + .skill-button .hotkey { + position: absolute; + top: 2px; + left: 2px; + font-size: var(--font-size-xs); + background: var(--color-bg-primary); + padding: 2px 4px; + border: var(--border-width-thin) solid var(--color-border-default); + color: var(--color-text-primary); + } + + .skill-button .icon { + font-size: var(--font-size-2xl); + margin-top: var(--spacing-sm); + } + + .skill-button .name { + font-size: var(--font-size-xs); + text-align: center; + padding: 0 4px; + } + + .skill-button .cost { + position: absolute; + bottom: 2px; + right: 2px; + font-size: var(--font-size-xs); + color: var(--color-accent-orange); + } + + .skill-button .cooldown { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + } + + /* End Turn Button (Bottom-Right) */ + .end-turn-button { + background: var(--color-bg-primary); + border: var(--border-width-medium) solid var(--color-accent-red); + padding: var(--spacing-md) var(--spacing-xl); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); + cursor: pointer; + transition: all var(--transition-normal); + pointer-events: auto; + font-family: var(--font-family); + } + + .end-turn-button:hover { + background: rgba(255, 102, 102, 0.2); + box-shadow: var(--shadow-glow-red); + transform: translateY(-2px); + } + + .end-turn-button:active { + transform: translateY(0); + } + + /* Responsive Design - Mobile (< 768px) */ + @media (max-width: 767px) { + .bottom-bar { + flex-direction: column; + align-items: stretch; + gap: var(--spacing-md); + height: auto; + min-height: 180px; + } + + .unit-status { + width: 100%; + min-width: auto; + } + + .action-bar { + justify-content: center; + flex-wrap: wrap; + } + + .end-turn-button { + width: 100%; + margin-top: var(--spacing-sm); + } + + .top-bar { + flex-direction: column; + height: auto; + min-height: 120px; + gap: var(--spacing-sm); + } + + .turn-queue { + justify-content: center; + flex-wrap: wrap; + } + + .global-info { + align-items: center; + width: 100%; + } + } + `, + ]; } static get properties() { @@ -553,8 +529,11 @@ export class CombatHUD extends LitElement { ${label} ${current}/${max} -
`; diff --git a/src/ui/components/MissionBoard.js b/src/ui/components/MissionBoard.js deleted file mode 100644 index a42a46e..0000000 --- a/src/ui/components/MissionBoard.js +++ /dev/null @@ -1,376 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { gameStateManager } from '../../core/GameStateManager.js'; - -/** - * MissionBoard.js - * Component for displaying and selecting available missions. - * @class - */ -export class MissionBoard extends LitElement { - static get styles() { - return css` - :host { - display: block; - background: rgba(20, 20, 30, 0.95); - border: 2px solid #555; - padding: 30px; - max-width: 1000px; - max-height: 80vh; - overflow-y: auto; - color: white; - font-family: 'Courier New', monospace; - } - - .header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 30px; - border-bottom: 2px solid #555; - padding-bottom: 15px; - } - - .header h2 { - margin: 0; - color: #ffd700; - font-size: 28px; - } - - .close-button { - background: transparent; - border: 2px solid #ff6666; - color: #ff6666; - width: 40px; - height: 40px; - font-size: 24px; - cursor: pointer; - transition: all 0.2s; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - } - - .close-button:hover { - background: #ff6666; - color: white; - } - - .missions-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: 20px; - margin-bottom: 20px; - } - - .mission-card { - background: rgba(0, 0, 0, 0.5); - border: 2px solid #555; - padding: 20px; - cursor: pointer; - transition: all 0.2s; - position: relative; - } - - .mission-card:hover { - border-color: #ffd700; - box-shadow: 0 0 15px rgba(255, 215, 0, 0.3); - transform: translateY(-2px); - } - - .mission-card.completed { - border-color: #00ff00; - opacity: 0.7; - } - - .mission-card.locked { - opacity: 0.5; - cursor: not-allowed; - border-color: #333; - } - - .mission-card.locked:hover { - transform: none; - border-color: #333; - box-shadow: none; - } - - .mission-header { - display: flex; - justify-content: space-between; - align-items: start; - margin-bottom: 15px; - } - - .mission-title { - font-size: 20px; - font-weight: bold; - color: #00ffff; - margin: 0; - } - - .mission-type { - font-size: 12px; - padding: 4px 8px; - background: rgba(255, 255, 255, 0.1); - border-radius: 4px; - text-transform: uppercase; - } - - .mission-type.STORY { - background: rgba(255, 0, 0, 0.3); - color: #ff6666; - } - - .mission-type.SIDE_QUEST { - background: rgba(255, 215, 0, 0.3); - color: #ffd700; - } - - .mission-type.TUTORIAL { - background: rgba(0, 255, 255, 0.3); - color: #00ffff; - } - - .mission-type.PROCEDURAL { - background: rgba(0, 255, 0, 0.3); - color: #00ff00; - } - - .mission-description { - font-size: 14px; - color: #aaa; - margin-bottom: 15px; - line-height: 1.5; - } - - .mission-rewards { - display: flex; - gap: 10px; - flex-wrap: wrap; - margin-bottom: 10px; - } - - .reward-item { - display: flex; - align-items: center; - gap: 5px; - font-size: 12px; - color: #ffd700; - } - - .mission-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 15px; - padding-top: 15px; - border-top: 1px solid #555; - } - - .difficulty { - font-size: 12px; - color: #aaa; - } - - .select-button { - background: #008800; - border: 2px solid #00ff00; - color: white; - padding: 8px 16px; - font-family: inherit; - font-size: 14px; - font-weight: bold; - cursor: pointer; - transition: all 0.2s; - border-radius: 4px; - } - - .select-button:hover:not(:disabled) { - background: #00aa00; - box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); - } - - .select-button:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - .empty-state { - text-align: center; - padding: 40px; - color: #666; - } - `; - } - - static get properties() { - return { - missions: { type: Array }, - completedMissions: { type: Set }, - }; - } - - constructor() { - super(); - this.missions = []; - this.completedMissions = new Set(); - } - - connectedCallback() { - super.connectedCallback(); - this._loadMissions(); - } - - _loadMissions() { - // Get all registered missions from MissionManager - const missionRegistry = gameStateManager.missionManager.missionRegistry; - this.missions = Array.from(missionRegistry.values()); - this.completedMissions = gameStateManager.missionManager.completedMissions || new Set(); - this.requestUpdate(); - } - - _isMissionAvailable(mission) { - // Check if mission prerequisites are met - // For now, all missions are available unless they have explicit prerequisites - return true; - } - - _isMissionCompleted(missionId) { - return this.completedMissions.has(missionId); - } - - _selectMission(mission) { - if (!this._isMissionAvailable(mission)) { - return; - } - - // Dispatch mission-selected event - this.dispatchEvent( - new CustomEvent('mission-selected', { - detail: { missionId: mission.id }, - bubbles: true, - composed: true, - }) - ); - } - - _formatRewards(rewards) { - const rewardItems = []; - - if (rewards?.currency) { - // Handle both snake_case (from JSON) and camelCase (from code) - const shards = rewards.currency.aether_shards || rewards.currency.aetherShards || 0; - const cores = rewards.currency.ancient_cores || rewards.currency.ancientCores || 0; - - if (shards > 0) { - rewardItems.push({ icon: '💎', text: `${shards} Shards` }); - } - if (cores > 0) { - rewardItems.push({ icon: '⚙️', text: `${cores} Cores` }); - } - } - - if (rewards?.xp) { - rewardItems.push({ icon: '⭐', text: `${rewards.xp} XP` }); - } - - return rewardItems; - } - - _getDifficultyLabel(config) { - if (config?.difficulty_tier) { - return `Tier ${config.difficulty_tier}`; - } - if (config?.difficulty) { - return config.difficulty.toUpperCase(); - } - if (config?.recommended_level) { - return `Level ${config.recommended_level}`; - } - return 'Unknown'; - } - - render() { - if (this.missions.length === 0) { - return html` -No missions available at this time.
-- ${mission.config?.description || 'No description available.'} -
- - ${rewards.length > 0 ? html` -No missions available at this time.
++ ${mission.config?.description || 'No description available.'} +
+ + ${rewards.length > 0 ? html` +