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.
404 lines
13 KiB
404 lines
13 KiB
import { CARDS, CARD_POOL } from "./data/cards.js"; |
|
import { START_RELIC_CHOICES } from "./data/relics.js"; |
|
import { ENEMIES } from "./data/enemies.js"; |
|
import { MAPS } from "./data/maps.js"; |
|
import { makePlayer, initDeck, draw } from "./engine/core.js"; |
|
import { createBattle, startPlayerTurn, playCard, endTurn, makeBattleContext, attachRelics } from "./engine/battle.js"; |
|
import { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, showDamageNumber } from "./ui/render.js"; |
|
|
|
const app = document.getElementById("app"); |
|
|
|
|
|
const root = { |
|
app, logs: [], map: MAPS.act1, nodeId: "n1", |
|
player: makePlayer(), |
|
relicStates: [], |
|
completedNodes: [], |
|
enemy: null, |
|
|
|
log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, |
|
render() { renderBattle(this); }, |
|
play(i) { playCard(this, i); }, |
|
showDamageNumber: showDamageNumber, |
|
end() { endTurn(this); }, |
|
|
|
|
|
go(nextId) { |
|
this.nodeId = nextId; // Always set nodeId (needed for battle logic) |
|
const node = this.map.nodes.find(n => n.id === nextId); |
|
if (!node) return; |
|
|
|
if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { |
|
|
|
this._battleInProgress = true; |
|
createBattle(this, node.enemy); |
|
renderBattle(this); |
|
} else { |
|
|
|
this.save(); |
|
if (node.kind === "rest") { |
|
renderRest(this); |
|
} else if (node.kind === "shop") { |
|
renderShop(this); |
|
} else if (node.kind === "event") { |
|
renderEvent(this); |
|
} else if (node.kind === "start") { |
|
renderMap(this); |
|
} |
|
} |
|
}, |
|
|
|
afterNode() { |
|
if (this.nodeId && !this.completedNodes.includes(this.nodeId)) { |
|
this.completedNodes.push(this.nodeId); |
|
} |
|
|
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
if (node.kind === "battle" || node.kind === "elite") { |
|
const choices = pickCards(3); |
|
this._pendingChoices = choices; |
|
renderReward(this, choices); |
|
return; |
|
} |
|
if (node.kind === "boss") { |
|
renderWin(this); return; |
|
} |
|
|
|
renderMap(this); |
|
}, |
|
|
|
takeReward(idx) { |
|
const card = this._pendingChoices[idx]; |
|
if (card) { |
|
this.player.deck.push(card.id); |
|
this.log(`Added card: ${card.name}`); |
|
} |
|
this._pendingChoices = null; |
|
this.save(); |
|
renderMap(this); |
|
}, |
|
skipReward() { |
|
this._pendingChoices = null; |
|
this.save(); |
|
renderMap(this); |
|
}, |
|
|
|
onWin() { |
|
this.log("Enemy defeated! 🎉"); |
|
|
|
const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold |
|
this.player.gold = (this.player.gold || 0) + goldReward; |
|
this.log(`+${goldReward} gold`); |
|
|
|
|
|
this._battleInProgress = false; |
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
if (node.kind === "boss") { |
|
this.save(); // Save progress before clearing on victory |
|
this.clearSave(); // Clear save on victory |
|
renderWin(this); |
|
} |
|
else { |
|
|
|
this.save(); |
|
this.afterNode(); |
|
} |
|
}, |
|
onLose() { |
|
|
|
this._battleInProgress = false; |
|
this.clearSave(); // Clear save on defeat |
|
renderLose(this); |
|
}, |
|
|
|
reset() { |
|
this.logs = []; |
|
this.player = makePlayer(); |
|
initDeck(this.player); |
|
this.nodeId = "n1"; |
|
this.completedNodes = []; |
|
|
|
renderRelicSelection(this); |
|
}, |
|
|
|
selectStartingRelic(relicId) { |
|
attachRelics(this, [relicId]); |
|
this.log(`Selected starting relic: ${relicId}`); |
|
this.save(); |
|
renderMap(this); |
|
}, |
|
|
|
save() { |
|
try { |
|
const saveData = { |
|
player: this.player, |
|
nodeId: this.nodeId, |
|
relicStates: this.relicStates, |
|
completedNodes: this.completedNodes, |
|
logs: this.logs.slice(-50), // Keep last 50 logs |
|
battleInProgress: this._battleInProgress || false, |
|
timestamp: Date.now() |
|
}; |
|
localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); |
|
} catch (e) { |
|
console.warn('Failed to save game:', e); |
|
} |
|
}, |
|
|
|
load() { |
|
try { |
|
const saveData = localStorage.getItem('birthday-spire-save'); |
|
if (saveData) { |
|
const data = JSON.parse(saveData); |
|
this.player = data.player; |
|
this.nodeId = data.nodeId; |
|
this.relicStates = data.relicStates || []; |
|
this.completedNodes = data.completedNodes || []; |
|
this.logs = data.logs || []; |
|
this._battleInProgress = data.battleInProgress || false; |
|
|
|
|
|
this.restoreCardEffects(); |
|
|
|
this.log('Game loaded from save.'); |
|
return true; |
|
} |
|
} catch (e) { |
|
console.warn('Failed to load game:', e); |
|
} |
|
return false; |
|
}, |
|
|
|
restoreCardEffects() { |
|
|
|
const restoreCard = (card) => { |
|
if (card && card.id && !card.effect) { |
|
const originalCard = CARDS[card.id]; |
|
if (originalCard && originalCard.effect) { |
|
card.effect = originalCard.effect; |
|
|
|
} |
|
} |
|
}; |
|
|
|
|
|
if (this.player.hand) { |
|
this.player.hand.forEach(restoreCard); |
|
} |
|
}, |
|
|
|
clearSave() { |
|
localStorage.removeItem('birthday-spire-save'); |
|
}, |
|
|
|
// Clear any old saves with outdated card IDs |
|
clearOldSaves() { |
|
localStorage.removeItem('birthday-spire-save'); |
|
} |
|
}; |
|
|
|
function pickCards(n) { |
|
const ids = shuffle(CARD_POOL.slice()).slice(0, n); |
|
return ids.map(id => CARDS[id]); |
|
} |
|
|
|
function shuffle(a) { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[a[i], a[j]] = [a[j], a[i]] } return a; } |
|
|
|
const _createBattle = root.go.bind(root); |
|
root.go = function(nextId) { |
|
_createBattle(nextId); |
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
if (node && (node.kind === "battle" || node.kind === "elite" || node.kind === "boss")) { |
|
const ctx = makeBattleContext(this); |
|
|
|
this.deal = ctx.deal; |
|
this.applyWeak = ctx.applyWeak; |
|
this.applyVulnerable = ctx.applyVulnerable; |
|
this.draw = ctx.draw; |
|
this.forceEndTurn = ctx.forceEndTurn; |
|
this.promptExhaust = ctx.promptExhaust; |
|
this.scalarFromWeak = ctx.scalarFromWeak; |
|
this.intentIsAttack = ctx.intentIsAttack; |
|
this.showDamageNumber = ctx.showDamageNumber; |
|
this.flags = ctx.flags; |
|
this.lastCard = ctx.lastCard; |
|
} |
|
}; |
|
|
|
|
|
function initializeGame() { |
|
const urlParams = new URLSearchParams(window.location.search); |
|
const screenParam = urlParams.get('screen'); |
|
const dev = urlParams.get('dev'); |
|
|
|
// Check if it's ThePrimeagen's birthday yet (September 9, 2025) |
|
// Skip countdown if ?dev=true is in URL |
|
const now = new Date(); |
|
const birthday = new Date('2025-09-09T00:00:00'); |
|
|
|
console.log('Current date:', now); |
|
console.log('Birthday date:', birthday); |
|
console.log('Before birthday?', now < birthday); |
|
console.log('Dev mode?', dev); |
|
|
|
if (now < birthday && dev !== 'true') { |
|
console.log('Showing countdown!'); |
|
showCountdown(birthday); |
|
return; |
|
} else { |
|
console.log('Showing game - either past birthday or dev mode'); |
|
} |
|
|
|
if (screenParam) { |
|
setupMockData(); |
|
|
|
switch (screenParam.toLowerCase()) { |
|
case 'victory': |
|
case 'win': |
|
renderWin(root); |
|
return; |
|
case 'defeat': |
|
case 'lose': |
|
renderLose(root); |
|
return; |
|
case 'map': |
|
renderMap(root); |
|
return; |
|
case 'shop': |
|
renderShop(root); |
|
return; |
|
case 'rest': |
|
renderRest(root); |
|
return; |
|
case 'event': |
|
renderEvent(root); |
|
return; |
|
case 'battle': |
|
root.go('n2'); // Battle node |
|
return; |
|
case 'upgrade': |
|
renderRest(root); |
|
setTimeout(() => { |
|
const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); |
|
if (upgradeBtn) upgradeBtn.click(); |
|
}, 100); |
|
return; |
|
case 'relic': |
|
case 'relics': |
|
renderRelicSelection(root); |
|
return; |
|
default: |
|
console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); |
|
loadNormalGame(); |
|
return; |
|
} |
|
} else { |
|
loadNormalGame(); |
|
} |
|
} |
|
|
|
function setupMockData() { |
|
root.player.hp = 42; |
|
root.player.maxHp = 50; |
|
root.player.gold = 150; |
|
root.player.energy = 3; |
|
root.player.deck = ['strike', 'defend', 'coffee_rush', 'raw_dog', 'segfault', 'virgin']; |
|
root.player.hand = ['strike', 'coffee_rush', 'raw_dog']; |
|
root.player.draw = ['defend', 'segfault']; |
|
root.player.discard = ['virgin']; |
|
|
|
attachRelics(root, ['coffee_thermos', 'cpp_compiler']); |
|
|
|
root.completedNodes = ['n1', 'n2', 'n4']; |
|
root.nodeId = 'n7'; |
|
root.logs = [ |
|
'Game loaded for testing', |
|
'Mock data initialized', |
|
'Ready for screen testing!' |
|
]; |
|
} |
|
|
|
function showCountdown(birthday) { |
|
document.body.innerHTML = ` |
|
<div class="countdown-screen"> |
|
<div class="countdown-content"> |
|
<div class="countdown-timer"> |
|
<div class="countdown-message"> |
|
<p>Your epic birthday surprise launches in:</p> |
|
</div> |
|
|
|
<div class="countdown-display"> |
|
<div class="time-unit"> |
|
<div class="time-number" id="days">--</div> |
|
<div class="time-label">Days</div> |
|
</div> |
|
<div class="time-separator">:</div> |
|
<div class="time-unit"> |
|
<div class="time-number" id="hours">--</div> |
|
<div class="time-label">Hours</div> |
|
</div> |
|
<div class="time-separator">:</div> |
|
<div class="time-unit"> |
|
<div class="time-number" id="minutes">--</div> |
|
<div class="time-label">Minutes</div> |
|
</div> |
|
<div class="time-separator">:</div> |
|
<div class="time-unit"> |
|
<div class="time-number" id="seconds">--</div> |
|
<div class="time-label">Seconds</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
// Start the countdown timer |
|
const timer = setInterval(() => { |
|
const now = new Date(); |
|
const timeLeft = birthday - now; |
|
|
|
if (timeLeft <= 0) { |
|
clearInterval(timer); |
|
// Birthday reached! Reload to show the game |
|
window.location.reload(); |
|
return; |
|
} |
|
|
|
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); |
|
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); |
|
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); |
|
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); |
|
|
|
document.getElementById('days').textContent = days.toString().padStart(2, '0'); |
|
document.getElementById('hours').textContent = hours.toString().padStart(2, '0'); |
|
document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0'); |
|
document.getElementById('seconds').textContent = seconds.toString().padStart(2, '0'); |
|
}, 1000); |
|
} |
|
|
|
function loadNormalGame() { |
|
const hasLoadedData = root.load(); |
|
if (hasLoadedData) { |
|
// If we were in a battle, resume it |
|
if (root._battleInProgress) { |
|
const node = root.map.nodes.find(n => n.id === root.nodeId); |
|
if (node && (node.kind === "battle" || node.kind === "elite" || node.kind === "boss")) { |
|
root.go(root.nodeId); |
|
} else { |
|
// Battle state inconsistent, go to map |
|
root._battleInProgress = false; |
|
renderMap(root); |
|
} |
|
} else { |
|
renderMap(root); |
|
} |
|
} else { |
|
root.reset(); |
|
} |
|
} |
|
|
|
initializeGame(); |
|
|
|
|