I can't believe I made this either...
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1731 lines
68 KiB

// Simple audio utility
function playSound(soundFile) {
try {
const audio = new Audio(`assets/sounds/${soundFile}`);
audio.volume = 0.3;
audio.play().catch(e => {}); // Silently fail if no audio
} catch (e) {
// Silently fail if audio not available
}
}
export function showDamageNumber(damage, target, isPlayer = false) {
const targetElement = isPlayer ?
document.querySelector('.player-battle-zone') :
document.querySelector('.enemy-battle-zone');
if (!targetElement) return;
const damageNumber = document.createElement('div');
damageNumber.className = 'damage-number';
damageNumber.textContent = damage;
const rect = targetElement.getBoundingClientRect();
damageNumber.style.left = `${rect.left + rect.width / 2}px`;
damageNumber.style.top = `${rect.top + rect.height / 2}px`;
document.body.appendChild(damageNumber);
requestAnimationFrame(() => {
damageNumber.classList.add('damage-number-animate');
});
setTimeout(() => {
if (damageNumber.parentNode) {
damageNumber.parentNode.removeChild(damageNumber);
}
}, 1000);
}
export async function renderBattle(root) {
const app = root.app;
const p = root.player, e = root.enemy;
const { ENEMIES } = await import("../data/enemies.js");
const enemyData = ENEMIES[e.id];
const backgroundImage = enemyData?.background || null;
const intentInfo = {
attack: { emoji: '', text: `Will attack for ${e.intent.value} damage`, color: 'danger' },
block: { emoji: '', text: `Will gain ${e.intent.value} block`, color: 'info' },
debuff: { emoji: '', text: 'Will apply a debuff', color: 'warning' }
}[e.intent.type] || { emoji: '', text: 'Unknown intent', color: 'neutral' };
app.innerHTML = `
<div class="battle-scene">
<div class="battle-background">
<div class="bg-particles"></div>
<div class="bg-glow"></div>
</div>
<div class="battle-arena" ${backgroundImage ? `style="background-image: url('${backgroundImage}'); background-size: cover; background-position: center; background-repeat: no-repeat;"` : ''}>
<div class="enemy-battle-zone">
<div class="enemy-container">
<div class="enemy-character">
<div class="enemy-sprite">
<div class="enemy-avatar">${getEnemyArt(e.id, ENEMIES)}</div>
<div class="enemy-shadow"></div>
${e.block > 0 ? `<div class="shield-effect"><img src="assets/card-art/shield.png" alt="Shield" class="shield-effect-img"></div>` : ''}
${e.weak > 0 ? `<div class="debuff-effect"><img src="assets/card-art/heart_damaged.png" alt="Weak" class="debuff-effect-img"></div>` : ''}
</div>
</div>
<div class="enemy-ui-panel">
<div class="enemy-nameplate">
<h2 class="enemy-title">${e.name}</h2>
<div class="enemy-level">${getEnemyType(e.id)}</div>
</div>
<div class="enemy-health-section">
<div class="health-bar-container">
<div class="health-bar enemy-health">
<div class="health-fill" style="width: ${(e.hp / e.maxHp) * 100}%"></div>
<div class="health-text">${e.hp} / ${e.maxHp}</div>
<div class="health-glow"></div>
</div>
</div>
${e.block > 0 ? `
<div class="status-effect block-status">
<img src="assets/card-art/shield.png" alt="Block" class="status-icon-img">
<span class="status-value">${e.block}</span>
<span class="status-label">Block</span>
</div>
` : ''}
</div>
<div class="intent-panel intent-${intentInfo.color}">
<div class="intent-header">
<span class="intent-label">Next Action</span>
</div>
<div class="intent-content">
<div class="intent-icon-large">${intentInfo.emoji}</div>
<div class="intent-description">${intentInfo.text}</div>
</div>
</div>
</div>
</div>
</div>
<div class="player-battle-zone">
<div class="player-container">
<div class="player-character">
<div class="player-sprite">
<div class="player-avatar">
<img src="assets/prime.webp" alt="Prime" class="player-avatar-img" />
</div>
<div class="player-shadow"></div>
${p.block > 0 ? `<div class="shield-effect"><img src="assets/card-art/shield.png" alt="Shield" class="shield-effect-img"></div>` : ''}
${p.weak > 0 ? `<div class="debuff-effect"><img src="assets/card-art/heart_damaged.png" alt="Weak" class="debuff-effect-img"></div>` : ''}
</div>
</div>
<div class="player-ui-panel">
<div class="player-nameplate">
<h2 class="player-title">ThePrimeagen</h2>
<div class="player-level">PLAYER</div>
</div>
<div class="player-health-section">
<div class="health-bar-container">
<div class="health-bar player-health">
<div class="health-fill" style="width: ${(p.hp / p.maxHp) * 100}%"></div>
<div class="health-text">${p.hp} / ${p.maxHp}</div>
<div class="health-glow"></div>
</div>
</div>
${p.block > 0 ? `
<div class="status-effect block-status">
<img src="assets/card-art/shield.png" alt="Block" class="status-icon-img">
<span class="status-value">${p.block}</span>
<span class="status-label">Block</span>
</div>
` : ''}
${p.weak > 0 ? `
<div class="status-effect weak-status">
<img src="assets/card-art/heart_damaged.png" alt="Weak" class="status-icon-img">
<span class="status-value">${p.weak}</span>
<span class="status-label">Weak</span>
</div>
` : ''}
</div>
<div class="player-energy-section">
<div class="energy-display">
<span class="energy-label">⚡</span>
<div class="energy-orbs">
${Array.from({ length: p.maxEnergy }, (_, i) =>
`<div class="energy-orb ${i < p.energy ? 'active' : 'inactive'}"></div>`
).join('')}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="battle-action-zone">
<div class="hand-area">
<div class="hand-header">
<div class="deck-counters">
</div>
</div>
<div class="cards-battlefield">
${p.hand.length === 0 ?
'<div class="no-cards-message">🎴 No cards in hand - End turn to draw new cards</div>' :
p.hand.map((card, i) => {
const canPlay = p.energy >= card.cost;
const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power';
return `
<div class="battle-card ${cardType} ${!canPlay ? 'unplayable' : 'playable'}" data-play="${i}">
<div class="card-glow"></div>
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${card.name}</div>
<div class="card-cost-orb ${!canPlay ? 'insufficient' : ''}">${card.cost}</div>
</div>
<div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div>
<div class="card-type-badge ${cardType}">${card.type}</div>
</div>
<div class="card-description-box">
<div class="card-text">${card.text}</div>
</div>
</div>
${!canPlay ? `<div class="card-disabled-overlay"><span>Need ${card.cost} energy</span></div>` : ''}
</div>
`;
}).join('')
}
</div>
<div class="hand-controls">
<button class="end-turn-btn" data-action="end">
<span class="end-turn-text">End Turn</span>
<span class="end-turn-hotkey">E</span>
</button>
</div>
</div>
</div>
<div class="fight-log-panel">
<div class="fight-log-header">
<span class="fight-log-title">Combat Log</span>
</div>
<div class="fight-log-content" id="fight-log-content">
${root.logs.slice(-20).map(log => `<div class="log-entry">${log}</div>`).join('')}
</div>
</div>
</div>
`;
app.querySelectorAll("[data-play]").forEach(btn => {
btn.addEventListener("mouseenter", () => {
if (btn.classList.contains('playable')) {
playSound('swipe.mp3');
root.selectedCardIndex = null;
updateCardSelection(root);
}
});
btn.addEventListener("click", () => {
const index = parseInt(btn.dataset.play, 10);
const card = p.hand[index];
if (p.energy >= card.cost) {
root.play(index);
// Clear selection when card is played via mouse
root.selectedCardIndex = null;
updateCardSelection(root);
}
});
});
const endTurnBtn = app.querySelector("[data-action='end']");
if (endTurnBtn) {
endTurnBtn.addEventListener("click", () => {
try {
root.end();
} catch (error) {
console.error("Error ending turn:", error);
}
});
}
// Initialize card selection state if not exists
if (!root.selectedCardIndex) {
root.selectedCardIndex = null;
}
window.onkeydown = (e) => {
if (e.key.toLowerCase() === "e") {
try {
root.end();
} catch (error) {
console.error("Error ending turn via keyboard:", error);
}
}
const n = parseInt(e.key, 10);
if (n >= 1 && n <= p.hand.length) {
const cardIndex = n - 1;
const card = p.hand[cardIndex];
if (root.selectedCardIndex === cardIndex) {
// Second press of same key - play the card
if (p.energy >= card.cost) {
root.play(cardIndex);
root.selectedCardIndex = null; // Clear selection
updateCardSelection(root);
}
} else {
// First press or different key - select the card
root.selectedCardIndex = cardIndex;
updateCardSelection(root);
playSound('swipe.mp3'); // Play swipe sound on keyboard selection
}
}
};
// Auto-scroll fight log to bottom
const logContent = document.getElementById('fight-log-content');
if (logContent) {
logContent.scrollTop = logContent.scrollHeight;
}
// Apply initial card selection visual state
updateCardSelection(root);
}
export async function renderMap(root) {
const { CARDS } = await import("../data/cards.js");
const { ENEMIES } = await import("../data/enemies.js");
const m = root.map;
const currentId = root.nodeId;
const currentNode = m.nodes.find(n => n.id === currentId);
const nextIds = currentNode ? currentNode.next : [];
const getNodeEmoji = (kind) => {
const emojis = {
start: '<img src="assets/card-art/staff.png" alt="Start" class="node-icon-img">',
battle: '<img src="assets/card-art/crossed_swords.png" alt="Battle" class="node-icon-img">',
elite: '<img src="assets/card-art/crown.png" alt="Battle" class="node-icon-img">',
boss: '<img src="assets/card-art/skull.png" alt="Boss" class="node-icon-img">',
rest: '<img src="assets/card-art/potion_heal.png" alt="Rest" class="node-icon-img">',
shop: '<img src="assets/card-art/diamond.png" alt="Shop" class="node-icon-img">',
event: '<img src="assets/card-art/crystal_cluster.png" alt="Event" class="node-icon-img">'
};
return emojis[kind] || '❓';
};
const getNodeDescription = (node) => {
switch (node.kind) {
case 'start':
return '<strong>Starting Point</strong>\nBegin your journey up ThePrimeagen Spire';
case 'battle':
const enemy = ENEMIES[node.enemy];
return `<strong>Battle</strong>\nFight: ${enemy?.name || 'Unknown Enemy'}\nHP: ${enemy?.maxHp || '?'}`;
case 'elite':
const elite = ENEMIES[node.enemy];
return `<strong>Elite Battle</strong>\nFight: ${elite?.name || 'Unknown Elite'}\nHP: ${elite?.maxHp || '?'}\nTough enemy with better rewards`;
case 'boss':
const boss = ENEMIES[node.enemy];
return `<strong>Boss Battle</strong>\nFight: ${boss?.name || 'Unknown Boss'}\nHP: ${boss?.maxHp || '?'}\nFinal challenge of the act`;
case 'rest':
return '<strong>Rest Site</strong>\nHeal up to 30% max HP\nor upgrade a card';
case 'shop':
return '<strong>Shop</strong>\nSpend your hard-earned gold';
case 'event':
return '<strong>Random Event</strong>\nBirthday-themed encounter\nUnknown outcome\nPotential rewards or challenges';
default:
return '<strong>Unknown</strong>\nMysterious node';
}
};
const getNodeTooltipData = (node) => {
const description = getNodeDescription(node);
let avatarPath = null;
if (['battle', 'elite', 'boss'].includes(node.kind) && node.enemy) {
const enemy = ENEMIES[node.enemy];
if (enemy?.avatar) {
avatarPath = enemy.avatar;
}
}
return { description, avatarPath };
};
root.app.innerHTML = `
<div class="map-screen">
<div class="map-header-section">
<div class="game-logo">
<svg width="600" height="240" viewBox="0 0 600 240" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ffd700;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ff8c00;stop-opacity:1" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1"/>
<feOffset dx="1" dy="1" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<text x="300" y="80" text-anchor="middle" font-family="'Kreon', serif" font-size="55" font-weight="700" fill="url(#textGradient)" filter="url(#glow)">
ThePrimeagen
</text>
<text x="300" y="170" text-anchor="middle" font-family="'Kreon', serif" font-size="85" font-weight="700" fill="url(#textGradient)" filter="url(#shadow) url(#glow)">
Spire
</text>
</svg>
</div>
</div>
<div class="player-status">
<div class="status-item">
<img src="assets/card-art/heart.png" alt="Health" class="status-icon-img">
<div class="hp-bar player-hp" style="width: 80px;">
<div class="hp-fill" style="width: ${(root.player.hp / root.player.maxHp) * 100}%"></div>
<span class="hp-text">${root.player.hp}/${root.player.maxHp}</span>
</div>
</div>
<div class="status-item">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="status-icon-img">
<span class="status-value">${root.player.gold || 0}</span>
</div>
<div class="status-item">
<img src="assets/card-art/book.png" alt="Deck" class="status-icon-img">
<span class="status-value">${root.player.deck.length} cards</span>
</div>
${root.relicStates.length > 0 ? `
<div class="status-item relics-status">
<img src="assets/card-art/runestone.png" alt="Relics" class="status-icon-img">
<div class="relics-inline">
${root.relicStates.map(r => `
<div class="relic-inline" title="${getRelicText(r.id)}">
${getRelicEmoji(r.id)}
</div>
`).join('')}
</div>
</div>
` : ''}
<button class="btn-reset-status" data-reset>
Start New Run
</button>
</div>
<div class="main-content">
<div class="map-section">
<div class="welcome-panel">
<div class="birthday-message">
<h2>Happy Birthday Prime!</h2>
<p>With coffee in hand and code on your side,<br>
ThePrimeagen Spire’s a treacherous ride. <br>
Gremlins await and errors conspire, <br>
But cake lies ahead at the top of the Spire. </p>
</div>
<div class="map-instructions">
<h3>How to Navigate the Spire</h3>
<ul>
<li><strong>Click a node</strong> to climb the way</li>
<li><strong>Choose your battles</strong> night or day</li>
<li><strong>Rest at fires</strong>, heal or train</li>
<li><strong>Each new card</strong> will grow your gain. </li>
<li><strong>At the summit</strong> face the fight</li>
<li><strong>Defeat the boss</strong>, win the night</li>
</ul>
<div class="birthday-wish">
<p><em>May your code be bug-free and your coffee stay hot,
May this birthday bring joy in each moment you’ve got. </em></p>
</div>
</div>
</div>
<div class="spire-map">
<div class="map-legend-overlay">
<div class="legend-title">Legend</div>
<div class="legend-item"><img src="assets/card-art/potion_heal.png" alt="Rest" class="legend-icon-img"> Rest</div>
<div class="legend-item"><img src="assets/card-art/crossed_swords.png" alt="Battle" class="legend-icon-img"> Enemy</div>
<div class="legend-item"><img src="assets/card-art/crown.png" alt="Battle" class="legend-icon-img"> Elite</div>
<div class="legend-item"><img src="assets/card-art/skull.png" alt="Battle" class="legend-icon-img"> Boss</div>
<div class="legend-item"><img src="assets/card-art/crystal_cluster.png" alt="Event" class="legend-icon-img"> Events</div>
<div class="legend-item"><img src="assets/card-art/diamond.png" alt="Shop" class="legend-icon-img"> Shop</div>
</div>
<svg class="spire-paths" viewBox="0 0 1000 800" preserveAspectRatio="xMidYMid meet">
${(() => {
const nodePositions = {
'n1': { x: 500, y: 720 }, // Start at bottom center - moved up slightly
'n2': { x: 350, y: 650 }, // Battle - left branch - moved right and up
'n3': { x: 650, y: 650 }, // Event - right branch - moved left and up
'n4': { x: 500, y: 540 }, // Battle - converge - moved up slightly
'n5': { x: 350, y: 400 }, // Rest - left
'n6': { x: 650, y: 400 }, // Shop - right
'n7': { x: 500, y: 300 }, // Battle - converge
'n8': { x: 500, y: 130 }, // Elite
'n9': { x: 500, y: 70 }, // Rest
'n10': { x: 500, y: 20 } // Boss at top
};
return m.nodes.map(node => {
if (!node.next || node.next.length === 0) return '';
return node.next.map(nextId => {
const fromPos = nodePositions[node.id];
const toPos = nodePositions[nextId];
if (!fromPos || !toPos) return '';
const isActivePath = (node.id === currentId && nextIds.includes(nextId)) ||
(parseInt(nextId.replace('n', '')) <= parseInt(currentId.replace('n', '')));
return `<line x1="${fromPos.x}" y1="${fromPos.y}" x2="${toPos.x}" y2="${toPos.y}"
class="spire-path ${isActivePath ? 'active' : ''}"
stroke="${isActivePath ? '#8B7355' : '#4A3A2A'}"
stroke-width="2"
stroke-dasharray="8,4"
opacity="${isActivePath ? '1' : '0.6'}"/>`;
}).join('');
}).join('');
})()}
</svg>
<div class="spire-nodes">
${(() => {
const nodePositions = {
'n1': { x: 500, y: 720 },
'n2': { x: 360, y: 650 },
'n3': { x: 630, y: 650 },
'n4': { x: 500, y: 540 },
'n5': { x: 360, y: 400 },
'n6': { x: 630, y: 400 },
'n7': { x: 500, y: 300 },
'n8': { x: 500, y: 210 },
'n9': { x: 500, y: 120 },
'n10': { x: 500, y: 40 }
};
return m.nodes.map(n => {
const isNext = nextIds.includes(n.id);
const isCurrent = n.id === currentId;
const isCompleted = root.completedNodes.includes(n.id);
const locked = (!isNext && !isCurrent && !isCompleted);
const pos = nodePositions[n.id];
if (!pos) return '';
const leftPercent = (pos.x / 1000) * 100;
const topPercent = (pos.y / 800) * 100;
const tooltipData = getNodeTooltipData(n);
return `
<div class="spire-node ${isCurrent ? 'current' : ''} ${isNext ? 'available' : ''} ${isCompleted ? 'completed' : ''} ${locked ? 'locked' : ''}"
style="left: ${leftPercent}%; top: ${topPercent}%; transform: translate(-50%, -50%);"
data-node="${isNext ? n.id : ''}"
data-tooltip="${tooltipData.description.replace(/\n/g, '<br>')}"
data-avatar="${tooltipData.avatarPath || ''}"
onmouseenter="showTooltip(event)"
onmouseleave="hideTooltip()">
<div class="node-background ${n.kind}"></div>
<div class="node-content">
<div class="node-icon">${getNodeEmoji(n.kind)}</div>
</div>
${isCurrent ? '<div class="current-indicator">★</div>' : ''}
</div>
`;
}).join('');
})()}
</div>
</div>
</div>
<div class="deck-stack-container">
<div class="deck-stack-header">
<span class="deck-count">Your deck</span>
</div>
<div class="deck-stack" data-tooltip="Hover to view deck">
${Object.entries(
root.player.deck.reduce((acc, cardId) => {
acc[cardId] = (acc[cardId] || 0) + 1;
return acc;
}, {})
).map(([cardId, count], index) => {
const card = CARDS[cardId];
if (!card) return '';
const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power';
return `
<div class="deck-stack-card ${cardType}" style="--card-index: ${index}">
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${card.name}</div>
<div class="card-cost-orb">${card.cost}</div>
</div>
<div class="card-art">${getCardArt(cardId)}</div>
<div class="card-description-box">
<div class="card-text">${card.text}</div>
</div>
${count > 1 ? `<div class="card-count-badge">×${count}</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
</div>
</div>
<div id="custom-tooltip" class="custom-tooltip"></div>
</div>
`;
root.app.querySelectorAll("[data-node]").forEach(el => {
if (!el.dataset.node) return;
el.addEventListener("click", () => root.go(el.dataset.node));
});
window.showTooltip = function (event) {
const tooltip = document.getElementById('custom-tooltip');
const node = event.target.closest('.spire-node');
const content = node.dataset.tooltip;
const avatarPath = node.dataset.avatar;
let tooltipHTML = '';
if (avatarPath) {
tooltipHTML = `
<div class="tooltip-with-avatar">
<div class="tooltip-avatar">
<img src="${avatarPath}" alt="Enemy Avatar" class="tooltip-avatar-img"
onerror="this.style.display='none';">
</div>
<div class="tooltip-content">${content}</div>
</div>
`;
} else {
tooltipHTML = content;
}
tooltip.innerHTML = tooltipHTML;
tooltip.style.display = 'block';
const rect = node.getBoundingClientRect();
tooltip.style.left = (rect.right + 15) + 'px';
tooltip.style.top = (rect.top + rect.height / 2 - tooltip.offsetHeight / 2) + 'px';
const tooltipRect = tooltip.getBoundingClientRect();
if (tooltipRect.right > window.innerWidth) {
tooltip.style.left = (rect.left - tooltip.offsetWidth - 15) + 'px';
}
if (tooltipRect.top < 0) {
tooltip.style.top = '10px';
}
if (tooltipRect.bottom > window.innerHeight) {
tooltip.style.top = (window.innerHeight - tooltip.offsetHeight - 10) + 'px';
}
};
window.hideTooltip = function () {
const tooltip = document.getElementById('custom-tooltip');
tooltip.style.display = 'none';
};
const resetBtn = root.app.querySelector("[data-reset]");
resetBtn.addEventListener("click", () => {
root.clearSave();
root.reset();
});
}
export function renderReward(root, choices) {
root.app.innerHTML = `
<div class="reward-screen">
<h1>Choose a Card</h1>
<div class="reward-cards-container">
${choices.map((c, idx) => {
const cardType = c.type === 'attack' ? 'attack' : c.type === 'skill' ? 'skill' : 'power';
return `
<div class="reward-card-wrapper" data-pick="${idx}">
<div class="battle-card ${cardType} reward-card">
<div class="card-glow"></div>
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${c.name}</div>
<div class="card-cost-orb">${c.cost}</div>
</div>
<div class="card-artwork">
<div class="card-art-icon">${getCardArt(c.id)}</div>
<div class="card-type-badge ${cardType}">${c.type}</div>
</div>
<div class="card-description-box">
<div class="card-text">${c.text}</div>
</div>
<div class="card-select-hint">Click to select</div>
</div>
</div>
</div>
`;
}).join("")}
</div>
<div class="reward-actions">
<button class="btn secondary skip-btn" data-skip>Skip Reward</button>
</div>
</div>
`;
root.app.querySelectorAll("[data-pick]").forEach(btn => {
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.pick, 10);
root.takeReward(idx);
});
});
root.app.querySelector("[data-skip]").addEventListener("click", () => root.skipReward());
}
export function renderRest(root) {
root.app.innerHTML = `
<div class="rest-screen">
<div class="rest-header">
<h1>Rest and Recover</h1>
<p>Take a moment to restore your strength</p>
</div>
<div class="rest-options">
<button class="rest-option" data-act="heal">
<div class="rest-icon">
<img src="assets/card-art/heart.png" alt="Heal" class="rest-icon-img">
</div>
<div class="rest-content">
<h3>Rest and Heal</h3>
<p>Restore 20% of your maximum health</p>
</div>
</button>
<button class="rest-option" data-act="upgrade">
<div class="rest-icon">
<img src="assets/card-art/scroll.png" alt="Upgrade" class="rest-icon-img">
</div>
<div class="rest-content">
<h3>Upgrade a Card</h3>
<p>Permanently improve one of your cards</p>
</div>
</button>
</div>
</div>
`;
root.app.querySelector("[data-act='heal']").addEventListener("click", () => {
const heal = Math.floor(root.player.maxHp * 0.2);
root.player.hp = Math.min(root.player.maxHp, root.player.hp + heal);
root.log(`Rested: +${heal} HP`);
root.afterNode();
});
root.app.querySelector("[data-act='upgrade']").addEventListener("click", () => {
renderUpgrade(root);
});
}
export function renderUpgrade(root) {
import("../data/cards.js").then(({ CARDS }) => {
const upgradableCards = root.player.deck
.map((cardId, index) => ({ cardId, index }))
.filter(({ cardId }) => {
const card = CARDS[cardId];
return card?.upgrades && !cardId.endsWith('+');
})
.slice(0, 3); // Show max 3 options
if (upgradableCards.length === 0) {
root.log("No cards can be upgraded.");
root.afterNode();
return;
}
root.app.innerHTML = `
<div class="upgrade-screen">
<div class="upgrade-header">
<h1>⬆ Upgrade a Card</h1>
<p>Select a card from your deck to permanently improve it</p>
</div>
<div class="upgrade-options">
${upgradableCards.map(({ cardId, index }) => {
const card = CARDS[cardId];
const upgradedCard = CARDS[card.upgrades];
if (!upgradedCard) {
return ''; // Skip if no upgrade found
}
return `
<div class="upgrade-option" data-upgrade="${index}">
<div class="upgrade-preview">
<div class="upgrade-action-header">
<h3>🔧 Upgrade ${card.name}</h3>
<p>Click to permanently improve this card</p>
</div>
<div class="upgrade-comparison">
<div class="upgrade-card-container">
<div class="upgrade-card-label">Current</div>
<div class="battle-card ${card.type} playable upgrade-card-before">
<div class="card-glow"></div>
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${card.name}</div>
<div class="card-cost-orb">${card.cost}</div>
</div>
<div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div>
<div class="card-type-badge ${card.type}">${card.type}</div>
</div>
<div class="card-description-box">
<div class="card-text">${card.text}</div>
</div>
</div>
</div>
</div>
<div class="upgrade-card-container">
<div class="upgrade-card-label">Upgraded</div>
<div class="battle-card ${upgradedCard.type} playable upgrade-card-after">
<div class="card-glow"></div>
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${upgradedCard.name}</div>
<div class="card-cost-orb">${upgradedCard.cost}</div>
</div>
<div class="card-artwork">
<div class="card-art-icon">${getCardArt(upgradedCard.id)}</div>
<div class="card-type-badge ${upgradedCard.type}">${upgradedCard.type}</div>
</div>
<div class="card-description-box">
<div class="card-text">${upgradedCard.text}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}).join("")}
</div>
<div class="upgrade-actions">
<button class="upgrade-skip" data-skip>Skip Upgrade</button>
</div>
</div>
`;
root.app.querySelectorAll("[data-upgrade]").forEach(btn => {
btn.addEventListener("click", () => {
const deckIndex = parseInt(btn.dataset.upgrade, 10);
const oldCardId = root.player.deck[deckIndex];
const newCardId = CARDS[oldCardId].upgrades;
root.player.deck[deckIndex] = newCardId;
root.log(`Upgraded ${CARDS[oldCardId].name}${CARDS[newCardId].name}`);
root.afterNode();
});
});
root.app.querySelector("[data-skip]").addEventListener("click", () => root.afterNode());
});
}
export function renderShop(root) {
import("../data/cards.js").then(({ CARDS, CARD_POOL }) => {
import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => {
const availableCards = CARD_POOL.filter(cardId => {
const ownedCount = root.player.deck.filter(deckCardId => deckCardId === cardId).length;
return ownedCount < 3;
});
const cardsToShow = availableCards.length >= 3 ? availableCards : CARD_POOL;
const shopCards = shuffle(cardsToShow.slice()).slice(0, 3).map(id => CARDS[id]);
const ownedRelicIds = root.relicStates.map(r => r.id);
const availableRelics = START_RELIC_CHOICES.filter(id => !ownedRelicIds.includes(id));
const shopRelic = availableRelics.length > 0 ? RELICS[availableRelics[0]] : null;
root.app.innerHTML = `
<div class="shop-screen">
<div class="shop-header">
<h1>Merchant's Shop</h1>
<p>Spend your hard-earned gold on powerful upgrades</p>
<div class="player-gold">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="gold-icon">
<span class="gold-amount">${root.player.gold || 100}</span>
</div>
</div>
<div class="shop-inventory">
<div class="shop-section">
<div class="shop-section-header">
<h2>Cards for Sale</h2>
<p>50 gold each</p>
</div>
<div class="shop-cards">
${shopCards.map((card, idx) => {
const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power';
const canAfford = (root.player.gold || 100) >= 50;
const ownedCount = root.player.deck.filter(deckCardId => deckCardId === card.id).length;
return `
<div class="shop-card-container">
<div class="battle-card ${cardType} ${canAfford ? 'playable' : 'unplayable'} shop-card" data-buy-card="${idx}">
<div class="card-glow"></div>
<div class="card-frame">
<div class="card-header-row">
<div class="card-title">${card.name}</div>
<div class="card-cost-orb">${card.cost}</div>
</div>
<div class="card-artwork">
<div class="card-art-icon">${getCardArt(card.id)}</div>
<div class="card-type-badge ${cardType}">${card.type}</div>
</div>
<div class="card-description-box">
<div class="card-text">${card.text}</div>
</div>
</div>
<div class="shop-card-price">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="price-icon">
<span>50</span>
</div>
${ownedCount > 0 ? `<div class="card-owned-indicator">Owned: ${ownedCount}</div>` : ''}
${!canAfford ? `<div class="card-disabled-overlay"><span>Need 50 gold</span></div>` : ''}
</div>
</div>
`;
}).join("")}
</div>
</div>
${shopRelic ? `
<div class="shop-section">
<div class="shop-section-header">
<h2>Mystical Relic</h2>
<p>100 gold</p>
</div>
<div class="shop-relics">
<div class="shop-relic-container">
<div class="shop-relic ${(root.player.gold || 100) >= 100 ? 'affordable' : 'unaffordable'}" data-buy-relic>
<div class="relic-icon">${getRelicEmoji(shopRelic.id)}</div>
<div class="relic-info">
<h3>${shopRelic.name}</h3>
<p>${shopRelic.text}</p>
</div>
<div class="shop-relic-price">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="price-icon">
<span>100</span>
</div>
${(root.player.gold || 100) < 100 ? `<div class="relic-disabled-overlay"><span>Need 100 gold</span></div>` : ''}
</div>
</div>
</div>
</div>
` : ''}
</div>
<div class="shop-actions">
<button class="shop-leave-btn" data-leave>
<img src="assets/card-art/exit.png" alt="Leave" class="leave-icon">
<span>Leave Shop</span>
</button>
</div>
</div>
`;
if (!root.player.gold) root.player.gold = 100;
root.app.querySelectorAll("[data-buy-card]").forEach(btn => {
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.buyCard, 10);
const card = shopCards[idx];
if (root.player.gold >= 50) {
root.player.gold -= 50;
root.player.deck.push(card.id);
root.log(`Bought ${card.name} for 50 gold.`);
btn.disabled = true;
btn.textContent = "SOLD";
// Update gold display
const goldDisplay = root.app.querySelector('.gold-amount');
if (goldDisplay) {
goldDisplay.textContent = root.player.gold;
}
// Update affordability of remaining items
updateShopAffordability(root);
} else {
root.log("Not enough gold!");
}
});
});
if (shopRelic) {
root.app.querySelector("[data-buy-relic]").addEventListener("click", () => {
if (root.player.gold >= 100) {
root.player.gold -= 100;
root.log(`Bought ${shopRelic.name} for 100 gold.`);
import("../engine/battle.js").then(({ attachRelics }) => {
const currentRelicIds = root.relicStates.map(r => r.id);
const newRelicIds = [...currentRelicIds, shopRelic.id];
attachRelics(root, newRelicIds);
});
root.app.querySelector("[data-buy-relic]").disabled = true;
root.app.querySelector("[data-buy-relic]").textContent = "SOLD";
// Update gold display
const goldDisplay = root.app.querySelector('.gold-amount');
if (goldDisplay) {
goldDisplay.textContent = root.player.gold;
}
// Update affordability of remaining items
updateShopAffordability(root);
} else {
root.log("Not enough gold!");
}
});
}
root.app.querySelector("[data-leave]").addEventListener("click", () => root.afterNode());
});
});
}
function updateCardSelection(root) {
// Remove selection from all cards
root.app.querySelectorAll('.battle-card').forEach(card => {
card.classList.remove('card-selected');
});
// Add selection to currently selected card
if (root.selectedCardIndex !== null) {
const selectedCard = root.app.querySelector(`[data-play="${root.selectedCardIndex}"]`);
if (selectedCard) {
selectedCard.classList.add('card-selected');
}
}
}
function updateShopAffordability(root) {
// Update card affordability
root.app.querySelectorAll("[data-buy-card]").forEach(btn => {
if (!btn.disabled) {
const cardContainer = btn.closest('.shop-card-container');
const overlay = cardContainer.querySelector('.card-disabled-overlay');
if (root.player.gold < 50) {
btn.classList.remove('playable');
btn.classList.add('unplayable');
if (!overlay) {
const newOverlay = document.createElement('div');
newOverlay.className = 'card-disabled-overlay';
newOverlay.innerHTML = '<span>Need 50 gold</span>';
cardContainer.appendChild(newOverlay);
}
} else {
btn.classList.remove('unplayable');
btn.classList.add('playable');
if (overlay) {
overlay.remove();
}
}
}
});
// Update relic affordability
const relicBtn = root.app.querySelector("[data-buy-relic]");
if (relicBtn && !relicBtn.disabled) {
const relicContainer = relicBtn.closest('.shop-relic-container');
const overlay = relicContainer.querySelector('.relic-disabled-overlay');
if (root.player.gold < 100) {
relicBtn.classList.remove('affordable');
relicBtn.classList.add('unaffordable');
if (!overlay) {
const newOverlay = document.createElement('div');
newOverlay.className = 'relic-disabled-overlay';
newOverlay.innerHTML = '<span>Need 100 gold</span>';
relicContainer.appendChild(newOverlay);
}
} else {
relicBtn.classList.remove('unaffordable');
relicBtn.classList.add('affordable');
if (overlay) {
overlay.remove();
}
}
}
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRelicEmoji(relicId) {
const relicArt = {
mech_kb: '<img src="assets/skill-art/Monk_29.png" alt="Kinesis" class="relic-skill-art">',
standing_desk: '<img src="assets/skill-art/Monk_30.png" alt="Motions" class="relic-skill-art">',
prime_hat: '<img src="assets/skill-art/Monk_31.png" alt="VS Code" class="relic-skill-art">',
coffee_thermos: '<img src="assets/skill-art/Monk_32.png" alt="Coffee Thermos" class="relic-skill-art">',
cpp_compiler: '<img src="assets/skill-art/Monk_33.png" alt="CPP Compiler" class="relic-skill-art">',
chat_mod_sword: '<img src="assets/skill-art/Monk_34.png" alt="Chat Mod Sword" class="relic-skill-art">'
};
return relicArt[relicId] || '💎';
}
function getRelicName(relicId) {
const names = {
mech_kb: 'Kinesis',
standing_desk: 'Motions',
prime_hat: 'VS Code',
coffee_thermos: 'Coffee Thermos',
cpp_compiler: 'C++ Compiler',
chat_mod_sword: 'Chat Mod Sword'
};
return names[relicId] || relicId;
}
function getRelicText(relicId) {
const texts = {
mech_kb: '+1 card draw each turn.',
standing_desk: '+10 Max HP.',
prime_hat: '-10% damage taken.',
coffee_thermos: 'Start each fight with Coffee Rush.',
cpp_compiler: 'First attack each turn deals double.',
chat_mod_sword: 'Start fights with 1 Weak on all enemies.'
};
return texts[relicId] || 'Unknown relic';
}
function getCardArt(cardId) {
const artMappings = {
strike: 'Monk_1.png',
'strike+': 'Monk_2.png',
defend: 'Monk_3.png',
'defend+': 'Monk_4.png',
coffee_rush: 'Monk_5.png', // Energy boost
'coffee_rush+': 'Monk_6.png', // Upgraded energy
macro: 'Monk_7.png', // Replay magic
refactor: 'Monk_8.png', // Refactoring tool
type_safety: 'Monk_9.png', // Protection
chat_ban: 'Monk_10.png', // Ban/restriction
segfault: 'Monk_11.png', // Powerful attack
null_pointer: 'Monk_12.png', // Precise strike
recursion: 'Monk_13.png', // Repetition
merge_conflict: 'Monk_14.png', // Dual attack
hotfix: 'Monk_15.png', // Emergency fix
production_deploy: 'Monk_16.png', // High risk/reward
gc: 'Monk_17.png', // Cleanup
async_await: 'Monk_18.png', // Time manipulation
stack_overflow: 'Monk_19.png', // Knowledge overflow
infinite_vim: 'Monk_20.png', // Infinite power
debug_print: 'Monk_21.png', // Information
git_commit: 'Monk_22.png', // Recording
memory_leak: 'Monk_23.png', // Draining effect
code_review: 'Monk_24.png', // Investigation
pair_programming: 'Monk_25.png', // Cooperation
rubber_duck: 'Monk_26.png', // Helpful companion
unit_test: 'Monk_27.png', // Testing/verification
sugar_crash: 'Monk_28.png' // Negative effect
};
const imagePath = artMappings[cardId];
if (imagePath) {
return `<img src="assets/skill-art/${imagePath}" alt="${cardId}" class="card-art-image" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
<span class="card-art-fallback" style="display: none;">${getCardArtFallback(cardId)}</span>`;
}
return getCardArtFallback(cardId);
}
function getCardArtFallback(cardId) {
const fallbacks = {
strike: '👊', defend: '🛡', coffee_rush: '☕', macro: '🔄',
refactor: '⚡', type_safety: '🔒', chat_ban: '🚫', segfault: '💥',
gc: '🗑', async_await: '⏳', stack_overflow: '📚', infinite_vim: '♾',
debug_print: '🐛', null_pointer: '❌', recursion: '🔁', git_commit: '📝',
memory_leak: '🕳', code_review: '👀', pair_programming: '👥', hotfix: '🚨',
rubber_duck: '🦆', merge_conflict: '⚔', unit_test: '✅', production_deploy: '🚀',
sugar_crash: '🍰'
};
return fallbacks[cardId] || '🃏';
}
function getEnemyArt(enemyId, ENEMIES = null) {
const enemyData = ENEMIES?.[enemyId];
const avatarPath = enemyData?.avatar || `assets/avatars/${enemyId}.png`;
return `<img src="${avatarPath}" alt="${enemyId}" class="enemy-avatar-img" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
<span class="enemy-fallback-emoji" style="display: none;">${getEnemyFallbackEmoji(enemyId)}</span>`;
}
function getEnemyFallbackEmoji(enemyId) {
const arts = {
chat_gremlin: '👹',
type_checker: '🤖',
js_blob: '🟢',
infinite_loop: '🌀',
merge_conflict_enemy: '⚔',
bug_404: '❌',
elite_ts_demon: '😈',
elite_refactor: '🐉',
boss_birthday_bug: '🎂👾'
};
return arts[enemyId] || '👾';
}
function getEnemyType(enemyId) {
if (enemyId.includes('boss_')) return 'BOSS';
if (enemyId.includes('elite_')) return 'ELITE';
return 'ENEMY';
}
export function renderRelicSelection(root) {
import("../data/relics.js").then(({ RELICS, START_RELIC_CHOICES }) => {
const relicChoices = START_RELIC_CHOICES.slice(0, 3); // Show first 3 relics
root.app.innerHTML = `
<div class="game-screen relic-select">
<div class="game-header">
<div class="game-logo relic-title-logo">
<svg width="600" height="240" viewBox="0 0 600 240" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#ffd700;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ff8c00;stop-opacity:1" />
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1"/>
<feOffset dx="1" dy="1" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.3"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<text x="300" y="80" text-anchor="middle" font-family="'Kreon', serif" font-size="55" font-weight="700" fill="url(#textGradient)" filter="url(#glow)">
ThePrimeagen
</text>
<text x="300" y="170" text-anchor="middle" font-family="'Kreon', serif" font-size="85" font-weight="700" fill="url(#textGradient)" filter="url(#shadow) url(#glow)">
Spire
</text>
</svg>
</div>
<h1>Choose a Starting Relic</h1>
<p>Select one of the following relics to begin your run.</p>
</div>
<div class="relic-options">
${relicChoices.map(relicId => {
const relic = RELICS[relicId];
return `
<div class="relic-option" data-relic="${relicId}">
<div class="relic-portrait">
<div class="relic-icon">${getRelicEmoji(relicId)}</div>
</div>
<div class="relic-info">
<div class="relic-name">${relic.name}</div>
<div class="relic-description">${relic.text}</div>
</div>
</div>
`;
}).join("")}
</div>
</div>
`;
root.app.querySelectorAll("[data-relic]").forEach(btn => {
btn.addEventListener("click", () => {
const relicId = btn.dataset.relic;
root.selectStartingRelic(relicId);
});
});
});
}
export function renderEvent(root) {
const events = [
{
title: "Birthday Cake",
text: "You find a delicious birthday cake! But it looks suspicious...",
artwork: "assets/card-art/bread.png",
choices: [
{
text: "Eat the whole cake (+15 HP, gain Sugar Crash curse)",
icon: "assets/card-art/apple.png",
risk: "high",
effect: () => {
root.player.hp = Math.min(root.player.maxHp, root.player.hp + 15);
root.player.deck.push("sugar_crash");
root.log("Ate cake: +15 HP, added Sugar Crash curse");
}
},
{
text: "Take a small bite (+8 HP)",
icon: "assets/card-art/heart.png",
risk: "low",
effect: () => {
root.player.hp = Math.min(root.player.maxHp, root.player.hp + 8);
root.log("Small bite: +8 HP");
}
},
{
text: "Leave it alone (gain 25 gold)",
icon: "assets/card-art/bag_of_gold.png",
risk: "none",
effect: () => {
root.player.gold += 25;
root.log("Resisted temptation: +25 gold");
}
}
]
},
{
title: "Birthday Present",
text: "A mysterious gift box sits before you. What could be inside?",
artwork: "assets/card-art/chest_closed.png",
choices: [
{
text: "Open it eagerly (Random card or lose 10 HP)",
icon: "assets/card-art/key.png",
risk: "high",
effect: () => {
if (Math.random() < 0.7) {
import("../data/cards.js").then(({ CARDS, CARD_POOL }) => {
const randomCard = CARD_POOL[Math.floor(Math.random() * CARD_POOL.length)];
root.player.deck.push(randomCard);
root.log(`Found ${CARDS[randomCard].name}!`);
});
} else {
root.player.hp = Math.max(1, root.player.hp - 10);
root.log("It was a trap! -10 HP");
}
}
},
{
text: "Open it carefully (+5 Max HP)",
icon: "assets/card-art/potion_heal.png",
risk: "low",
effect: () => {
root.player.maxHp += 5;
root.player.hp += 5;
root.log("Careful approach: +5 Max HP");
}
},
{
text: "Don't touch it (gain 30 gold)",
icon: "assets/card-art/bag_of_gold.png",
risk: "none",
effect: () => {
root.player.gold += 30;
root.log("Played it safe: +30 gold");
}
}
]
},
{
title: "Birthday Balloons",
text: "Colorful balloons float by. One has a note attached: 'Pop me for a surprise!'",
artwork: "assets/card-art/feather.png",
choices: [
{
text: "Pop the balloon (Remove a random basic card from deck)",
icon: "assets/card-art/scroll.png",
risk: "medium",
effect: () => {
const basicCards = root.player.deck.filter(id => id === "strike" || id === "defend");
if (basicCards.length > 0) {
const toRemove = basicCards[0];
const index = root.player.deck.indexOf(toRemove);
root.player.deck.splice(index, 1);
root.log(`Removed ${toRemove} from deck`);
} else {
root.log("No basic cards to remove");
}
}
},
{
text: "Collect the balloons (+1 Energy next 3 fights)",
icon: "assets/card-art/magic_sphere.png",
risk: "low",
effect: () => {
root.flags.bonusEnergyFights = 3;
root.log("Collected balloons: +1 Energy next 3 fights");
}
},
{
text: "Ignore them (heal 12 HP)",
icon: "assets/card-art/heart.png",
risk: "none",
effect: () => {
root.player.hp = Math.min(root.player.maxHp, root.player.hp + 12);
root.log("Focused on rest: +12 HP");
}
}
]
}
];
const event = events[Math.floor(Math.random() * events.length)];
root.app.innerHTML = `
<div class="event-screen">
<div class="event-header">
<h1>${event.title}</h1>
<p>A birthday adventure awaits your decision</p>
<div class="player-status-inline">
<div class="status-item">
<img src="assets/card-art/heart.png" alt="Health" class="status-icon-img">
<span>${root.player.hp}/${root.player.maxHp} HP</span>
</div>
<div class="status-item">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="status-icon-img">
<span>${root.player.gold || 0} Gold</span>
</div>
</div>
</div>
<div class="event-content">
<div class="event-story">
<div class="event-artwork">
<img src="${event.artwork}" alt="Event" class="event-artwork-img">
</div>
<div class="event-description">
<p>${event.text}</p>
</div>
</div>
<div class="event-choices">
<h3>Choose your action:</h3>
<div class="choices-grid">
${event.choices.map((choice, idx) => `
<div class="event-choice ${choice.risk}-risk" data-choice="${idx}">
<div class="choice-icon">
<img src="${choice.icon}" alt="Choice" class="choice-icon-img">
</div>
<div class="choice-content">
<div class="choice-text">${choice.text}</div>
<div class="choice-risk-badge ${choice.risk}">
${choice.risk === 'high' ? 'High Risk' : choice.risk === 'medium' ? 'Medium Risk' : choice.risk === 'low' ? 'Low Risk' : 'Safe'}
</div>
</div>
</div>
`).join("")}
</div>
</div>
</div>
</div>
`;
root.app.querySelectorAll("[data-choice]").forEach(btn => {
btn.addEventListener("click", () => {
const idx = parseInt(btn.dataset.choice, 10);
event.choices[idx].effect();
root.afterNode();
});
});
}
export function renderWin(root) {
const finalStats = {
totalTurns: root.turnCount || 0,
cardsPlayed: root.cardsPlayedCount || 0,
finalHP: root.player.hp,
maxHP: root.player.maxHp,
finalGold: root.player.gold || 0,
deckSize: root.player.deck.length,
relicsCollected: root.relicStates.length
};
root.app.innerHTML = `
<div class="victory-screen">
<div class="victory-header">
<div class="victory-crown">
<img src="assets/card-art/crown.png" alt="Victory Crown" class="crown-img">
</div>
<h1>VICTORY ACHIEVED!</h1>
<h2>ThePrimeagen Spire Has Been Conquered!</h2>
<p>ThePrimeagen's birthday celebration can continue in peace!</p>
</div>
<div class="victory-content">
<div class="victory-artwork">
<div class="victory-scene">
<img src="assets/card-art/trophy.png" alt="Trophy" class="victory-trophy">
<div class="victory-glow"></div>
</div>
</div>
<div class="victory-stats">
<h3>Final Statistics</h3>
<div class="stats-grid">
<div class="stat-item">
<img src="assets/card-art/heart.png" alt="Health" class="stat-icon">
<div class="stat-info">
<span class="stat-label">Final Health</span>
<span class="stat-value">${finalStats.finalHP}/${finalStats.maxHP}</span>
</div>
</div>
<div class="stat-item">
<img src="assets/card-art/bag_of_gold.png" alt="Gold" class="stat-icon">
<div class="stat-info">
<span class="stat-label">Gold Remaining</span>
<span class="stat-value">${finalStats.finalGold}</span>
</div>
</div>
<div class="stat-item">
<img src="assets/card-art/book.png" alt="Deck" class="stat-icon">
<div class="stat-info">
<span class="stat-label">Final Deck Size</span>
<span class="stat-value">${finalStats.deckSize} cards</span>
</div>
</div>
<div class="stat-item">
<img src="assets/card-art/runestone.png" alt="Relics" class="stat-icon">
<div class="stat-info">
<span class="stat-label">Relics Collected</span>
<span class="stat-value">${finalStats.relicsCollected}</span>
</div>
</div>
</div>
</div>
<div class="victory-relics">
<h3>Relics Mastered</h3>
<div class="relics-showcase">
${root.relicStates.length > 0 ?
root.relicStates.map(r => `
<div class="relic-showcase-item" title="${getRelicText(r.id)}">
<div class="relic-showcase-icon">${getRelicEmoji(r.id)}</div>
<div class="relic-showcase-name">${getRelicName(r.id)}</div>
</div>
`).join('') :
'<div class="no-relics">No relics collected this run</div>'
}
</div>
</div>
<div class="victory-message">
<div class="birthday-celebration">
<h3>Birthday Celebration Complete!</h3>
<p>Thanks to your heroic efforts in your old age. ThePrimeagen's boomer years shall continue!</p>
<p class="victory-quote">"Happy Birthday Prime! Hope you have a good one!"</p>
</div>
</div>
</div>
<div class="victory-actions">
<button class="victory-btn primary" data-replay>
<img src="assets/card-art/scroll.png" alt="New Run" class="btn-icon">
<span>Start New Adventure</span>
</button>
</div>
</div>
`;
root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset());
}
export function renderLose(root) {
const finalStats = {
totalTurns: root.turnCount || 0,
cardsPlayed: root.cardsPlayedCount || 0,
finalHP: 0, // Player is defeated
maxHP: root.player.maxHp,
finalGold: root.player.gold || 0,
deckSize: root.player.deck.length,
relicsCollected: root.relicStates.length,
nodeId: root.nodeId || 'unknown'
};
root.app.innerHTML = `
<div class="defeat-screen">
<div class="defeat-header">
<h1>You Failed!</h1>
<h2>The Spire Claims Another Developer</h2>
<p>It seems age has slowed the CPU upstairs…
Better luck on the next run!</p>
</div>
<div class="defeat-stats">
<h3>Final Debug Report</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">Turns Survived</div>
<div class="stat-value">${finalStats.totalTurns}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">Cards Played</div>
<div class="stat-value">${finalStats.cardsPlayed}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">HP Lost</div>
<div class="stat-value">${finalStats.maxHP}/${finalStats.maxHP}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">Gold Earned</div>
<div class="stat-value">${finalStats.finalGold}</div>
</div>
</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">Deck Size</div>
<div class="stat-value">${finalStats.deckSize} cards</div>
</div>
</div>
<div class="stat-item">
<div class="stat-info">
<div class="stat-label">Relics Found</div>
<div class="stat-value">${finalStats.relicsCollected}</div>
</div>
</div>
</div>
</div>
${root.relicStates.length > 0 ? `
<div class="defeat-relics">
<h3>Tools Collected</h3>
<div class="relics-showcase">
${root.relicStates.map(relic => `
<div class="relic-showcase-item">
<div class="relic-showcase-icon">${getRelicEmoji(relic.id)}</div>
<div class="relic-showcase-name">${relic.id.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</div>
</div>
`).join('')}
</div>
</div>
` : `
<div class="defeat-relics">
<div class="no-relics">No relics were collected during this run.</div>
</div>
`}
<div class="defeat-message">
<div class="debug-session">
<h3>Post-Mortem Analysis</h3>
<div class="defeat-quote">
"Debugging is twice as hard as writing the code in the first place.<br/>
Therefore, if you write the code as cleverly as possible,<br/>
you are, by definition, not smart enough to debug it."<br/>
<em>- Brian Kernighan</em>
</div>
</div>
</div>
<div class="defeat-actions">
<button class="defeat-btn primary-btn" data-replay>
<span class="btn-icon">🔄</span>
<span>Try Again</span>
</button>
<button class="defeat-btn secondary-btn" data-menu>
<span class="btn-icon">🏠</span>
<span>Main Menu</span>
</button>
</div>
</div>
</div>
`;
root.app.querySelector("[data-replay]").addEventListener("click", () => {
root.reset();
});
root.app.querySelector("[data-menu]").addEventListener("click", () => {
root.reset();
});
}