Browse Source

think this is the final gameplay commit

main
Stephanie Gredell 4 months ago
parent
commit
16650df85b
  1. BIN
      assets/sounds/played-card.mp3
  2. BIN
      assets/sounds/would-you-like-to-play.mp3
  3. 5
      src/data/cards.js
  4. 53
      src/data/enemies.js
  5. 28
      src/data/maps.js
  6. 68
      src/main.js
  7. 1263
      src/ui/render.js

BIN
assets/sounds/played-card.mp3

Binary file not shown.

BIN
assets/sounds/would-you-like-to-play.mp3

Binary file not shown.

5
src/data/cards.js

@ -1,5 +1,3 @@
export const CARDS = { export const CARDS = {
strike: { strike: {
id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy", id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy",
@ -158,7 +156,6 @@ export const CARDS = {
id: "code_review", name: "Code Review", cost: 1, type: "skill", text: "Look at top 3 cards. Put 1 in hand, rest on bottom of deck.", id: "code_review", name: "Code Review", cost: 1, type: "skill", text: "Look at top 3 cards. Put 1 in hand, rest on bottom of deck.",
art: "Monk_24.png", art: "Monk_24.png",
effect: (ctx) => { effect: (ctx) => {
ctx.draw(1); ctx.draw(1);
ctx.log("Code review reveals useful insights. You draw a card."); ctx.log("Code review reveals useful insights. You draw a card.");
} }
@ -338,7 +335,7 @@ export const CARDS = {
export const STARTER_DECK = [ export const STARTER_DECK = [
"strike", "strike", "defend", "defend", "strike", "strike", "defend", "defend",
"segfault", "coffee_rush", "skill_issue", "git_commit", "segfault", "coffee_rush", "skill_issue", "git_commit",
"stack_trace", "raw_dog" "stack_trace", "stack_trace"
]; ];
export const CARD_POOL = [ export const CARD_POOL = [

53
src/data/enemies.js

@ -22,8 +22,15 @@ export const ENEMIES = {
ctx.log("Codegirl resolves the merge conflict and heals 8 HP!"); ctx.log("Codegirl resolves the merge conflict and heals 8 HP!");
} }
}, },
lowkeyabu: {
id: "lowkeyabu", name: "LowKeyAbu", maxHp: 85,
avatar: "assets/avatars/7.png", // Powerful demon/witch
background: "assets/backgrounds/castle.png", // Repeat background
ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 10 },
onDebuff: (ctx) => ctx.applyVulnerable(ctx.player, 1)
},
nightshadedude: { nightshadedude: {
id: "nightshadedude", name: "Nightshadedude", maxHp: 85, id: "nightshadedude", name: "Nightshadedude", maxHp: 120,
avatar: "assets/avatars/11.png", // Powerful demon/witch avatar: "assets/avatars/11.png", // Powerful demon/witch
background: "assets/backgrounds/dead forest.png", // Repeat background background: "assets/backgrounds/dead forest.png", // Repeat background
ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 14 }, ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 14 },
@ -57,28 +64,28 @@ export const ENEMIES = {
}, },
// ACT 2 ENEMIES - Harder versions // ACT 2 ENEMIES - Harder versions
senior_dev: { teej: {
id: "senior_dev", name: "Senior Dev", maxHp: 65, id: "teej", name: "Teej", maxHp: 65,
avatar: "assets/avatars/elite_ts_demon.png", avatar: "assets/avatars/elite_ts_demon.png",
background: "assets/backgrounds/castle.png", background: "assets/backgrounds/castle.png",
ai: (turn) => turn % 3 === 0 ? { type: "debuff", value: 2 } : { type: "attack", value: turn % 2 === 0 ? 12 : 14 }, ai: (turn) => turn % 3 === 0 ? { type: "debuff", value: 2 } : { type: "attack", value: turn % 2 === 0 ? 12 : 14 },
onDebuff: (ctx) => ctx.applyWeak(ctx.player, 2) onDebuff: (ctx) => ctx.applyWeak(ctx.player, 2)
}, },
tech_lead: { begin: {
id: "tech_lead", name: "Tech Lead", maxHp: 80, id: "begin", name: "Begin", maxHp: 80,
avatar: "assets/avatars/infinite_loop.png", avatar: "assets/avatars/infinite_loop.png",
background: "assets/backgrounds/dead forest.png", background: "assets/backgrounds/dead forest.png",
ai: (turn) => (turn % 2 === 0) ? { type: "attack", value: 16 } : { type: "block", value: 12 } ai: (turn) => (turn % 2 === 0) ? { type: "attack", value: 16 } : { type: "block", value: 12 }
}, },
code_reviewer: { adam: {
id: "code_reviewer", name: "Code Reviewer", maxHp: 70, id: "adam", name: "Adam", maxHp: 70,
avatar: "assets/avatars/chat_gremlin.png", avatar: "assets/avatars/chat_gremlin.png",
background: "assets/backgrounds/terrace.png", background: "assets/backgrounds/terrace.png",
ai: (turn) => turn % 4 === 0 ? { type: "debuff", value: 1 } : { type: "attack", value: 13 }, ai: (turn) => turn % 4 === 0 ? { type: "debuff", value: 1 } : { type: "attack", value: 13 },
onDebuff: (ctx) => { ctx.applyVulnerable(ctx.player, 1); ctx.log("Code Reviewer finds bugs in your logic!"); } onDebuff: (ctx) => { ctx.applyVulnerable(ctx.player, 1); ctx.log("Adam finds bugs in your logic!"); }
}, },
scrum_master: { david: {
id: "scrum_master", name: "Scrum Master", maxHp: 90, id: "david", name: "David", maxHp: 90,
avatar: "assets/avatars/js_blob.png", avatar: "assets/avatars/js_blob.png",
background: "assets/backgrounds/castle.png", background: "assets/backgrounds/castle.png",
ai: (turn) => { ai: (turn) => {
@ -87,10 +94,10 @@ export const ENEMIES = {
if (cyc === 1) return { type: "attack", value: 11 }; if (cyc === 1) return { type: "attack", value: 11 };
return { type: "debuff", value: 1 }; return { type: "debuff", value: 1 };
}, },
onDebuff: (ctx) => { ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; ctx.log("Scrum Master schedules another meeting! Lose 1 energy next turn."); } onDebuff: (ctx) => { ctx.flags.nextTurnEnergyPenalty = (ctx.flags.nextTurnEnergyPenalty || 0) + 1; ctx.log("David schedules another meeting! Lose 1 energy next turn."); }
}, },
architect: { dax: {
id: "architect", name: "The Architect", maxHp: 150, id: "dax", name: "Dax", maxHp: 150,
avatar: "assets/avatars/bug_404.png", avatar: "assets/avatars/bug_404.png",
background: "assets/backgrounds/throne room.png", background: "assets/backgrounds/throne room.png",
ai: (turn) => { ai: (turn) => {
@ -101,7 +108,23 @@ export const ENEMIES = {
if (cyc === 4) return { type: "attack", value: 30 }; if (cyc === 4) return { type: "attack", value: 30 };
return { type: "attack", value: 20 }; return { type: "attack", value: 20 };
}, },
onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("The Architect redesigns your entire approach!"); }, onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("Dax redesigns your entire approach!"); },
onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("The Architect refactors and optimizes, healing 12 HP!"); } onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("Dax refactors and optimizes, healing 12 HP!"); }
},
taylor: {
id: "taylor", name: "Taylor Otwell", maxHp: 150,
avatar: "assets/avatars/bug_404.png",
background: "assets/backgrounds/throne room.png",
ai: (turn) => {
const cyc = turn % 5;
if (cyc === 1) return { type: "debuff", value: 2 };
if (cyc === 2) return { type: "attack", value: 25 };
if (cyc === 3) return { type: "block", value: 15 };
if (cyc === 4) return { type: "attack", value: 30 };
return { type: "attack", value: 20 };
},
onDebuff: (ctx) => { ctx.applyWeak(ctx.player, 2); ctx.applyVulnerable(ctx.player, 1); ctx.log("Taylor redesigns your entire approach!"); },
onBlock: (ctx) => { ctx.enemy.hp = Math.min(ctx.enemy.maxHp, ctx.enemy.hp + 12); ctx.log("Taylor refactors and optimizes, healing 12 HP!"); }
} }
}; };

28
src/data/maps.js

@ -14,27 +14,27 @@ export const MAPS = {
{ id: "n9", kind: "rest", next: ["n11"], x: 350, y: 330 }, { id: "n9", kind: "rest", next: ["n11"], x: 350, y: 330 },
{ id: "n10", kind: "shop", next: ["n11"], x: 650, y: 330 }, { id: "n10", kind: "shop", next: ["n11"], x: 650, y: 330 },
{ id: "n11", kind: "battle", enemy: "lithium", next: ["n12"], x: 500, y: 280 }, { id: "n11", kind: "battle", enemy: "lithium", next: ["n12"], x: 500, y: 280 },
{ id: "n12", kind: "elite", enemy: "nightshadedude", next: ["n13"], x: 500, y: 205 }, { id: "n12", kind: "elite", enemy: "lowkeyabu", next: ["n13"], x: 500, y: 205 },
{ id: "n13", kind: "rest", next: ["n14"], x: 500, y: 125 }, { id: "n13", kind: "rest", next: ["n14"], x: 500, y: 125 },
{ id: "n14", kind: "boss", enemy: "teej", next: [], x: 500, y: 40 }, { id: "n14", kind: "boss", enemy: "nightshadedude", next: [], x: 500, y: 40 },
] ]
}, },
act2: { act2: {
id: "act2", name: "Birthday Spire — Act II: The Corporate Ladder", id: "act2", name: "Birthday Spire — Act II: The Corporate Ladder",
nodes: [ nodes: [
{ id: "a2n1", kind: "start", next: ["a2n2", "a2n3"], x: 500, y: 760 }, { id: "a2n1", kind: "start", next: ["a2n2", "a2n3"], x: 500, y: 760 },
{ id: "a2n2", kind: "battle", enemy: "senior_dev", next: ["a2n4", "a2n5"], x: 400, y: 680 }, { id: "a2n2", kind: "battle", enemy: "teej", next: ["a2n5"], x: 300, y: 695 },
{ id: "a2n3", kind: "event", next: ["a2n5"], x: 600, y: 680 }, { id: "a2n3", kind: "event", next: ["a2n5"], x: 650, y: 695 },
{ id: "a2n4", kind: "shop", next: ["a2n6"], x: 300, y: 600 }, { id: "a2n5", kind: "battle", enemy: "begin", next: ["a2n4", "a2n7"], x: 500, y: 600 },
{ id: "a2n5", kind: "battle", enemy: "tech_lead", next: ["a2n6", "a2n7"], x: 500, y: 600 }, { id: "a2n4", kind: "shop", next: ["a2n6"], x: 350, y: 525 },
{ id: "a2n6", kind: "battle", enemy: "code_reviewer", next: ["a2n8"], x: 400, y: 520 }, { id: "a2n7", kind: "rest", next: ["a2n6"], x: 650, y: 525 },
{ id: "a2n7", kind: "rest", next: ["a2n8"], x: 600, y: 520 }, { id: "a2n6", kind: "battle", enemy: "adam", next: ["a2n8"], x: 500, y: 460 },
{ id: "a2n8", kind: "battle", enemy: "scrum_master", next: ["a2n9", "a2n10"], x: 500, y: 440 }, { id: "a2n8", kind: "battle", enemy: "david", next: ["a2n9", "a2n10"], x: 500, y: 360 },
{ id: "a2n9", kind: "event", next: ["a2n11"], x: 350, y: 360 }, { id: "a2n9", kind: "rest", next: ["a2n11"], x: 350, y: 320 },
{ id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 360 }, { id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 320 },
{ id: "a2n11", kind: "elite", enemy: "senior_dev", next: ["a2n12"], x: 500, y: 280 }, { id: "a2n11", kind: "elite", enemy: "dax", next: ["a2n12"], x: 500, y: 250 },
{ id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 200 }, { id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 140 },
{ id: "a2n13", kind: "boss", enemy: "architect", next: [], x: 500, y: 120 }, { id: "a2n13", kind: "boss", enemy: "taylor", next: [], x: 500, y: 40 },
] ]
} }
}; };

68
src/main.js

@ -28,7 +28,7 @@ const root = {
this.nodeId = nextId; // Always set nodeId (needed for battle logic) this.nodeId = nextId; // Always set nodeId (needed for battle logic)
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;
if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") { if (node.kind === "battle" || node.kind === "elite" || node.kind === "boss") {
this._battleInProgress = true; this._battleInProgress = true;
@ -53,7 +53,7 @@ const root = {
if (this.nodeId && !this.completedNodes.includes(this.nodeId)) { if (this.nodeId && !this.completedNodes.includes(this.nodeId)) {
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") {
@ -79,10 +79,10 @@ const root = {
this.save(); this.save();
await renderMap(this); await renderMap(this);
}, },
async skipReward() { async skipReward() {
this._pendingChoices = null; this._pendingChoices = null;
this.save(); this.save();
await renderMap(this); await renderMap(this);
}, },
async onWin() { async onWin() {
@ -91,12 +91,12 @@ const root = {
const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold const goldReward = Math.floor(Math.random() * 20) + 15; // 15-35 gold
this.player.gold = (this.player.gold || 0) + goldReward; this.player.gold = (this.player.gold || 0) + goldReward;
this.log(`+${goldReward} gold`); this.log(`+${goldReward} gold`);
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]) {
@ -112,20 +112,19 @@ const root = {
// 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 renderWin(this);
} }
} }
else { else {
this.save(); this.save();
this.afterNode(); 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 renderLose(this);
}, },
reset() { reset() {
@ -142,7 +141,6 @@ const root = {
async selectStartingRelic(relicId) { async selectStartingRelic(relicId) {
attachRelics(this, [relicId]); attachRelics(this, [relicId]);
this.log(`Selected starting relic: ${relicId}`);
this.save(); this.save();
await renderMap(this); await renderMap(this);
}, },
@ -170,20 +168,20 @@ const root = {
const saveData = localStorage.getItem('birthday-spire-save'); const saveData = localStorage.getItem('birthday-spire-save');
if (saveData) { if (saveData) {
const data = JSON.parse(saveData); const data = JSON.parse(saveData);
// Validate essential save data // Validate essential save data
if (!data || typeof data !== 'object') { if (!data || typeof data !== 'object') {
throw new Error('Invalid save data format'); throw new Error('Invalid save data format');
} }
if (!data.player || typeof data.player !== 'object') { if (!data.player || typeof data.player !== 'object') {
throw new Error('Invalid player data'); throw new Error('Invalid player data');
} }
if (!data.nodeId || typeof data.nodeId !== 'string') { if (!data.nodeId || typeof data.nodeId !== 'string') {
throw new Error('Invalid node ID'); throw new Error('Invalid node ID');
} }
// Validate current act and ensure map exists // Validate current act and ensure map exists
const actId = data.currentAct || "act1"; const actId = data.currentAct || "act1";
if (!MAPS[actId]) { if (!MAPS[actId]) {
@ -192,9 +190,9 @@ const root = {
} else { } else {
this.currentAct = actId; this.currentAct = actId;
} }
this.map = MAPS[this.currentAct]; this.map = MAPS[this.currentAct];
// Validate that the nodeId exists in the current map // Validate that the nodeId exists in the current map
const nodeExists = this.map.nodes.some(n => n.id === data.nodeId); const nodeExists = this.map.nodes.some(n => n.id === data.nodeId);
if (!nodeExists) { if (!nodeExists) {
@ -203,7 +201,7 @@ const root = {
} else { } else {
this.nodeId = data.nodeId; this.nodeId = data.nodeId;
} }
// Validate player data has required fields // Validate player data has required fields
if (typeof data.player.hp !== 'number' || data.player.hp < 0) { if (typeof data.player.hp !== 'number' || data.player.hp < 0) {
throw new Error('Invalid player HP'); throw new Error('Invalid player HP');
@ -214,15 +212,15 @@ const root = {
if (!Array.isArray(data.player.deck)) { if (!Array.isArray(data.player.deck)) {
throw new Error('Invalid player deck'); throw new Error('Invalid player deck');
} }
this.player = data.player; this.player = data.player;
this.relicStates = Array.isArray(data.relicStates) ? data.relicStates : []; this.relicStates = Array.isArray(data.relicStates) ? data.relicStates : [];
this.completedNodes = Array.isArray(data.completedNodes) ? data.completedNodes : []; this.completedNodes = Array.isArray(data.completedNodes) ? data.completedNodes : [];
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);
this.restoreCardEffects(); this.restoreCardEffects();
this.log('Game loaded from save.'); this.log('Game loaded from save.');
return true; return true;
} }
@ -255,7 +253,7 @@ const root = {
clearSave() { clearSave() {
localStorage.removeItem('birthday-spire-save'); localStorage.removeItem('birthday-spire-save');
}, },
// Clear any old saves with outdated card IDs // Clear any old saves with outdated card IDs
clearOldSaves() { clearOldSaves() {
localStorage.removeItem('birthday-spire-save'); localStorage.removeItem('birthday-spire-save');
@ -295,21 +293,21 @@ async function initializeGame() {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const screenParam = urlParams.get('screen'); const screenParam = urlParams.get('screen');
const dev = urlParams.get('dev'); const dev = urlParams.get('dev');
// Check if it's ThePrimeagen's birthday yet (September 9, 2025) // Check if it's ThePrimeagen's birthday yet (September 9, 2025)
// Skip countdown if ?dev=true is in URL // Skip countdown if ?dev=true is in URL
const now = new Date(); const now = new Date();
const birthday = new Date('2025-09-09T00:00:00'); const birthday = new Date('2025-09-09T00:00:00');
if (now < birthday && dev !== 'true') { if (now < birthday && dev !== 'true') {
showCountdown(birthday); showCountdown(birthday);
return; return;
} }
if (screenParam) { if (screenParam) {
setupMockData(); setupMockData();
switch (screenParam.toLowerCase()) { switch (screenParam.toLowerCase()) {
case 'victory': case 'victory':
case 'win': case 'win':
@ -364,9 +362,9 @@ function setupMockData() {
root.player.hand = ['strike', 'coffee_rush', 'raw_dog']; root.player.hand = ['strike', 'coffee_rush', 'raw_dog'];
root.player.draw = ['defend', 'segfault']; root.player.draw = ['defend', 'segfault'];
root.player.discard = ['virgin']; root.player.discard = ['virgin'];
attachRelics(root, ['coffee_thermos', 'cpp_compiler']); attachRelics(root, ['coffee_thermos', 'cpp_compiler']);
// Test Act 2 if ?act2=true is in URL // Test Act 2 if ?act2=true is in URL
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('act2') === 'true') { if (urlParams.get('act2') === 'true') {
@ -424,24 +422,24 @@ function showCountdown(birthday) {
</div> </div>
</div> </div>
`; `;
// Start the countdown timer // Start the countdown timer
const timer = setInterval(() => { const timer = setInterval(() => {
const now = new Date(); const now = new Date();
const timeLeft = birthday - now; const timeLeft = birthday - now;
if (timeLeft <= 0) { if (timeLeft <= 0) {
clearInterval(timer); clearInterval(timer);
// Birthday reached! Reload to show the game // Birthday reached! Reload to show the game
window.location.reload(); window.location.reload();
return; return;
} }
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
document.getElementById('days').textContent = days.toString().padStart(2, '0'); document.getElementById('days').textContent = days.toString().padStart(2, '0');
document.getElementById('hours').textContent = hours.toString().padStart(2, '0'); document.getElementById('hours').textContent = hours.toString().padStart(2, '0');
document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0'); document.getElementById('minutes').textContent = minutes.toString().padStart(2, '0');

1263
src/ui/render.js

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save