Add skill unlocking and movement functionality to combat system
Implement event handling for skill unlocking to refresh the combat HUD. Introduce movement mode activation via hotkey during combat, allowing players to switch between skill targeting and movement. Enhance the GameLoop to manage skill usage and cooldowns effectively, and update the CombatHUD with new UI elements for movement actions. Ensure proper integration with existing game state management for a seamless user experience.
This commit is contained in:
parent
68646f7f7b
commit
178389309d
5 changed files with 244 additions and 14 deletions
|
|
@ -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;
|
||||
|
||||
skills.push({
|
||||
// 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: cooldown,
|
||||
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: existingAction.name,
|
||||
icon: existingAction.icon,
|
||||
costAP: costAP,
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
11
src/index.js
11
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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
|
||||
<!-- Action Bar (Bottom-Center) -->
|
||||
<div class="action-bar">
|
||||
<!-- Movement Button -->
|
||||
<button
|
||||
class="movement-button ${!this.combatState?.targetingMode
|
||||
? "active"
|
||||
: ""}"
|
||||
@click="${this._handleMovementClick}"
|
||||
title="Movement Mode (M)"
|
||||
>
|
||||
<span class="icon">🚶</span>
|
||||
<span class="name">Move</span>
|
||||
</button>
|
||||
${activeUnit?.skills?.map(
|
||||
(skill, index) => html`
|
||||
<button
|
||||
class="skill-button"
|
||||
class="skill-button ${this.combatState?.targetingMode &&
|
||||
this.combatState?.activeSkillId === skill.id
|
||||
? "active"
|
||||
: ""}"
|
||||
?disabled="${!skill.isAvailable}"
|
||||
@click="${() => this._handleSkillClick(skill.id)}"
|
||||
@mouseenter="${() => this._handleSkillHover(skill.id)}"
|
||||
|
|
|
|||
|
|
@ -62,6 +62,12 @@ export class GameViewport extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
#handleMovementClick() {
|
||||
if (gameStateManager.gameLoop) {
|
||||
gameStateManager.gameLoop.onMovementClicked();
|
||||
}
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
const container = this.shadowRoot.getElementById("canvas-container");
|
||||
const loop = new GameLoop();
|
||||
|
|
@ -127,6 +133,7 @@ export class GameViewport extends LitElement {
|
|||
.combatState=${this.combatState}
|
||||
@end-turn=${this.#handleEndTurn}
|
||||
@skill-click=${this.#handleSkillClick}
|
||||
@movement-click=${this.#handleMovementClick}
|
||||
></combat-hud>
|
||||
<dialogue-overlay></dialogue-overlay>`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue