diff --git a/src/core/GameLoop.js b/src/core/GameLoop.js index a380592..e162e0d 100644 --- a/src/core/GameLoop.js +++ b/src/core/GameLoop.js @@ -312,6 +312,43 @@ export class GameLoop { // Open character sheet for active unit this.openCharacterSheet(); } + if (code === "KeyM") { + // Movement mode hotkey + if (this.gameStateManager?.currentState === "STATE_COMBAT") { + this.onMovementClicked(); + } + } + // Number key hotkeys for skills (1-5) + if ( + this.gameStateManager?.currentState === "STATE_COMBAT" && + this.turnSystem + ) { + const activeUnit = this.turnSystem.getActiveUnit(); + if (activeUnit && activeUnit.team === "PLAYER") { + const skills = activeUnit.actions || []; + let skillIndex = -1; + + // Map key codes to skill indices (1-5) + if (code === "Digit1" || code === "Numpad1") { + skillIndex = 0; + } else if (code === "Digit2" || code === "Numpad2") { + skillIndex = 1; + } else if (code === "Digit3" || code === "Numpad3") { + skillIndex = 2; + } else if (code === "Digit4" || code === "Numpad4") { + skillIndex = 3; + } else if (code === "Digit5" || code === "Numpad5") { + skillIndex = 4; + } + + if (skillIndex >= 0 && skillIndex < skills.length) { + const skill = skills[skillIndex]; + if (skill && skill.id) { + this.onSkillClicked(skill.id); + } + } + } + } } /** @@ -488,6 +525,12 @@ export class GameLoop { const activeUnit = this.turnSystem.getActiveUnit(); if (!activeUnit || activeUnit.team !== "PLAYER") return; + // If clicking the same skill that's already active, cancel targeting + if (this.combatState === "TARGETING_SKILL" && this.activeSkillId === skillId) { + this.cancelSkillTargeting(); + return; + } + // Find skill in unit's actions const skill = (activeUnit.actions || []).find((a) => a.id === skillId); if (!skill) { @@ -516,6 +559,9 @@ export class GameLoop { ); } + // Update combat state to refresh UI (show cancel button) + this.updateCombatState().catch(console.error); + console.log(`Entering targeting mode for skill: ${skillId}`); } @@ -655,6 +701,29 @@ export class GameLoop { this.updateMovementHighlights(activeUnit); } } + // Update combat state to refresh UI + this.updateCombatState().catch(console.error); + } + + /** + * Handles movement button click from CombatHUD. + * Returns to movement mode from skill targeting. + */ + onMovementClicked() { + if (!this.turnSystem) return; + + const activeUnit = this.turnSystem.getActiveUnit(); + if (!activeUnit || activeUnit.team !== "PLAYER") return; + + // If we're in skill targeting mode, cancel it and return to movement + if (this.combatState === "TARGETING_SKILL") { + this.cancelSkillTargeting(); + } else { + // If already in movement mode, ensure movement highlights are shown + this.updateMovementHighlights(activeUnit); + // Update combat state to refresh UI + this.updateCombatState().catch(console.error); + } } /** @@ -1576,9 +1645,28 @@ export class GameLoop { // Build active unit status if we have an active unit (for UI) let unitStatus = null; if (activeUnit) { - // Calculate max AP using formula: 3 + floor(speed/5) - const speed = activeUnit.baseStats?.speed || 10; - const maxAP = 3 + Math.floor(speed / 5); + // Calculate effective speed (including equipment and skill tree bonuses) + let effectiveSpeed = activeUnit.baseStats?.speed || 10; + + // Add equipment bonuses if available + if (activeUnit.loadout && this.inventoryManager) { + const loadoutSlots = ["mainHand", "offHand", "body", "accessory"]; + for (const slot of loadoutSlots) { + const itemInstance = activeUnit.loadout[slot]; + if (itemInstance) { + const itemDef = this.inventoryManager.itemRegistry?.get( + itemInstance.defId + ); + if (itemDef && itemDef.stats && itemDef.stats.speed) { + effectiveSpeed += itemDef.stats.speed; + } + } + } + } + + // Calculate max AP using formula: 3 + floor(effectiveSpeed/5) + // We'll add skill tree bonuses to speed below when we generate the skill tree + let maxAP = 3 + Math.floor(effectiveSpeed / 5); // Convert status effects to status icons const statuses = (activeUnit.statusEffects || []).map((effect) => ({ @@ -1642,6 +1730,22 @@ export class GameLoop { const factory = new SkillTreeFactory(templateRegistry, skillMap); const skillTree = factory.createTree(classDef); + // Add speed boosts from unlocked nodes to effective speed + for (const nodeId of mastery.unlockedNodes) { + const nodeDef = skillTree.nodes?.[nodeId]; + if ( + nodeDef && + nodeDef.type === "STAT_BOOST" && + nodeDef.data && + nodeDef.data.stat === "speed" + ) { + effectiveSpeed += nodeDef.data.value || 0; + } + } + + // Recalculate maxAP with skill tree bonuses + maxAP = 3 + Math.floor(effectiveSpeed / 5); + // Add unlocked ACTIVE_SKILL nodes to skills array for (const nodeId of mastery.unlockedNodes) { const nodeDef = skillTree.nodes?.[nodeId]; @@ -1654,18 +1758,41 @@ export class GameLoop { // Add skill to skills array (avoid duplicates) if (!skills.find((s) => s.id === skillId)) { - // Get costAP and cooldown from full skill definition + // Get costAP from full skill definition const costAP = fullSkill?.costs?.ap || skillData.costAP || 3; - const cooldown = fullSkill?.cooldown_turns || skillData.cooldown || 0; + const baseCooldown = fullSkill?.cooldown_turns || skillData.cooldown || 0; + + // Ensure skill exists in unit.actions for cooldown tracking + if (!activeUnit.actions) { + activeUnit.actions = []; + } + let existingAction = activeUnit.actions.find( + (a) => a.id === skillId + ); + + // If action doesn't exist, create it with cooldown 0 (ready to use immediately) + if (!existingAction) { + existingAction = { + id: skillId, + name: skillData.name || fullSkill?.name || "Unknown Skill", + icon: skillData.icon || fullSkill?.icon || "⚔", + costAP: costAP, + cooldown: 0, // Newly unlocked skills start ready to use + }; + activeUnit.actions.push(existingAction); + } + + // Use current cooldown from the action (which gets decremented by TurnSystem) + const currentCooldown = existingAction.cooldown || 0; skills.push({ id: skillId, - name: skillData.name || fullSkill?.name || "Unknown Skill", - icon: skillData.icon || fullSkill?.icon || "⚔", + name: existingAction.name, + icon: existingAction.icon, costAP: costAP, - cooldown: cooldown, + cooldown: currentCooldown, isAvailable: - activeUnit.currentAP >= costAP && cooldown === 0, + activeUnit.currentAP >= costAP && currentCooldown === 0, }); } } @@ -1796,7 +1923,8 @@ export class GameLoop { // UI-enriched fields (for backward compatibility) activeUnit: unitStatus, // Object for UI enrichedQueue: enrichedQueue, // Objects for UI display - targetingMode: false, // Will be set when player selects a skill + targetingMode: this.combatState === "TARGETING_SKILL", // True when player is targeting a skill + activeSkillId: this.activeSkillId || null, // ID of the skill being targeted (for UI toggle state) roundNumber: turnSystemState.round, // Alias for UI }; @@ -1818,6 +1946,19 @@ export class GameLoop { return; } + // Clear any active skill targeting state and highlights + if (this.combatState === "TARGETING_SKILL" || this.activeSkillId) { + this.combatState = "IDLE"; + this.activeSkillId = null; + // Clear skill targeting highlights (range and AoE reticle) + if (this.voxelManager) { + this.voxelManager.clearHighlights(); + } + } + + // Clear movement highlights + this.clearMovementHighlights(); + // DELEGATE to TurnSystem this.turnSystem.endTurn(activeUnit); diff --git a/src/grid/VoxelManager.js b/src/grid/VoxelManager.js index 7c8ef3f..2ad3535 100644 --- a/src/grid/VoxelManager.js +++ b/src/grid/VoxelManager.js @@ -246,7 +246,7 @@ export class VoxelManager { // mesh.material.dispose(); } - mesh.dispose(); + // Note: Mesh objects don't have a dispose() method, only geometry and material do }); this.meshes.clear(); @@ -514,7 +514,7 @@ export class VoxelManager { mesh.material.dispose(); } } - mesh.dispose(); + // Note: Mesh objects don't have a dispose() method, only geometry and material do }); this.rangeHighlights.clear(); } @@ -534,7 +534,7 @@ export class VoxelManager { mesh.material.dispose(); } } - mesh.dispose(); + // Note: Mesh objects don't have a dispose() method, only geometry and material do }); this.aoeReticle.clear(); } diff --git a/src/index.js b/src/index.js index 50e2197..13a6ffa 100644 --- a/src/index.js +++ b/src/index.js @@ -150,9 +150,20 @@ window.addEventListener("open-character-sheet", async (e) => { 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") { + // 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); diff --git a/src/ui/combat-hud.js b/src/ui/combat-hud.js index 5c0397c..72f14fa 100644 --- a/src/ui/combat-hud.js +++ b/src/ui/combat-hud.js @@ -278,6 +278,7 @@ export class CombatHUD extends LitElement { position: relative; transition: all 0.2s; pointer-events: auto; + color: white; } .skill-button:hover:not(:disabled) { @@ -292,6 +293,52 @@ export class CombatHUD extends LitElement { 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; @@ -300,6 +347,7 @@ export class CombatHUD extends LitElement { background: rgba(0, 0, 0, 0.8); padding: 2px 4px; border: 1px solid #555; + color: white; } .skill-button .icon { @@ -439,6 +487,15 @@ export class CombatHUD extends LitElement { } } + _handleMovementClick() { + this.dispatchEvent( + new CustomEvent("movement-click", { + bubbles: true, + composed: true, + }) + ); + } + _handleSkillHover(skillId) { this.dispatchEvent( new CustomEvent("hover-skill", { @@ -596,10 +653,24 @@ export class CombatHUD extends LitElement {
+ + ${activeUnit?.skills?.map( (skill, index) => html`