feat: spell book UI, iron skin spell, buffs system and devlog update
This commit is contained in:
42
DEVLOG.md
42
DEVLOG.md
@@ -379,3 +379,45 @@ Establecimiento de la base completa del motor de juego con generación procedime
|
|||||||
- ✅ Visualización 3D con Three.js
|
- ✅ Visualización 3D con Three.js
|
||||||
- ✅ Sistema de cámara isométrica
|
- ✅ Sistema de cámara isométrica
|
||||||
- ✅ Carga de texturas y assets
|
- ✅ Carga de texturas y assets
|
||||||
|
|
||||||
|
## Sesión 9: Pulido de Combate, UI de Hechizos y Buffs
|
||||||
|
**Fecha:** 8 de Enero de 2026
|
||||||
|
|
||||||
|
### Objetivos
|
||||||
|
- Resolver la duplicación de animaciones en el ataque de los monstruos.
|
||||||
|
- Mejorar la interfaz de usuario para el manejo de hechizos (Libro de Hechizos Visual).
|
||||||
|
- Implementar validaciones de línea de visión (LOS) en el lanzamiento de hechizos.
|
||||||
|
- Añadir nuevos hechizos ("Piel de Hierro") y sistema de duración de efectos (Buffs).
|
||||||
|
|
||||||
|
### Cambios Realizados
|
||||||
|
|
||||||
|
#### 1. Corrección de Animaciones y Audio
|
||||||
|
- **Doble Animación**: Se eliminó la llamada redundante a `onEntityHit` dentro de `MonsterAI.js`. Ahora el feedback visual (destello rojo/temblor) se delega exclusivamente a `game.onCombatResult`, unificando el flujo entre héroes y monstruos y evitando que la animación se dispare dos veces.
|
||||||
|
- **Audio**: Se investigó el retraso en el audio del golpe. Se decidió mantener el sonido actual (`sword1.mp3`) por el momento.
|
||||||
|
|
||||||
|
#### 2. Interfaz de Usuario (UI)
|
||||||
|
- **Botón de Inventario**: Añadido un botón placeholder "🎒 INVENTARIO" a las fichas de todos los aventureros.
|
||||||
|
- **Libro de Hechizos (Mago)**:
|
||||||
|
- Se reemplazó la lista de botones de texto por un sistema visual de cartas.
|
||||||
|
- Al hacer clic en "HECHIZOS", se despliega una mano de cartas generadas dinámicamente con plantillas (`attack_template`, `defense_template`, `healing_template`).
|
||||||
|
- Las cartas muestran el coste de poder en la esquina y se oscurecen si no hay maná suficiente.
|
||||||
|
- Implementado cierre automático al seleccionar o hacer clic fuera.
|
||||||
|
|
||||||
|
#### 3. Sistema de Magia y Buffs
|
||||||
|
- **Validación LOS**: Corregido bug donde "Bola de Fuego" podía lanzarse a través de muros aunque la previsualización mostrara rojo. Ahora `onCellClick` valida estrictamente la línea de visión antes de ejecutar.
|
||||||
|
- **Nuevo Hechizo: Piel de Hierro**:
|
||||||
|
- Coste: 5. Tipo: Defensa.
|
||||||
|
- Efecto: Otorga +2 a Resistencia durante 1 turno.
|
||||||
|
- Requiere selección de objetivo (héroe).
|
||||||
|
- **Sistema de Buffs Temporales**:
|
||||||
|
- Implementado evento `turn_ended` en `TurnManager`.
|
||||||
|
- Añadido método `handleEndTurn` en `GameEngine` para gestionar la duración de los efectos.
|
||||||
|
- Los buffs ahora se limpian automáticamente cuando su duración llega a 0, revirtiendo los cambios en las estadísticas.
|
||||||
|
|
||||||
|
### Estado Actual
|
||||||
|
El combate se siente mucho más sólido sin las animaciones dobles. La interfaz del mago es ahora visualmente atractiva y funcional. El sistema de magia soporta hechizos de defensa y buffs con duración limitada, abriendo la puerta a mecánicas más complejas.
|
||||||
|
|
||||||
|
### Próximos Pasos
|
||||||
|
- Implementar la funcionalidad real del Inventario.
|
||||||
|
- Añadir más cartas/hechizos y refinar el diseño visual de los textos en las cartas.
|
||||||
|
- Ajustar el timing del sonido de ataque para sincronizarlo perfectamente con la animación de impacto.
|
||||||
|
|||||||
BIN
public/assets/images/dungeon1/spells/attack_template.png
Normal file
BIN
public/assets/images/dungeon1/spells/attack_template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/assets/images/dungeon1/spells/defense_template.png
Normal file
BIN
public/assets/images/dungeon1/spells/defense_template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/assets/images/dungeon1/spells/healing_template.png
Normal file
BIN
public/assets/images/dungeon1/spells/healing_template.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 MiB |
@@ -4,18 +4,32 @@ export const SPELLS = [
|
|||||||
id: 'fireball',
|
id: 'fireball',
|
||||||
name: 'Bola de Fuego',
|
name: 'Bola de Fuego',
|
||||||
type: 'attack',
|
type: 'attack',
|
||||||
cost: 1,
|
cost: 5,
|
||||||
range: 12, // Arbitrary line of sight
|
range: 12, // Arbitrary line of sight
|
||||||
damageDice: 1,
|
damageDice: 1,
|
||||||
damageBonus: 'hero_level', // Dynamic logic
|
damageBonus: 'hero_level', // Dynamic logic
|
||||||
area: 2, // 2x2
|
area: 2, // 2x2
|
||||||
description: "Elige un área de 2x2 casillas en línea de visión. Cada miniatura sufre 1D6 + Nivel herois."
|
description: "Elige un área de 2x2 casillas en línea de visión. Cada miniatura sufre 1D6 + Nivel herois."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'iron_skin',
|
||||||
|
name: 'Piel de Hierro',
|
||||||
|
type: 'defense',
|
||||||
|
cost: 1,
|
||||||
|
range: 'board', // Anywhere on board
|
||||||
|
target: 'single_hero', // Needs selection
|
||||||
|
effect: {
|
||||||
|
stat: 'toughness',
|
||||||
|
value: 2,
|
||||||
|
duration: 1
|
||||||
|
},
|
||||||
|
description: "Elige a un Aventurero. +2 a Resistencia durante este turno."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'healing_hands',
|
id: 'healing_hands',
|
||||||
name: 'Manos Curadoras',
|
name: 'Manos Curadoras',
|
||||||
type: 'heal',
|
type: 'heal',
|
||||||
cost: 1,
|
cost: 2,
|
||||||
range: 'board', // Same board section
|
range: 'board', // Same board section
|
||||||
healAmount: 1,
|
healAmount: 1,
|
||||||
target: 'all_heroes',
|
target: 'all_heroes',
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export class GameEngine {
|
|||||||
this.resetHeroMoves();
|
this.resetHeroMoves();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// End of Turn Logic (Buffs, cooldowns, etc)
|
||||||
|
this.turnManager.on('turn_ended', (turn) => {
|
||||||
|
this.handleEndTurn();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
resetHeroMoves() {
|
resetHeroMoves() {
|
||||||
@@ -64,6 +69,49 @@ export class GameEngine {
|
|||||||
console.log("Refilled Hero Moves");
|
console.log("Refilled Hero Moves");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEndTurn() {
|
||||||
|
console.log("[GameEngine] Handling End of Turn Effects...");
|
||||||
|
|
||||||
|
if (!this.heroes) return;
|
||||||
|
|
||||||
|
this.heroes.forEach(hero => {
|
||||||
|
if (hero.buffs && hero.buffs.length > 0) {
|
||||||
|
// Decrement duration
|
||||||
|
hero.buffs.forEach(buff => {
|
||||||
|
buff.duration--;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove expired
|
||||||
|
const activeBuffs = [];
|
||||||
|
const expiredBuffs = [];
|
||||||
|
|
||||||
|
hero.buffs.forEach(buff => {
|
||||||
|
if (buff.duration > 0) {
|
||||||
|
activeBuffs.push(buff);
|
||||||
|
} else {
|
||||||
|
expiredBuffs.push(buff);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revert expired
|
||||||
|
expiredBuffs.forEach(buff => {
|
||||||
|
if (buff.stat === 'toughness') {
|
||||||
|
hero.stats.toughness -= buff.value;
|
||||||
|
if (hero.tempStats && hero.tempStats.toughnessBonus) {
|
||||||
|
hero.tempStats.toughnessBonus -= buff.value;
|
||||||
|
}
|
||||||
|
console.log(`[GameEngine] Buff expired: ${buff.id} on ${hero.name}. -${buff.value} ${buff.stat}`);
|
||||||
|
if (this.onShowMessage) {
|
||||||
|
this.onShowMessage("Efecto Finalizado", `La ${buff.id === 'iron_skin' ? 'Piel de Hierro' : 'Magia'} de ${hero.name} se desvanece.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hero.buffs = activeBuffs;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createParty() {
|
createParty() {
|
||||||
this.heroes = [];
|
this.heroes = [];
|
||||||
this.monsters = []; // Initialize monsters array
|
this.monsters = []; // Initialize monsters array
|
||||||
@@ -204,17 +252,32 @@ export class GameEngine {
|
|||||||
targetCells.push({ x: x, y: y });
|
targetCells.push({ x: x, y: y });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW: Enforce LOS Check before execution
|
||||||
|
const caster = this.selectedEntity;
|
||||||
|
if (caster) {
|
||||||
|
const targetObj = { x: x, y: y };
|
||||||
|
const los = this.checkLineOfSightStrict(caster, targetObj);
|
||||||
|
if (!los || !los.clear) {
|
||||||
|
if (this.onShowMessage) this.onShowMessage('Bloqueado', 'No tienes línea de visión.');
|
||||||
|
// Do NOT cancel targeting, let them try again
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Execute Spell
|
// Execute Spell
|
||||||
const result = this.executeSpell(this.currentSpell, targetCells);
|
const result = this.executeSpell(this.currentSpell, targetCells);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Success
|
// Success
|
||||||
|
this.cancelTargeting();
|
||||||
|
if (window.RENDERER) window.RENDERER.hideAreaPreview();
|
||||||
} else {
|
} else {
|
||||||
if (this.onShowMessage) this.onShowMessage('Fallo', result.reason || 'No se pudo lanzar el hechizo.');
|
if (this.onShowMessage) this.onShowMessage('Fallo', result.reason || 'No se pudo lanzar el hechizo.');
|
||||||
|
this.cancelTargeting(); // Cancel on error? maybe keep open? usually cancel.
|
||||||
|
if (window.RENDERER) window.RENDERER.hideAreaPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancelTargeting();
|
|
||||||
if (window.RENDERER) window.RENDERER.hideAreaPreview();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export class MagicSystem {
|
|||||||
return this.resolveHeal(caster, spell);
|
return this.resolveHeal(caster, spell);
|
||||||
} else if (spell.type === 'attack') {
|
} else if (spell.type === 'attack') {
|
||||||
return this.resolveAttack(caster, spell, targetCells);
|
return this.resolveAttack(caster, spell, targetCells);
|
||||||
|
} else if (spell.type === 'defense') {
|
||||||
|
return this.resolveDefense(caster, spell, targetCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, reason: 'unknown_spell_type' };
|
return { success: false, reason: 'unknown_spell_type' };
|
||||||
@@ -156,4 +158,67 @@ export class MagicSystem {
|
|||||||
|
|
||||||
return { success: true, type: 'attack', hits: 1 }; // Return success immediately
|
return { success: true, type: 'attack', hits: 1 }; // Return success immediately
|
||||||
}
|
}
|
||||||
|
resolveDefense(caster, spell, targetCells) {
|
||||||
|
// Needs a target hero
|
||||||
|
let targetHero = null;
|
||||||
|
|
||||||
|
// Find hero in target cells
|
||||||
|
for (const cell of targetCells) {
|
||||||
|
const h = this.game.heroes.find(h => h.x === cell.x && h.y === cell.y);
|
||||||
|
if (h) {
|
||||||
|
targetHero = h;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetHero) {
|
||||||
|
return { success: false, reason: 'no_target_hero' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const effect = spell.effect;
|
||||||
|
if (!effect) return { success: false };
|
||||||
|
|
||||||
|
// Apply Buff
|
||||||
|
if (effect.stat === 'toughness') {
|
||||||
|
// Store original if not already stored (simple buffering)
|
||||||
|
if (!targetHero.tempStats) targetHero.tempStats = {};
|
||||||
|
|
||||||
|
// Stackable? Probably not for same spell.
|
||||||
|
// Check if already has this buff?
|
||||||
|
// For simplicity: Add modifier
|
||||||
|
if (!targetHero.tempStats.toughnessBonus) targetHero.tempStats.toughnessBonus = 0;
|
||||||
|
|
||||||
|
targetHero.tempStats.toughnessBonus += effect.value;
|
||||||
|
// Also modify actual stat for calculation access?
|
||||||
|
// Usually stats are accessed via getter or direct.
|
||||||
|
// If direct property, we modify it and store original?
|
||||||
|
// Let's modify the stat directly for now and trust Turn Manager to revert or track it.
|
||||||
|
// BETTER: modify 'toughness' in stats, store 'buff_iron_skin' in activeBuffs?
|
||||||
|
|
||||||
|
targetHero.stats.toughness += effect.value;
|
||||||
|
|
||||||
|
// Mark for cleanup (Pseudo-implementation for cleanup)
|
||||||
|
if (!targetHero.buffs) targetHero.buffs = [];
|
||||||
|
targetHero.buffs.push({
|
||||||
|
id: spell.id,
|
||||||
|
stat: 'toughness',
|
||||||
|
value: effect.value,
|
||||||
|
duration: effect.duration
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.game.onShowMessage) {
|
||||||
|
this.game.onShowMessage('Piel de Hierro', `Resistencia de ${targetHero.name} +${effect.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual Effect
|
||||||
|
if (window.RENDERER) {
|
||||||
|
window.RENDERER.triggerVisualEffect('defense_buff', targetHero.x, targetHero.y);
|
||||||
|
// Highlight or keep aura?
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MagicSystem] Applied ${spell.name} to ${targetHero.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, type: 'defense', target: targetHero.name };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,9 +230,8 @@ export class MonsterAI {
|
|||||||
// Step 2: Attack animation delay (500ms)
|
// Step 2: Attack animation delay (500ms)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Step 3: Trigger hit visual on defender (if hit succeeded)
|
// Step 3: Trigger hit visual on defender (if hit succeeded)
|
||||||
if (result.hitSuccess && this.game.onEntityHit) {
|
// Step 3: Trigger hit visual on defender REMOVED (Handled by onCombatResult)
|
||||||
this.game.onEntityHit(hero.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
|
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export class TurnManager {
|
|||||||
|
|
||||||
endTurn() {
|
endTurn() {
|
||||||
console.log(`--- TURN ${this.currentTurn} END ---`);
|
console.log(`--- TURN ${this.currentTurn} END ---`);
|
||||||
|
this.emit('turn_ended', this.currentTurn);
|
||||||
this.currentTurn++;
|
this.currentTurn++;
|
||||||
this.startPowerPhase();
|
this.startPowerPhase();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -600,50 +600,45 @@ export class UIManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
card.appendChild(bowBtn);
|
card.appendChild(bowBtn);
|
||||||
} else if (hero.key === 'wizard') {
|
}
|
||||||
// SPELLS UI
|
|
||||||
const spellsTitle = document.createElement('div');
|
|
||||||
spellsTitle.textContent = "HECHIZOS (Poder: " + (this.game.turnManager.power || 0) + ")";
|
|
||||||
spellsTitle.style.marginTop = '10px';
|
|
||||||
spellsTitle.style.fontSize = '12px';
|
|
||||||
spellsTitle.style.fontWeight = 'bold';
|
|
||||||
spellsTitle.style.color = '#aa88ff';
|
|
||||||
spellsTitle.style.borderBottom = '1px solid #aa88ff';
|
|
||||||
card.appendChild(spellsTitle);
|
|
||||||
|
|
||||||
SPELLS.forEach(spell => {
|
// INVENTORY BUTTON (For All Heroes)
|
||||||
const btn = document.createElement('button');
|
const invBtn = document.createElement('button');
|
||||||
btn.textContent = `${spell.name} (${spell.cost})`;
|
invBtn.textContent = '🎒 INVENTARIO';
|
||||||
btn.title = spell.description;
|
invBtn.style.width = '100%';
|
||||||
btn.style.width = '100%';
|
invBtn.style.padding = '8px';
|
||||||
btn.style.padding = '5px';
|
invBtn.style.marginTop = '8px';
|
||||||
btn.style.marginTop = '4px';
|
invBtn.style.backgroundColor = '#444';
|
||||||
btn.style.fontSize = '11px';
|
invBtn.style.color = '#fff';
|
||||||
btn.style.fontFamily = '"Cinzel", serif';
|
invBtn.style.border = '1px solid #777';
|
||||||
|
invBtn.style.borderRadius = '4px';
|
||||||
|
invBtn.style.fontFamily = '"Cinzel", serif';
|
||||||
|
invBtn.style.fontSize = '12px';
|
||||||
|
invBtn.style.cursor = 'not-allowed'; // Placeholder functionality
|
||||||
|
invBtn.title = 'Inventario (Próximamente)';
|
||||||
|
card.appendChild(invBtn);
|
||||||
|
|
||||||
const canCast = this.game.canCastSpell(spell);
|
// SPELLS UI (Wizard Only)
|
||||||
|
if (hero.key === 'wizard') {
|
||||||
|
const spellsBtn = document.createElement('button');
|
||||||
|
spellsBtn.textContent = '🔮 HECHIZOS';
|
||||||
|
spellsBtn.style.width = '100%';
|
||||||
|
spellsBtn.style.padding = '8px';
|
||||||
|
spellsBtn.style.marginTop = '5px';
|
||||||
|
spellsBtn.style.backgroundColor = '#4b0082'; // Indigo
|
||||||
|
spellsBtn.style.color = '#fff';
|
||||||
|
spellsBtn.style.border = '1px solid #8a2be2';
|
||||||
|
spellsBtn.style.borderRadius = '4px';
|
||||||
|
spellsBtn.style.fontFamily = '"Cinzel", serif';
|
||||||
|
spellsBtn.style.cursor = 'pointer';
|
||||||
|
|
||||||
btn.style.backgroundColor = canCast ? '#4b0082' : '#333';
|
spellsBtn.onclick = (e) => {
|
||||||
btn.style.color = canCast ? '#fff' : '#888';
|
e.stopPropagation();
|
||||||
btn.style.border = '1px solid #666';
|
// Toggle Spell Book UI
|
||||||
btn.style.cursor = canCast ? 'pointer' : 'not-allowed';
|
this.toggleSpellBook(hero);
|
||||||
|
};
|
||||||
|
|
||||||
if (canCast) {
|
card.appendChild(spellsBtn);
|
||||||
btn.onclick = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (spell.type === 'attack') {
|
|
||||||
// Use Targeting Mode
|
|
||||||
this.game.startSpellTargeting(spell);
|
|
||||||
} else {
|
|
||||||
// Healing is instant/global (no target needed for 'healing_hands')
|
|
||||||
this.game.executeSpell(spell);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
card.appendChild(btn);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
card.dataset.heroId = hero.id;
|
card.dataset.heroId = hero.id;
|
||||||
@@ -1381,4 +1376,165 @@ export class UIManager {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
toggleSpellBook(hero) {
|
||||||
|
// Close if already open
|
||||||
|
if (this.spellBookContainer) {
|
||||||
|
document.body.removeChild(this.spellBookContainer);
|
||||||
|
this.spellBookContainer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Container
|
||||||
|
const container = document.createElement('div');
|
||||||
|
Object.assign(container.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '140px', // Just above placement/UI bottom area
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '15px',
|
||||||
|
backgroundColor: 'rgba(20, 10, 30, 0.9)',
|
||||||
|
padding: '20px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: '2px solid #9933ff',
|
||||||
|
zIndex: '1500',
|
||||||
|
boxShadow: '0 0 20px rgba(100, 0, 255, 0.5)'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Title
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.textContent = "LIBRO DE HECHIZOS";
|
||||||
|
Object.assign(title.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-30px',
|
||||||
|
left: '0',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: '#d8bfff',
|
||||||
|
fontFamily: '"Cinzel", serif',
|
||||||
|
fontSize: '18px',
|
||||||
|
textShadow: '0 0 5px #8a2be2'
|
||||||
|
});
|
||||||
|
container.appendChild(title);
|
||||||
|
|
||||||
|
const currentPower = this.game.turnManager.power || 0;
|
||||||
|
|
||||||
|
// Render Spells as Cards
|
||||||
|
SPELLS.forEach(spell => {
|
||||||
|
const canCast = this.game.canCastSpell(spell);
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
Object.assign(card.style, {
|
||||||
|
width: '180px',
|
||||||
|
height: '260px',
|
||||||
|
position: 'relative',
|
||||||
|
cursor: canCast ? 'pointer' : 'not-allowed',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
filter: canCast ? 'none' : 'grayscale(100%) brightness(50%)',
|
||||||
|
backgroundImage: this.getSpellTemplate(spell.type),
|
||||||
|
backgroundSize: 'cover'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canCast) {
|
||||||
|
card.onmouseenter = () => { card.style.transform = 'scale(1.1) translateY(-10px)'; card.style.zIndex = '10'; };
|
||||||
|
card.onmouseleave = () => { card.style.transform = 'scale(1)'; card.style.zIndex = '1'; };
|
||||||
|
|
||||||
|
card.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// Close book
|
||||||
|
document.body.removeChild(this.spellBookContainer);
|
||||||
|
this.spellBookContainer = null;
|
||||||
|
|
||||||
|
if (spell.type === 'attack' || spell.type === 'defense') {
|
||||||
|
// Use Targeting Mode for attacks AND defense (buffs on allies)
|
||||||
|
this.game.startSpellTargeting(spell);
|
||||||
|
} else {
|
||||||
|
// Global/Instant spells (like healing_hands currently configured as global in previous logic, though typically healing hands is touch... let's keep it consistent with previous logic for now)
|
||||||
|
this.game.executeSpell(spell);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// COST INDICATOR (Top Left)
|
||||||
|
const costBadge = document.createElement('div');
|
||||||
|
costBadge.textContent = spell.cost;
|
||||||
|
Object.assign(costBadge.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '12px',
|
||||||
|
left: '12px',
|
||||||
|
width: '30px',
|
||||||
|
height: '30px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
color: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '2px solid #000',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontFamily: 'serif'
|
||||||
|
});
|
||||||
|
card.appendChild(costBadge);
|
||||||
|
|
||||||
|
// NAME (Middle/Top area roughly)
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
|
nameEl.textContent = spell.name.toUpperCase();
|
||||||
|
Object.assign(nameEl.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '45px', // Adjusted for template header
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#000',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontFamily: '"Cinzel", serif',
|
||||||
|
padding: '0 10px',
|
||||||
|
boxSizing: 'border-box'
|
||||||
|
});
|
||||||
|
card.appendChild(nameEl);
|
||||||
|
|
||||||
|
// DESCRIPTION (Bottom area)
|
||||||
|
const descEl = document.createElement('div');
|
||||||
|
descEl.textContent = spell.description;
|
||||||
|
Object.assign(descEl.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '30px',
|
||||||
|
left: '10px',
|
||||||
|
width: '160px',
|
||||||
|
height: '80px',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#000',
|
||||||
|
textAlign: 'center',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontFamily: 'serif',
|
||||||
|
lineHeight: '1.2'
|
||||||
|
});
|
||||||
|
card.appendChild(descEl);
|
||||||
|
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
this.spellBookContainer = container;
|
||||||
|
|
||||||
|
// Close on click outside (simple implementation)
|
||||||
|
const closeHandler = (e) => {
|
||||||
|
if (this.spellBookContainer && !this.spellBookContainer.contains(e.target) && e.target !== this.spellBookContainer) {
|
||||||
|
// We don't auto close here to avoid conflicts, just relying on toggle or card click
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getSpellTemplate(type) {
|
||||||
|
// Return CSS url string based on type
|
||||||
|
// Templates: attack_template.png, defense_template.png, healing_template.png
|
||||||
|
let filename = 'attack_template.png';
|
||||||
|
if (type === 'heal') filename = 'healing_template.png';
|
||||||
|
if (type === 'defense') filename = 'defense_template.png';
|
||||||
|
|
||||||
|
return `url('/assets/images/dungeon1/spells/${filename}')`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user