feat: spell book UI, iron skin spell, buffs system and devlog update
This commit is contained in:
@@ -4,18 +4,32 @@ export const SPELLS = [
|
||||
id: 'fireball',
|
||||
name: 'Bola de Fuego',
|
||||
type: 'attack',
|
||||
cost: 1,
|
||||
cost: 5,
|
||||
range: 12, // Arbitrary line of sight
|
||||
damageDice: 1,
|
||||
damageBonus: 'hero_level', // Dynamic logic
|
||||
area: 2, // 2x2
|
||||
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',
|
||||
name: 'Manos Curadoras',
|
||||
type: 'heal',
|
||||
cost: 1,
|
||||
cost: 2,
|
||||
range: 'board', // Same board section
|
||||
healAmount: 1,
|
||||
target: 'all_heroes',
|
||||
|
||||
@@ -53,6 +53,11 @@ export class GameEngine {
|
||||
this.resetHeroMoves();
|
||||
}
|
||||
});
|
||||
|
||||
// End of Turn Logic (Buffs, cooldowns, etc)
|
||||
this.turnManager.on('turn_ended', (turn) => {
|
||||
this.handleEndTurn();
|
||||
});
|
||||
}
|
||||
|
||||
resetHeroMoves() {
|
||||
@@ -64,6 +69,49 @@ export class GameEngine {
|
||||
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() {
|
||||
this.heroes = [];
|
||||
this.monsters = []; // Initialize monsters array
|
||||
@@ -204,17 +252,32 @@ export class GameEngine {
|
||||
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
|
||||
const result = this.executeSpell(this.currentSpell, targetCells);
|
||||
|
||||
if (result.success) {
|
||||
// Success
|
||||
this.cancelTargeting();
|
||||
if (window.RENDERER) window.RENDERER.hideAreaPreview();
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ export class MagicSystem {
|
||||
return this.resolveHeal(caster, spell);
|
||||
} else if (spell.type === 'attack') {
|
||||
return this.resolveAttack(caster, spell, targetCells);
|
||||
} else if (spell.type === 'defense') {
|
||||
return this.resolveDefense(caster, spell, targetCells);
|
||||
}
|
||||
|
||||
return { success: false, reason: 'unknown_spell_type' };
|
||||
@@ -156,4 +158,67 @@ export class MagicSystem {
|
||||
|
||||
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)
|
||||
setTimeout(() => {
|
||||
// Step 3: Trigger hit visual on defender (if hit succeeded)
|
||||
if (result.hitSuccess && this.game.onEntityHit) {
|
||||
this.game.onEntityHit(hero.id);
|
||||
}
|
||||
// Step 3: Trigger hit visual on defender REMOVED (Handled by onCombatResult)
|
||||
|
||||
|
||||
// Step 4: Remove green ring after red ring appears (1200ms for red ring duration)
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -88,6 +88,7 @@ export class TurnManager {
|
||||
|
||||
endTurn() {
|
||||
console.log(`--- TURN ${this.currentTurn} END ---`);
|
||||
this.emit('turn_ended', this.currentTurn);
|
||||
this.currentTurn++;
|
||||
this.startPowerPhase();
|
||||
}
|
||||
|
||||
@@ -600,50 +600,45 @@ export class UIManager {
|
||||
};
|
||||
}
|
||||
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 => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = `${spell.name} (${spell.cost})`;
|
||||
btn.title = spell.description;
|
||||
btn.style.width = '100%';
|
||||
btn.style.padding = '5px';
|
||||
btn.style.marginTop = '4px';
|
||||
btn.style.fontSize = '11px';
|
||||
btn.style.fontFamily = '"Cinzel", serif';
|
||||
// INVENTORY BUTTON (For All Heroes)
|
||||
const invBtn = document.createElement('button');
|
||||
invBtn.textContent = '🎒 INVENTARIO';
|
||||
invBtn.style.width = '100%';
|
||||
invBtn.style.padding = '8px';
|
||||
invBtn.style.marginTop = '8px';
|
||||
invBtn.style.backgroundColor = '#444';
|
||||
invBtn.style.color = '#fff';
|
||||
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';
|
||||
btn.style.color = canCast ? '#fff' : '#888';
|
||||
btn.style.border = '1px solid #666';
|
||||
btn.style.cursor = canCast ? 'pointer' : 'not-allowed';
|
||||
spellsBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
// Toggle Spell Book UI
|
||||
this.toggleSpellBook(hero);
|
||||
};
|
||||
|
||||
if (canCast) {
|
||||
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.appendChild(spellsBtn);
|
||||
}
|
||||
|
||||
card.dataset.heroId = hero.id;
|
||||
@@ -1381,4 +1376,165 @@ export class UIManager {
|
||||
}, 500);
|
||||
}, 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