448 lines
19 KiB
JavaScript
448 lines
19 KiB
JavaScript
export class UnitCardManager {
|
|
constructor(parentContainer, game, callbacks) {
|
|
this.parentContainer = parentContainer;
|
|
this.game = game;
|
|
this.callbacks = callbacks || {}; // { showModal, toggleSpellBook }
|
|
|
|
this.cardsContainer = null;
|
|
this.currentHeroCard = null;
|
|
this.currentMonsterCard = null;
|
|
this.monsterContainer = null;
|
|
this.placeholderCard = null;
|
|
this.attackButton = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.cardsContainer = document.createElement('div');
|
|
Object.assign(this.cardsContainer.style, {
|
|
position: 'absolute',
|
|
left: '10px',
|
|
top: '220px', // Below minimap
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: '15px',
|
|
pointerEvents: 'auto'
|
|
});
|
|
this.parentContainer.appendChild(this.cardsContainer);
|
|
|
|
this.createPlaceholderCard();
|
|
}
|
|
|
|
createPlaceholderCard() {
|
|
const card = document.createElement('div');
|
|
Object.assign(card.style, {
|
|
width: '180px',
|
|
height: '280px',
|
|
backgroundColor: 'rgba(20, 20, 20, 0.95)',
|
|
border: '2px solid #8B4513',
|
|
borderRadius: '8px',
|
|
padding: '10px',
|
|
fontFamily: '"Cinzel", serif',
|
|
color: '#888',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
textAlign: 'center'
|
|
});
|
|
|
|
const iconContainer = document.createElement('div');
|
|
Object.assign(iconContainer.style, {
|
|
width: '100px',
|
|
height: '100px',
|
|
borderRadius: '50%',
|
|
border: '2px solid #8B4513',
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: '20px'
|
|
});
|
|
|
|
const icon = document.createElement('div');
|
|
icon.textContent = '🎴';
|
|
icon.style.fontSize = '48px';
|
|
iconContainer.appendChild(icon);
|
|
card.appendChild(iconContainer);
|
|
|
|
const text = document.createElement('div');
|
|
text.textContent = 'Selecciona un Aventurero';
|
|
text.style.fontSize = '14px';
|
|
text.style.color = '#DAA520';
|
|
card.appendChild(text);
|
|
|
|
this.placeholderCard = card;
|
|
this.cardsContainer.appendChild(card);
|
|
}
|
|
|
|
showHeroCard(hero) {
|
|
if (this.placeholderCard && this.placeholderCard.parentNode) {
|
|
this.cardsContainer.removeChild(this.placeholderCard);
|
|
}
|
|
if (this.currentHeroCard && this.currentHeroCard.parentNode) {
|
|
this.cardsContainer.removeChild(this.currentHeroCard);
|
|
}
|
|
|
|
this.currentHeroCard = this.createHeroCard(hero);
|
|
this.cardsContainer.insertBefore(this.currentHeroCard, this.cardsContainer.firstChild);
|
|
}
|
|
|
|
hideHeroCard() {
|
|
if (this.currentHeroCard && this.currentHeroCard.parentNode) {
|
|
this.cardsContainer.removeChild(this.currentHeroCard);
|
|
this.currentHeroCard = null;
|
|
}
|
|
// Show placeholder only if no cards are visible
|
|
if (!this.currentMonsterCard && this.placeholderCard && !this.placeholderCard.parentNode) {
|
|
this.cardsContainer.appendChild(this.placeholderCard);
|
|
}
|
|
}
|
|
|
|
updateHeroCard(heroId) {
|
|
if (!this.currentHeroCard || this.currentHeroCard.dataset.heroId !== heroId) return;
|
|
|
|
const hero = this.game.heroes.find(h => h.id === heroId);
|
|
if (!hero) return;
|
|
|
|
// NEW: Update stats using data-attributes for robustness
|
|
const updateStat = (key, value) => {
|
|
const el = this.currentHeroCard.querySelector(`[data-stat="${key}"]`);
|
|
if (el) el.textContent = value;
|
|
};
|
|
|
|
updateStat('Her', `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}`);
|
|
updateStat('Mov', `${hero.currentMoves || 0}/${hero.stats.move}`);
|
|
updateStat('Oro', hero.stats.gold || 0);
|
|
|
|
if (hero.key === 'wizard') {
|
|
updateStat('Pod', hero.stats.power || 0);
|
|
}
|
|
}
|
|
|
|
createHeroCard(hero) {
|
|
const card = document.createElement('div');
|
|
Object.assign(card.style, {
|
|
width: '180px',
|
|
backgroundColor: 'rgba(20, 20, 20, 0.95)',
|
|
border: '2px solid #8B4513',
|
|
borderRadius: '8px',
|
|
padding: '10px',
|
|
fontFamily: '"Cinzel", serif',
|
|
color: '#fff',
|
|
transition: 'all 0.3s',
|
|
cursor: 'pointer'
|
|
});
|
|
|
|
card.onmouseenter = () => { card.style.borderColor = '#DAA520'; card.style.transform = 'scale(1.05)'; };
|
|
card.onmouseleave = () => { card.style.borderColor = '#8B4513'; card.style.transform = 'scale(1)'; };
|
|
card.onclick = () => { if (this.game.onCellClick) this.game.onCellClick(hero.x, hero.y); };
|
|
|
|
// Portrait
|
|
const portrait = document.createElement('div');
|
|
Object.assign(portrait.style, {
|
|
width: '100px',
|
|
height: '100px',
|
|
borderRadius: '50%',
|
|
overflow: 'hidden',
|
|
border: '2px solid #DAA520',
|
|
marginBottom: '8px',
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto',
|
|
backgroundColor: '#000',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
});
|
|
|
|
const tokenPath = `/assets/images/dungeon1/tokens/heroes/${hero.key}.png?v=2`;
|
|
const img = document.createElement('img');
|
|
img.src = tokenPath;
|
|
Object.assign(img.style, { width: '100%', height: '100%', objectFit: 'cover' });
|
|
img.onerror = () => { portrait.innerHTML = `<div style="color: #DAA520; font-size: 48px;">?</div>`; };
|
|
portrait.appendChild(img);
|
|
card.appendChild(portrait);
|
|
|
|
// Name
|
|
const name = document.createElement('div');
|
|
name.textContent = hero.name;
|
|
Object.assign(name.style, {
|
|
fontSize: '16px', fontWeight: 'bold', color: '#DAA520', textAlign: 'center', marginBottom: '8px', textTransform: 'uppercase'
|
|
});
|
|
card.appendChild(name);
|
|
|
|
if (hero.hasLantern) {
|
|
const lantern = document.createElement('div');
|
|
lantern.textContent = '🏮 Portador de la Lámpara';
|
|
Object.assign(lantern.style, { fontSize: '10px', color: '#FFA500', textAlign: 'center', marginBottom: '8px' });
|
|
card.appendChild(lantern);
|
|
}
|
|
|
|
// Stats
|
|
const statsGrid = document.createElement('div');
|
|
Object.assign(statsGrid.style, { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '12px', marginBottom: '8px' });
|
|
|
|
const stats = [
|
|
{ label: 'H.C', value: hero.stats.ws || 0 },
|
|
{ label: 'H.P', value: hero.stats.bs || 0 },
|
|
{ label: 'Fuer', value: hero.stats.str || 0 },
|
|
{ label: 'Res', value: hero.stats.toughness || 0 },
|
|
{ label: 'Her', value: `${hero.currentWounds || hero.stats.wounds}/${hero.stats.wounds}` },
|
|
{ label: 'Ini', value: hero.stats.initiative || 0 },
|
|
{ label: 'Ata', value: hero.stats.attacks || 0 },
|
|
{ label: 'Mov', value: `${hero.currentMoves || 0}/${hero.stats.move}` },
|
|
{ label: 'Oro', value: hero.stats.gold || 0 }
|
|
];
|
|
|
|
// USER REQUEST: Show Power for Wizard
|
|
if (hero.key === 'wizard') {
|
|
stats.push({ label: 'Pod', value: hero.stats.power || 0 });
|
|
}
|
|
|
|
stats.forEach(stat => {
|
|
const el = document.createElement('div');
|
|
Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' });
|
|
|
|
const l = document.createElement('span'); l.textContent = stat.label + ':'; l.style.color = '#AAA';
|
|
const v = document.createElement('span'); v.textContent = stat.value; v.style.color = '#FFF'; v.style.fontWeight = 'bold';
|
|
v.dataset.stat = stat.label; // Add data attribute for easier updates
|
|
|
|
el.appendChild(l); el.appendChild(v);
|
|
statsGrid.appendChild(el);
|
|
});
|
|
card.appendChild(statsGrid);
|
|
|
|
// Elf Bow Button
|
|
if (hero.key === 'elf') {
|
|
const isPinned = this.game.isEntityPinned(hero);
|
|
const hasAttacked = hero.hasAttacked;
|
|
const bowBtn = document.createElement('button');
|
|
bowBtn.textContent = hasAttacked ? '🏹 YA DISPARADO' : '🏹 DISPARAR ARCO';
|
|
Object.assign(bowBtn.style, {
|
|
width: '100%', padding: '8px', marginTop: '8px',
|
|
color: '#fff', border: '1px solid #fff', borderRadius: '4px',
|
|
fontFamily: '"Cinzel", serif', cursor: (isPinned || hasAttacked) ? 'not-allowed' : 'pointer',
|
|
backgroundColor: (isPinned || hasAttacked) ? '#555' : '#2E8B57'
|
|
});
|
|
|
|
if (isPinned) bowBtn.title = "¡Estás trabado en combate cuerpo a cuerpo!";
|
|
else if (hasAttacked) bowBtn.title = "Ya has atacado en esta fase.";
|
|
else {
|
|
bowBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.game.startRangedTargeting();
|
|
if (this.callbacks.showModal) this.callbacks.showModal('Modo Disparo', 'Selecciona un enemigo visible para disparar.');
|
|
};
|
|
}
|
|
card.appendChild(bowBtn);
|
|
}
|
|
|
|
// Break Away Button (Destrabarse)
|
|
const isPinned = this.game.isEntityPinned(hero) && !hero.hasEscapedPin;
|
|
const canTryBreak = isPinned && hero.currentMoves > 0 && !hero.hasAttacked; // Can only try if hasn't acted yet?
|
|
// Rules say: "can attempt to escape... if achieved... moves as normal".
|
|
// If fails "must stay and fight".
|
|
|
|
if (isPinned) {
|
|
const breakBtn = document.createElement('button');
|
|
const target = hero.stats.pin_target || 6;
|
|
breakBtn.textContent = `🏃 DESTRABARSE (${target}+)`;
|
|
Object.assign(breakBtn.style, {
|
|
width: '100%', padding: '8px', marginTop: '8px',
|
|
color: '#fff', border: '1px solid #FFA500', borderRadius: '4px',
|
|
fontFamily: '"Cinzel", serif',
|
|
cursor: canTryBreak ? 'pointer' : 'not-allowed',
|
|
backgroundColor: canTryBreak ? '#FF8C00' : '#555'
|
|
});
|
|
|
|
if (!canTryBreak) {
|
|
if (hero.hasAttacked) breakBtn.title = "Ya has atacado.";
|
|
else if (hero.currentMoves <= 0) breakBtn.title = "No tienes movimiento.";
|
|
} else {
|
|
breakBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
const result = this.game.attemptBreakAway(hero);
|
|
|
|
// Show result
|
|
const color = result.success ? '#00ff00' : '#ff0000';
|
|
const msg = result.success ? "¡Escapada con éxito!" : "¡Fallo! Debes luchar.";
|
|
|
|
if (this.callbacks.showModal) {
|
|
this.callbacks.showModal(
|
|
result.success ? '¡Destrabado!' : '¡Atrapado!',
|
|
`Resultado del dado: <b style="color:${color}">${result.roll}</b> (Necesitabas ${result.target}+)<br>${msg}`
|
|
);
|
|
}
|
|
|
|
// Update UI (Refresh card to show movement unlocked or locked)
|
|
this.updateHeroCard(hero.id);
|
|
if (this.callbacks.refresh) this.callbacks.refresh(); // Or just let update handle it
|
|
};
|
|
}
|
|
card.appendChild(breakBtn);
|
|
}
|
|
|
|
// Inventory
|
|
const invBtn = document.createElement('button');
|
|
invBtn.textContent = '🎒 INVENTARIO';
|
|
Object.assign(invBtn.style, {
|
|
width: '100%', padding: '8px', marginTop: '8px', backgroundColor: '#5D4037',
|
|
color: '#fff', border: '1px solid #8B4513', borderRadius: '4px',
|
|
fontFamily: '"Cinzel", serif', fontSize: '12px', cursor: 'pointer'
|
|
});
|
|
invBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
if (this.callbacks.toggleInventory) this.callbacks.toggleInventory(hero);
|
|
};
|
|
card.appendChild(invBtn);
|
|
|
|
|
|
|
|
// Wizard Spells
|
|
if (hero.key === 'wizard') {
|
|
const spellsBtn = document.createElement('button');
|
|
spellsBtn.textContent = '🔮 HECHIZOS';
|
|
Object.assign(spellsBtn.style, {
|
|
width: '100%', padding: '8px', marginTop: '5px', backgroundColor: '#4b0082',
|
|
color: '#fff', border: '1px solid #8a2be2', borderRadius: '4px',
|
|
fontFamily: '"Cinzel", serif', cursor: 'pointer'
|
|
});
|
|
spellsBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
if (this.callbacks.toggleSpellBook) this.callbacks.toggleSpellBook(hero);
|
|
};
|
|
card.appendChild(spellsBtn);
|
|
}
|
|
|
|
card.dataset.heroId = hero.id;
|
|
return card;
|
|
}
|
|
|
|
createMonsterCard(monster) {
|
|
const card = document.createElement('div');
|
|
Object.assign(card.style, {
|
|
width: '180px', backgroundColor: 'rgba(40, 20, 20, 0.95)', border: '2px solid #8B0000',
|
|
borderRadius: '8px', padding: '10px', fontFamily: '"Cinzel", serif', color: '#fff'
|
|
});
|
|
|
|
// Portrait
|
|
const portrait = document.createElement('div');
|
|
Object.assign(portrait.style, {
|
|
width: '100px', height: '100px', borderRadius: '50%', overflow: 'hidden',
|
|
border: '2px solid #8B0000', marginBottom: '8px', marginLeft: 'auto', marginRight: 'auto',
|
|
backgroundColor: '#000', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|
});
|
|
const img = document.createElement('img');
|
|
img.src = `/assets/images/dungeon1/tokens/enemies/${monster.key}.png?v=2`;
|
|
Object.assign(img.style, { width: '100%', height: '100%', objectFit: 'cover' });
|
|
img.onerror = () => { portrait.innerHTML = `<div style="color: #8B0000; font-size: 48px;">👹</div>`; };
|
|
portrait.appendChild(img);
|
|
card.appendChild(portrait);
|
|
|
|
// Name
|
|
const name = document.createElement('div');
|
|
name.textContent = monster.name;
|
|
Object.assign(name.style, {
|
|
fontSize: '16px', fontWeight: 'bold', color: '#FF4444', textAlign: 'center', marginBottom: '8px', textTransform: 'uppercase'
|
|
});
|
|
card.appendChild(name);
|
|
|
|
// Stats
|
|
const statsGrid = document.createElement('div');
|
|
Object.assign(statsGrid.style, { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '4px', fontSize: '12px' });
|
|
const stats = [
|
|
{ label: 'H.C', value: monster.stats.ws || 0 },
|
|
{ label: 'Fuer', value: monster.stats.str || 0 },
|
|
{ label: 'Res', value: monster.stats.toughness || 0 },
|
|
{ label: 'Her', value: `${monster.currentWounds || monster.stats.wounds}/${monster.stats.wounds}` },
|
|
{ label: 'Ini', value: monster.stats.initiative || 0 },
|
|
{ label: 'Ata', value: monster.stats.attacks || 0 }
|
|
];
|
|
|
|
stats.forEach(stat => {
|
|
const el = document.createElement('div');
|
|
Object.assign(el.style, { backgroundColor: 'rgba(0, 0, 0, 0.5)', padding: '3px 5px', borderRadius: '3px', display: 'flex', justifyContent: 'space-between' });
|
|
const l = document.createElement('span'); l.style.color = '#AAA'; l.textContent = stat.label + ':';
|
|
const v = document.createElement('span'); v.style.color = '#FFF'; v.textContent = stat.value; v.style.fontWeight = 'bold';
|
|
el.appendChild(l); el.appendChild(v);
|
|
statsGrid.appendChild(el);
|
|
});
|
|
card.appendChild(statsGrid);
|
|
card.dataset.monsterId = monster.id;
|
|
return card;
|
|
}
|
|
|
|
showMonsterCard(monster) {
|
|
this.hideMonsterCard();
|
|
|
|
// Create a sub-container for monster card + button
|
|
this.monsterContainer = document.createElement('div');
|
|
Object.assign(this.monsterContainer.style, {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '8px'
|
|
});
|
|
|
|
this.currentMonsterCard = this.createMonsterCard(monster);
|
|
this.monsterContainer.appendChild(this.currentMonsterCard);
|
|
|
|
this.attackButton = document.createElement('button');
|
|
this.attackButton.textContent = '⚔️ ATACAR';
|
|
Object.assign(this.attackButton.style, {
|
|
width: '180px', padding: '12px', backgroundColor: '#8B0000', color: '#fff',
|
|
border: '2px solid #FF4444', borderRadius: '8px', fontFamily: '"Cinzel", serif',
|
|
fontSize: '16px', fontWeight: 'bold', cursor: 'pointer', transition: 'all 0.2s'
|
|
});
|
|
|
|
this.attackButton.onmouseenter = () => { this.attackButton.style.backgroundColor = '#FF0000'; this.attackButton.style.transform = 'scale(1.05)'; };
|
|
this.attackButton.onmouseleave = () => { this.attackButton.style.backgroundColor = '#8B0000'; this.attackButton.style.transform = 'scale(1)'; };
|
|
|
|
this.attackButton.onclick = () => {
|
|
if (this.game.performHeroAttack) {
|
|
const result = this.game.performHeroAttack(monster.id);
|
|
if (result && result.success) {
|
|
this.hideMonsterCard();
|
|
if (this.game.selectedMonster) {
|
|
if (this.game.onEntitySelect) this.game.onEntitySelect(this.game.selectedMonster.id, false);
|
|
this.game.selectedMonster = null;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
this.monsterContainer.appendChild(this.attackButton);
|
|
this.cardsContainer.appendChild(this.monsterContainer);
|
|
}
|
|
|
|
showRangedAttackUI(monster) {
|
|
this.showMonsterCard(monster); // Creates button as "ATACAR"
|
|
|
|
if (this.attackButton) {
|
|
this.attackButton.textContent = '🏹 DISPARAR';
|
|
this.attackButton.style.backgroundColor = '#2E8B57';
|
|
this.attackButton.style.border = '2px solid #32CD32';
|
|
|
|
this.attackButton.onclick = () => {
|
|
const result = this.game.performRangedAttack(monster.id);
|
|
if (result && result.success) {
|
|
this.game.cancelTargeting();
|
|
this.hideMonsterCard();
|
|
}
|
|
};
|
|
|
|
this.attackButton.onmouseenter = () => { this.attackButton.style.backgroundColor = '#3CB371'; this.attackButton.style.transform = 'scale(1.05)'; };
|
|
this.attackButton.onmouseleave = () => { this.attackButton.style.backgroundColor = '#2E8B57'; this.attackButton.style.transform = 'scale(1)'; };
|
|
}
|
|
}
|
|
|
|
hideMonsterCard() {
|
|
if (this.monsterContainer && this.monsterContainer.parentNode) {
|
|
this.cardsContainer.removeChild(this.monsterContainer);
|
|
}
|
|
this.monsterContainer = null;
|
|
this.currentMonsterCard = null;
|
|
this.attackButton = null;
|
|
}
|
|
}
|