|
|
|
@ -7,6 +7,15 @@ import { createBattle, startPlayerTurn, playCard, endTurn, makeBattleContext, at |
|
|
|
import { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, renderUpgrade, updateCardSelection, showDamageNumber, renderCodeReviewSelection } from "./ui/render.js"; |
|
|
|
import { renderBattle, renderMap, renderReward, renderRest, renderShop, renderWin, renderLose, renderEvent, renderRelicSelection, renderUpgrade, updateCardSelection, showDamageNumber, renderCodeReviewSelection } from "./ui/render.js"; |
|
|
|
import { InputManager } from "./input/InputManager.js"; |
|
|
|
import { InputManager } from "./input/InputManager.js"; |
|
|
|
import { CommandInvoker } from "./commands/CommandInvoker.js"; |
|
|
|
import { CommandInvoker } from "./commands/CommandInvoker.js"; |
|
|
|
|
|
|
|
import { GameStateMachine } from "./engine/GameStateMachine.js"; |
|
|
|
|
|
|
|
import { MapState } from "./engine/states/MapState.js"; |
|
|
|
|
|
|
|
import { BattleState } from "./engine/states/BattleState.js"; |
|
|
|
|
|
|
|
import { ShopState } from "./engine/states/ShopState.js"; |
|
|
|
|
|
|
|
import { RestState } from "./engine/states/RestState.js"; |
|
|
|
|
|
|
|
import { EventState } from "./engine/states/EventState.js"; |
|
|
|
|
|
|
|
import { VictoryState } from "./engine/states/VictoryState.js"; |
|
|
|
|
|
|
|
import { DefeatState } from "./engine/states/DefeatState.js"; |
|
|
|
|
|
|
|
import { RelicSelectionState } from "./engine/states/RelicSelectionState.js"; |
|
|
|
|
|
|
|
|
|
|
|
const app = document.getElementById("app"); |
|
|
|
const app = document.getElementById("app"); |
|
|
|
|
|
|
|
|
|
|
|
@ -20,6 +29,7 @@ const root = { |
|
|
|
enemy: null, |
|
|
|
enemy: null, |
|
|
|
inputManager: null, // Will be initialized later
|
|
|
|
inputManager: null, // Will be initialized later
|
|
|
|
commandInvoker: new CommandInvoker(), |
|
|
|
commandInvoker: new CommandInvoker(), |
|
|
|
|
|
|
|
stateMachine: null, // Will be initialized below
|
|
|
|
currentEvent: null, // For event handling
|
|
|
|
currentEvent: null, // For event handling
|
|
|
|
currentShopCards: null, // For shop handling
|
|
|
|
currentShopCards: null, // For shop handling
|
|
|
|
currentShopRelic: null, // For shop relic handling
|
|
|
|
currentShopRelic: null, // For shop relic handling
|
|
|
|
@ -27,7 +37,14 @@ const root = { |
|
|
|
_codeReviewCallback: null, // For code review completion
|
|
|
|
_codeReviewCallback: null, // For code review completion
|
|
|
|
|
|
|
|
|
|
|
|
log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, |
|
|
|
log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, |
|
|
|
async render() { await renderBattle(this); }, |
|
|
|
async render() {
|
|
|
|
|
|
|
|
if (this.stateMachine) { |
|
|
|
|
|
|
|
await this.stateMachine.render(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Fallback for initialization
|
|
|
|
|
|
|
|
await renderBattle(this); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, |
|
|
|
play(i) {
|
|
|
|
play(i) {
|
|
|
|
const battleCtx = makeBattleContext(this); |
|
|
|
const battleCtx = makeBattleContext(this); |
|
|
|
playCard(battleCtx, i);
|
|
|
|
playCard(battleCtx, i);
|
|
|
|
@ -44,23 +61,17 @@ const root = { |
|
|
|
const node = this.map.nodes.find(n => n.id === nextId); |
|
|
|
const node = this.map.nodes.find(n => n.id === nextId); |
|
|
|
if (!node) return; |
|
|
|
if (!node) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Use state machine for transitions
|
|
|
|
if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { |
|
|
|
if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { |
|
|
|
|
|
|
|
await this.stateMachine.setState('BATTLE'); |
|
|
|
this._battleInProgress = true; |
|
|
|
} else if (node.kind === "rest") { |
|
|
|
createBattle(this, node.enemy); |
|
|
|
await this.stateMachine.setState('REST'); |
|
|
|
await renderBattle(this); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.save(); |
|
|
|
|
|
|
|
if (node.kind === "rest") { |
|
|
|
|
|
|
|
await renderRest(this); |
|
|
|
|
|
|
|
} else if (node.kind === "shop") { |
|
|
|
} else if (node.kind === "shop") { |
|
|
|
renderShop(this); |
|
|
|
await this.stateMachine.setState('SHOP'); |
|
|
|
} else if (node.kind === "event") { |
|
|
|
} else if (node.kind === "event") { |
|
|
|
renderEvent(this); |
|
|
|
await this.stateMachine.setState('EVENT'); |
|
|
|
} else if (node.kind === "start") { |
|
|
|
} else if (node.kind === "start") { |
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
@ -69,8 +80,8 @@ const root = { |
|
|
|
this.completedNodes.push(this.nodeId); |
|
|
|
this.completedNodes.push(this.nodeId); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
|
|
|
|
|
|
|
|
|
|
if (node.kind === "battle" || node.kind === "elite") { |
|
|
|
if (node.kind === "battle" || node.kind === "elite") { |
|
|
|
const choices = pickCards(3); |
|
|
|
const choices = pickCards(3); |
|
|
|
this._pendingChoices = choices; |
|
|
|
this._pendingChoices = choices; |
|
|
|
@ -78,10 +89,11 @@ const root = { |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
if (node.kind === "boss") { |
|
|
|
if (node.kind === "boss") { |
|
|
|
await renderWin(this); return; |
|
|
|
await this.stateMachine.setState('VICTORY'); |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
async takeReward(idx) { |
|
|
|
async takeReward(idx) { |
|
|
|
@ -92,12 +104,12 @@ const root = { |
|
|
|
} |
|
|
|
} |
|
|
|
this._pendingChoices = null; |
|
|
|
this._pendingChoices = null; |
|
|
|
this.save(); |
|
|
|
this.save(); |
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
async skipReward() { |
|
|
|
async skipReward() { |
|
|
|
this._pendingChoices = null; |
|
|
|
this._pendingChoices = null; |
|
|
|
this.save(); |
|
|
|
this.save(); |
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
async onWin() { |
|
|
|
async onWin() { |
|
|
|
@ -111,9 +123,11 @@ const root = { |
|
|
|
this._battleInProgress = false; |
|
|
|
this._battleInProgress = false; |
|
|
|
|
|
|
|
|
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
|
|
const node = this.map.nodes.find(n => n.id === this.nodeId); |
|
|
|
|
|
|
|
|
|
|
|
if (node.kind === "boss") { |
|
|
|
if (node.kind === "boss") { |
|
|
|
// Check if there's a next act
|
|
|
|
// Check if there's a next act
|
|
|
|
const nextAct = this.currentAct === "act1" ? "act2" : null; |
|
|
|
const nextAct = this.currentAct === "act1" ? "act2" : null; |
|
|
|
|
|
|
|
|
|
|
|
if (nextAct && MAPS[nextAct]) { |
|
|
|
if (nextAct && MAPS[nextAct]) { |
|
|
|
// Advance to next act
|
|
|
|
// Advance to next act
|
|
|
|
this.currentAct = nextAct; |
|
|
|
this.currentAct = nextAct; |
|
|
|
@ -128,27 +142,27 @@ const root = { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.save(); |
|
|
|
this.save(); |
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// Final victory
|
|
|
|
// Final victory
|
|
|
|
this.save(); // Save progress before clearing on victory
|
|
|
|
this.save(); // Save progress before clearing on victory
|
|
|
|
this.clearSave(); // Clear save on victory
|
|
|
|
this.clearSave(); // Clear save on victory
|
|
|
|
await renderWin(this); |
|
|
|
await this.stateMachine.setState('VICTORY'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
this.save(); |
|
|
|
this.save(); |
|
|
|
this.afterNode(); |
|
|
|
await this.afterNode(); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
async onLose() { |
|
|
|
async onLose() { |
|
|
|
|
|
|
|
|
|
|
|
this._battleInProgress = false; |
|
|
|
this._battleInProgress = false; |
|
|
|
this.clearSave(); // Clear save on defeat
|
|
|
|
this.clearSave(); // Clear save on defeat
|
|
|
|
await renderLose(this); |
|
|
|
await this.stateMachine.setState('DEFEAT'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
reset() { |
|
|
|
async reset() { |
|
|
|
this.logs = []; |
|
|
|
this.logs = []; |
|
|
|
this.player = makePlayer(); |
|
|
|
this.player = makePlayer(); |
|
|
|
initDeck(this.player); |
|
|
|
initDeck(this.player); |
|
|
|
@ -157,13 +171,13 @@ const root = { |
|
|
|
this.nodeId = "n1"; |
|
|
|
this.nodeId = "n1"; |
|
|
|
this.completedNodes = []; |
|
|
|
this.completedNodes = []; |
|
|
|
|
|
|
|
|
|
|
|
renderRelicSelection(this); |
|
|
|
await this.stateMachine.setState('RELIC_SELECTION'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
async selectStartingRelic(relicId) { |
|
|
|
async selectStartingRelic(relicId) { |
|
|
|
attachRelics(this, [relicId]); |
|
|
|
attachRelics(this, [relicId]); |
|
|
|
this.save(); |
|
|
|
this.save(); |
|
|
|
await renderMap(this); |
|
|
|
await this.stateMachine.setState('MAP'); |
|
|
|
}, |
|
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
save() { |
|
|
|
save() { |
|
|
|
@ -176,6 +190,7 @@ const root = { |
|
|
|
completedNodes: this.completedNodes, |
|
|
|
completedNodes: this.completedNodes, |
|
|
|
logs: this.logs.slice(-50), // Keep last 50 logs
|
|
|
|
logs: this.logs.slice(-50), // Keep last 50 logs
|
|
|
|
battleInProgress: this._battleInProgress || false, |
|
|
|
battleInProgress: this._battleInProgress || false, |
|
|
|
|
|
|
|
stateMachine: this.stateMachine ? this.stateMachine.getSaveData() : null, |
|
|
|
timestamp: Date.now() |
|
|
|
timestamp: Date.now() |
|
|
|
}; |
|
|
|
}; |
|
|
|
localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); |
|
|
|
localStorage.setItem('birthday-spire-save', JSON.stringify(saveData)); |
|
|
|
@ -300,6 +315,11 @@ const root = { |
|
|
|
this.logs = Array.isArray(data.logs) ? data.logs : []; |
|
|
|
this.logs = Array.isArray(data.logs) ? data.logs : []; |
|
|
|
this._battleInProgress = Boolean(data.battleInProgress); |
|
|
|
this._battleInProgress = Boolean(data.battleInProgress); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Restore state machine state
|
|
|
|
|
|
|
|
if (data.stateMachine && this.stateMachine) { |
|
|
|
|
|
|
|
this.stateMachine.restoreFromSave(data.stateMachine); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this.restoreCardEffects(); |
|
|
|
this.restoreCardEffects(); |
|
|
|
|
|
|
|
|
|
|
|
this.log('Game loaded from save.'); |
|
|
|
this.log('Game loaded from save.'); |
|
|
|
@ -343,6 +363,21 @@ const root = { |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize State Machine
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
root.stateMachine = new GameStateMachine(root); |
|
|
|
|
|
|
|
root.stateMachine.registerState('MAP', new MapState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('BATTLE', new BattleState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('SHOP', new ShopState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('REST', new RestState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('EVENT', new EventState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('VICTORY', new VictoryState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('DEFEAT', new DefeatState()); |
|
|
|
|
|
|
|
root.stateMachine.registerState('RELIC_SELECTION', new RelicSelectionState()); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('Error initializing state machine:', error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function pickCards(n) { |
|
|
|
function pickCards(n) { |
|
|
|
const ids = shuffle(CARD_POOL.slice()).slice(0, n); |
|
|
|
const ids = shuffle(CARD_POOL.slice()).slice(0, n); |
|
|
|
return ids.map(id => CARDS[id]); |
|
|
|
return ids.map(id => CARDS[id]); |
|
|
|
@ -394,29 +429,29 @@ async function initializeGame() { |
|
|
|
switch (screenParam.toLowerCase()) { |
|
|
|
switch (screenParam.toLowerCase()) { |
|
|
|
case 'victory': |
|
|
|
case 'victory': |
|
|
|
case 'win': |
|
|
|
case 'win': |
|
|
|
await renderWin(root); |
|
|
|
await root.stateMachine.setState('VICTORY'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'defeat': |
|
|
|
case 'defeat': |
|
|
|
case 'lose': |
|
|
|
case 'lose': |
|
|
|
await renderLose(root); |
|
|
|
await root.stateMachine.setState('DEFEAT'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'map': |
|
|
|
case 'map': |
|
|
|
await renderMap(root); |
|
|
|
await root.stateMachine.setState('MAP'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'shop': |
|
|
|
case 'shop': |
|
|
|
renderShop(root); |
|
|
|
await root.stateMachine.setState('SHOP'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'rest': |
|
|
|
case 'rest': |
|
|
|
await renderRest(root); |
|
|
|
await root.stateMachine.setState('REST'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'event': |
|
|
|
case 'event': |
|
|
|
renderEvent(root); |
|
|
|
await root.stateMachine.setState('EVENT'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'battle': |
|
|
|
case 'battle': |
|
|
|
root.go('n2'); // Battle node
|
|
|
|
root.go('n2'); // Battle node (uses state machine internally)
|
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'upgrade': |
|
|
|
case 'upgrade': |
|
|
|
await renderRest(root); |
|
|
|
await root.stateMachine.setState('REST'); |
|
|
|
setTimeout(() => { |
|
|
|
setTimeout(() => { |
|
|
|
const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); |
|
|
|
const upgradeBtn = root.app.querySelector("[data-act='upgrade']"); |
|
|
|
if (upgradeBtn) upgradeBtn.click(); |
|
|
|
if (upgradeBtn) upgradeBtn.click(); |
|
|
|
@ -424,7 +459,7 @@ async function initializeGame() { |
|
|
|
return; |
|
|
|
return; |
|
|
|
case 'relic': |
|
|
|
case 'relic': |
|
|
|
case 'relics': |
|
|
|
case 'relics': |
|
|
|
renderRelicSelection(root); |
|
|
|
await root.stateMachine.setState('RELIC_SELECTION'); |
|
|
|
return; |
|
|
|
return; |
|
|
|
default: |
|
|
|
default: |
|
|
|
console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); |
|
|
|
console.warn(`Unknown screen: ${screenParam}. Loading normal game.`); |
|
|
|
@ -541,19 +576,23 @@ async function loadNormalGame() { |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
// Battle state inconsistent, go to map
|
|
|
|
// Battle state inconsistent, go to map
|
|
|
|
root._battleInProgress = false; |
|
|
|
root._battleInProgress = false; |
|
|
|
await renderMap(root); |
|
|
|
await root.stateMachine.setState('MAP'); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
await renderMap(root); |
|
|
|
await root.stateMachine.setState('MAP'); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
} else { |
|
|
|
root.reset(); |
|
|
|
await root.reset(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Initialize InputManager
|
|
|
|
// Initialize InputManager
|
|
|
|
|
|
|
|
try { |
|
|
|
root.inputManager = new InputManager(root); |
|
|
|
root.inputManager = new InputManager(root); |
|
|
|
root.inputManager.initGlobalListeners(); |
|
|
|
root.inputManager.initGlobalListeners(); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
|
|
console.error('Error initializing InputManager:', error); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Make modules available globally for InputManager
|
|
|
|
// Make modules available globally for InputManager
|
|
|
|
window.gameModules = { |
|
|
|
window.gameModules = { |
|
|
|
@ -561,5 +600,7 @@ window.gameModules = { |
|
|
|
render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection } |
|
|
|
render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection } |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
initializeGame(); |
|
|
|
initializeGame().catch(error => { |
|
|
|
|
|
|
|
console.error('Error during game initialization:', error); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|