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 @@ @@ -1,5 +1,3 @@
export const CARDS = {
strike: {
id: "strike", name: "Strike", cost: 1, type: "attack", text: "Deal 6.", target: "enemy",
@ -158,7 +156,6 @@ export const CARDS = { @@ -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.",
art: "Monk_24.png",
effect: (ctx) => {
ctx.draw(1);
ctx.log("Code review reveals useful insights. You draw a card.");
}
@ -338,7 +335,7 @@ export const CARDS = { @@ -338,7 +335,7 @@ export const CARDS = {
export const STARTER_DECK = [
"strike", "strike", "defend", "defend",
"segfault", "coffee_rush", "skill_issue", "git_commit",
"stack_trace", "raw_dog"
"stack_trace", "stack_trace"
];
export const CARD_POOL = [

53
src/data/enemies.js

@ -22,8 +22,15 @@ export const ENEMIES = { @@ -22,8 +22,15 @@ export const ENEMIES = {
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: {
id: "nightshadedude", name: "Nightshadedude", maxHp: 85,
id: "nightshadedude", name: "Nightshadedude", maxHp: 120,
avatar: "assets/avatars/11.png", // Powerful demon/witch
background: "assets/backgrounds/dead forest.png", // Repeat background
ai: (turn) => turn % 3 === 1 ? { type: "debuff", value: 1 } : { type: "attack", value: 14 },
@ -57,28 +64,28 @@ export const ENEMIES = { @@ -57,28 +64,28 @@ export const ENEMIES = {
},
// ACT 2 ENEMIES - Harder versions
senior_dev: {
id: "senior_dev", name: "Senior Dev", maxHp: 65,
teej: {
id: "teej", name: "Teej", maxHp: 65,
avatar: "assets/avatars/elite_ts_demon.png",
background: "assets/backgrounds/castle.png",
ai: (turn) => turn % 3 === 0 ? { type: "debuff", value: 2 } : { type: "attack", value: turn % 2 === 0 ? 12 : 14 },
onDebuff: (ctx) => ctx.applyWeak(ctx.player, 2)
},
tech_lead: {
id: "tech_lead", name: "Tech Lead", maxHp: 80,
begin: {
id: "begin", name: "Begin", maxHp: 80,
avatar: "assets/avatars/infinite_loop.png",
background: "assets/backgrounds/dead forest.png",
ai: (turn) => (turn % 2 === 0) ? { type: "attack", value: 16 } : { type: "block", value: 12 }
},
code_reviewer: {
id: "code_reviewer", name: "Code Reviewer", maxHp: 70,
adam: {
id: "adam", name: "Adam", maxHp: 70,
avatar: "assets/avatars/chat_gremlin.png",
background: "assets/backgrounds/terrace.png",
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: {
id: "scrum_master", name: "Scrum Master", maxHp: 90,
david: {
id: "david", name: "David", maxHp: 90,
avatar: "assets/avatars/js_blob.png",
background: "assets/backgrounds/castle.png",
ai: (turn) => {
@ -87,10 +94,10 @@ export const ENEMIES = { @@ -87,10 +94,10 @@ export const ENEMIES = {
if (cyc === 1) return { type: "attack", value: 11 };
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: {
id: "architect", name: "The Architect", maxHp: 150,
dax: {
id: "dax", name: "Dax", maxHp: 150,
avatar: "assets/avatars/bug_404.png",
background: "assets/backgrounds/throne room.png",
ai: (turn) => {
@ -101,7 +108,23 @@ export const ENEMIES = { @@ -101,7 +108,23 @@ export const ENEMIES = {
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("The Architect 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!"); }
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("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 = { @@ -14,27 +14,27 @@ export const MAPS = {
{ id: "n9", kind: "rest", next: ["n11"], x: 350, 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: "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: "n14", kind: "boss", enemy: "teej", next: [], x: 500, y: 40 },
{ id: "n14", kind: "boss", enemy: "nightshadedude", next: [], x: 500, y: 40 },
]
},
act2: {
id: "act2", name: "Birthday Spire — Act II: The Corporate Ladder",
nodes: [
{ 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: "a2n3", kind: "event", next: ["a2n5"], x: 600, y: 680 },
{ id: "a2n4", kind: "shop", next: ["a2n6"], x: 300, y: 600 },
{ id: "a2n5", kind: "battle", enemy: "tech_lead", next: ["a2n6", "a2n7"], x: 500, y: 600 },
{ id: "a2n6", kind: "battle", enemy: "code_reviewer", next: ["a2n8"], x: 400, y: 520 },
{ id: "a2n7", kind: "rest", next: ["a2n8"], x: 600, y: 520 },
{ id: "a2n8", kind: "battle", enemy: "scrum_master", next: ["a2n9", "a2n10"], x: 500, y: 440 },
{ id: "a2n9", kind: "event", next: ["a2n11"], x: 350, y: 360 },
{ id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 360 },
{ id: "a2n11", kind: "elite", enemy: "senior_dev", next: ["a2n12"], x: 500, y: 280 },
{ id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 200 },
{ id: "a2n13", kind: "boss", enemy: "architect", next: [], x: 500, y: 120 },
{ id: "a2n2", kind: "battle", enemy: "teej", next: ["a2n5"], x: 300, y: 695 },
{ id: "a2n3", kind: "event", next: ["a2n5"], x: 650, y: 695 },
{ id: "a2n5", kind: "battle", enemy: "begin", next: ["a2n4", "a2n7"], x: 500, y: 600 },
{ id: "a2n4", kind: "shop", next: ["a2n6"], x: 350, y: 525 },
{ id: "a2n7", kind: "rest", next: ["a2n6"], x: 650, y: 525 },
{ id: "a2n6", kind: "battle", enemy: "adam", next: ["a2n8"], x: 500, y: 460 },
{ id: "a2n8", kind: "battle", enemy: "david", next: ["a2n9", "a2n10"], x: 500, y: 360 },
{ id: "a2n9", kind: "rest", next: ["a2n11"], x: 350, y: 320 },
{ id: "a2n10", kind: "shop", next: ["a2n11"], x: 650, y: 320 },
{ id: "a2n11", kind: "elite", enemy: "dax", next: ["a2n12"], x: 500, y: 250 },
{ id: "a2n12", kind: "rest", next: ["a2n13"], x: 500, y: 140 },
{ id: "a2n13", kind: "boss", enemy: "taylor", next: [], x: 500, y: 40 },
]
}
};

68
src/main.js

@ -28,7 +28,7 @@ const root = { @@ -28,7 +28,7 @@ const root = {
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;
@ -53,7 +53,7 @@ const root = { @@ -53,7 +53,7 @@ const root = {
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") {
@ -79,10 +79,10 @@ const root = { @@ -79,10 +79,10 @@ const root = {
this.save();
await renderMap(this);
},
async skipReward() {
this._pendingChoices = null;
async skipReward() {
this._pendingChoices = null;
this.save();
await renderMap(this);
await renderMap(this);
},
async onWin() {
@ -91,12 +91,12 @@ const root = { @@ -91,12 +91,12 @@ const root = {
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") {
if (node.kind === "boss") {
// Check if there's a next act
const nextAct = this.currentAct === "act1" ? "act2" : null;
if (nextAct && MAPS[nextAct]) {
@ -112,20 +112,19 @@ const root = { @@ -112,20 +112,19 @@ const root = {
// Final victory
this.save(); // Save progress before clearing on victory
this.clearSave(); // Clear save on victory
await renderWin(this);
await renderWin(this);
}
}
else {
this.save();
this.afterNode();
}
},
async onLose() {
async onLose() {
this._battleInProgress = false;
this.clearSave(); // Clear save on defeat
await renderLose(this);
await renderLose(this);
},
reset() {
@ -142,7 +141,6 @@ const root = { @@ -142,7 +141,6 @@ const root = {
async selectStartingRelic(relicId) {
attachRelics(this, [relicId]);
this.log(`Selected starting relic: ${relicId}`);
this.save();
await renderMap(this);
},
@ -170,20 +168,20 @@ const root = { @@ -170,20 +168,20 @@ const root = {
const saveData = localStorage.getItem('birthday-spire-save');
if (saveData) {
const data = JSON.parse(saveData);
// Validate essential save data
if (!data || typeof data !== 'object') {
throw new Error('Invalid save data format');
}
if (!data.player || typeof data.player !== 'object') {
throw new Error('Invalid player data');
}
if (!data.nodeId || typeof data.nodeId !== 'string') {
throw new Error('Invalid node ID');
}
// Validate current act and ensure map exists
const actId = data.currentAct || "act1";
if (!MAPS[actId]) {
@ -192,9 +190,9 @@ const root = { @@ -192,9 +190,9 @@ const root = {
} else {
this.currentAct = actId;
}
this.map = MAPS[this.currentAct];
// Validate that the nodeId exists in the current map
const nodeExists = this.map.nodes.some(n => n.id === data.nodeId);
if (!nodeExists) {
@ -203,7 +201,7 @@ const root = { @@ -203,7 +201,7 @@ const root = {
} else {
this.nodeId = data.nodeId;
}
// Validate player data has required fields
if (typeof data.player.hp !== 'number' || data.player.hp < 0) {
throw new Error('Invalid player HP');
@ -214,15 +212,15 @@ const root = { @@ -214,15 +212,15 @@ const root = {
if (!Array.isArray(data.player.deck)) {
throw new Error('Invalid player deck');
}
this.player = data.player;
this.relicStates = Array.isArray(data.relicStates) ? data.relicStates : [];
this.completedNodes = Array.isArray(data.completedNodes) ? data.completedNodes : [];
this.logs = Array.isArray(data.logs) ? data.logs : [];
this._battleInProgress = Boolean(data.battleInProgress);
this.restoreCardEffects();
this.log('Game loaded from save.');
return true;
}
@ -255,7 +253,7 @@ const root = { @@ -255,7 +253,7 @@ const root = {
clearSave() {
localStorage.removeItem('birthday-spire-save');
},
// Clear any old saves with outdated card IDs
clearOldSaves() {
localStorage.removeItem('birthday-spire-save');
@ -295,21 +293,21 @@ async function initializeGame() { @@ -295,21 +293,21 @@ async 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');
if (now < birthday && dev !== 'true') {
showCountdown(birthday);
return;
}
if (screenParam) {
setupMockData();
switch (screenParam.toLowerCase()) {
case 'victory':
case 'win':
@ -364,9 +362,9 @@ function setupMockData() { @@ -364,9 +362,9 @@ function setupMockData() {
root.player.hand = ['strike', 'coffee_rush', 'raw_dog'];
root.player.draw = ['defend', 'segfault'];
root.player.discard = ['virgin'];
attachRelics(root, ['coffee_thermos', 'cpp_compiler']);
// Test Act 2 if ?act2=true is in URL
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('act2') === 'true') {
@ -424,24 +422,24 @@ function showCountdown(birthday) { @@ -424,24 +422,24 @@ function showCountdown(birthday) {
</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');

1263
src/ui/render.js

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