From e4a8b31bc5e4b926dd794669cc46af9733d87fe1 Mon Sep 17 00:00:00 2001 From: Stephanie Gredell Date: Fri, 5 Sep 2025 17:05:53 -0700 Subject: [PATCH] refactor --- src/data/cards.js | 63 ++++++++++++--- src/engine/battle.js | 23 ++++-- src/engine/core.js | 35 ++++++++ src/input/InputManager.js | 164 +++++++++++++++++++++++++++++++++++++ src/main.js | 8 +- src/ui/render.js | 165 ++++++++++++++------------------------ style.css | 131 ++++++++++++++++++++++++++++++ 7 files changed, 464 insertions(+), 125 deletions(-) diff --git a/src/data/cards.js b/src/data/cards.js index 5fe338d..f503713 100644 --- a/src/data/cards.js +++ b/src/data/cards.js @@ -130,9 +130,15 @@ export const CARDS = { ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); if (prevHp > 0 && ctx.enemy.hp <= 0) { ctx.log("Recursion activates and strikes again!"); - ctx.enemy.hp = 1; ctx.deal(ctx.enemy, ctx.scalarFromWeak(5)); + + // Check for battle end after second attack + if (ctx.enemy.hp <= 0) { + ctx.enemy.hp = 0; + ctx.onWin(); + return; + } } } }, @@ -156,8 +162,44 @@ 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."); + const topCards = ctx.peekTop(3); + if (topCards.length === 0) { + ctx.log("No cards left in deck to review."); + return; + } + + // Store selection state for modal + ctx.root._codeReviewCards = topCards; + ctx.root._codeReviewCallback = (selectedIndex) => { + // Get the selected card + const selectedCard = topCards[selectedIndex]; + + // Remove the peeked cards from draw pile (they were only peeked) + topCards.forEach((card, i) => { + const drawIndex = ctx.root.player.draw.findIndex(id => id === card.id); + if (drawIndex >= 0) { + ctx.root.player.draw.splice(drawIndex, 1); + } + }); + + // Add selected card to hand + ctx.addToHand(selectedCard); + + // Put remaining cards on bottom of deck + topCards.forEach((card, i) => { + if (i !== selectedIndex) { + ctx.putOnBottom(card.id); + } + }); + + ctx.log(`Code review complete. Added ${selectedCard.name} to hand.`); + ctx.render(); + }; + + // Show selection modal + if (window.gameModules?.render?.renderCodeReviewSelection) { + window.gameModules.render.renderCodeReviewSelection(ctx.root, topCards); + } } }, @@ -292,15 +334,12 @@ export const CARDS = { art: "Monk_34.png", effect: (ctx) => { ctx.deal(ctx.enemy, ctx.scalarFromWeak(15)); - if (ctx.player.hand.length > 1) { // Don't remove this card itself - const otherCards = ctx.player.hand.filter(c => c.id !== "git_push_force"); - if (otherCards.length > 0) { - const randomCard = otherCards[Math.floor(Math.random() * otherCards.length)]; - const handIdx = ctx.player.hand.findIndex(c => c === randomCard); - const [card] = ctx.player.hand.splice(handIdx, 1); - ctx.player.draw.push(card.id); - ctx.log(`${card.name} was force-pushed back to your deck!`); - } + if (ctx.player.hand.length > 0) { // Check if there are any cards left in hand + const randomCard = ctx.player.hand[Math.floor(Math.random() * ctx.player.hand.length)]; + const handIdx = ctx.player.hand.findIndex(c => c === randomCard); + const [card] = ctx.player.hand.splice(handIdx, 1); + ctx.player.draw.push(card.id); + ctx.log(`${card.name} was force-pushed back to your deck!`); } } }, diff --git a/src/engine/battle.js b/src/engine/battle.js index f39a658..67e9c78 100644 --- a/src/engine/battle.js +++ b/src/engine/battle.js @@ -1,7 +1,7 @@ import { ENEMIES } from "../data/enemies.js"; import { RELICS } from "../data/relics.js"; import { CARDS } from "../data/cards.js"; -import { draw, endTurnDiscard, clamp, cloneCard, shuffle } from "./core.js"; +import { draw, endTurnDiscard, clamp, cloneCard, shuffle, peekTopCards, putCardOnBottomOfDeck, addCardToHand } from "./core.js"; export function createBattle(ctx, enemyId) { const enemyData = ENEMIES[enemyId]; @@ -74,6 +74,12 @@ export function playCard(ctx, handIndex) { ctx.player.energy -= actualCost; ctx.lastCard = card.id; + // Remove the played card from hand BEFORE running effect to prevent index shifting issues + let usedCard = null; + if (card.type !== "power") { + const [removed] = ctx.player.hand.splice(handIndex, 1); + usedCard = removed; + } const prevDeal = ctx.deal; ctx.deal = (target, amount) => { @@ -108,13 +114,12 @@ export function playCard(ctx, handIndex) { return; } - - if (card.type !== "power") { - const [used] = ctx.player.hand.splice(handIndex, 1); + // Handle card disposal after effect (if it was removed from hand) + if (usedCard) { if (!card.exhaust) { - ctx.player.discard.push(used.id); + ctx.player.discard.push(usedCard.id); } else { - ctx.log(`${used.name} is exhausted and removed from the fight.`); + ctx.log(`${usedCard.name} is exhausted and removed from the fight.`); } } @@ -167,6 +172,7 @@ export function enemyTurn(ctx) { if (ctx.player.hp <= 0) { ctx.onLose(); return; } + if (ctx.enemy.hp <= 0) { ctx.enemy.hp = 0; ctx.onWin(); return; } e.turn++; try { @@ -235,6 +241,7 @@ export function makeBattleContext(root) { showDamageNumber: root.showDamageNumber, lastCard: null, flags: {}, + root: root, // Provide access to root for complex card effects // New mechanics for advanced cards moveFromDiscardToHand: (cardId) => { const idx = root.player.discard.findIndex(id => id === cardId); @@ -249,6 +256,10 @@ export function makeBattleContext(root) { } return false; }, + // Code Review card mechanics + peekTop: (n) => peekTopCards(root.player, n), + putOnBottom: (cardId) => putCardOnBottomOfDeck(root.player, cardId), + addToHand: (card) => addCardToHand(root.player, card), countCardType: (type) => { const allCards = [...root.player.deck, ...root.player.hand.map(c => c.id), ...root.player.draw, ...root.player.discard]; return allCards.filter(id => CARDS[id]?.type === type).length; diff --git a/src/engine/core.js b/src/engine/core.js index 17f4944..87668a7 100644 --- a/src/engine/core.js +++ b/src/engine/core.js @@ -46,6 +46,41 @@ export function endTurnDiscard(player) { player.energy = player.maxEnergy; } +// Peek at top N cards from draw pile without removing them +export function peekTopCards(player, n = 3) { + const cards = []; + const drawPile = [...player.draw]; // Copy to avoid modifying original + + for (let i = 0; i < n && i < drawPile.length; i++) { + const cardId = drawPile[drawPile.length - 1 - i]; // Peek from top (end of array) + const originalCard = CARDS[cardId]; + if (originalCard) { + const clonedCard = cloneCard(originalCard); + clonedCard._drawIndex = drawPile.length - 1 - i; // Store original position + cards.push(clonedCard); + } + } + + return cards; +} + +// Put a specific card on the bottom of the draw pile +export function putCardOnBottomOfDeck(player, cardId) { + // Remove from draw pile first (if it's there) + const drawIndex = player.draw.findIndex(id => id === cardId); + if (drawIndex >= 0) { + player.draw.splice(drawIndex, 1); + } + + // Add to bottom of draw pile (beginning of array) + player.draw.unshift(cardId); +} + +// Add a specific card object to hand +export function addCardToHand(player, card) { + player.hand.push(card); +} + export function cloneCard(c) { if (!c) { diff --git a/src/input/InputManager.js b/src/input/InputManager.js index 2dd87c0..d05df91 100644 --- a/src/input/InputManager.js +++ b/src/input/InputManager.js @@ -46,6 +46,19 @@ export class InputManager { this.handleEscapeKey(event); } + // Handle number keys for code review selection + if (this.root._codeReviewCards && event.key >= '1' && event.key <= '3') { + const selectedIndex = parseInt(event.key, 10) - 1; + if (selectedIndex < this.root._codeReviewCards.length) { + event.preventDefault(); + if (this.root._codeReviewCallback) { + this.root._codeReviewCallback(selectedIndex); + this.root._codeReviewCards = null; + this.root._codeReviewCallback = null; + } + } + } + // Add other global shortcuts here as needed } @@ -103,12 +116,30 @@ export class InputManager { return; } + const buyRelicElement = target.closest('[data-buy-relic]'); + if (buyRelicElement) { + this.handleShopRelicBuy(buyRelicElement, event); + return; + } + + const leaveElement = target.closest('[data-leave]'); + if (leaveElement) { + this.handleLeaveShop(leaveElement, event); + return; + } + const relicElement = target.closest('[data-relic]'); if (relicElement) { this.handleRelicSelection(relicElement, event); return; } + const codeReviewElement = target.closest('[data-code-review-pick]'); + if (codeReviewElement) { + this.handleCodeReviewPick(codeReviewElement, event); + return; + } + // Check for direct data attributes on target (fallback) if (target.dataset.node !== undefined) { this.handleMapNodeClick(target, event); @@ -234,12 +265,116 @@ export class InputManager { if (goldDisplay) { goldDisplay.textContent = this.root.player.gold; } + + // Update affordability of remaining items + this.updateShopAffordability(); + + // Save immediately to persist purchase + this.root.save(); } else { this.root.log("Not enough gold!"); } } } + /** + * Handle shop relic purchases + */ + handleShopRelicBuy(element, event) { + if (this.root.currentShopRelic) { + const relic = this.root.currentShopRelic; + if (this.root.player.gold >= 100) { + this.root.player.gold -= 100; + this.root.log(`Bought ${relic.name} for 100 gold.`); + + // Attach the relic + import("../engine/battle.js").then(({ attachRelics }) => { + const currentRelicIds = this.root.relicStates.map(r => r.id); + const newRelicIds = [...currentRelicIds, relic.id]; + attachRelics(this.root, newRelicIds); + }); + + element.disabled = true; + element.textContent = "SOLD"; + + // Update gold display + const goldDisplay = this.root.app.querySelector('.gold-amount'); + if (goldDisplay) { + goldDisplay.textContent = this.root.player.gold; + } + + // Update affordability of remaining items + this.updateShopAffordability(); + + // Save immediately to persist purchase + this.root.save(); + } else { + this.root.log("Not enough gold!"); + } + } + } + + /** + * Update shop item affordability + */ + updateShopAffordability() { + // Update card affordability + this.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 (this.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 = 'Need 50 gold'; + cardContainer.appendChild(newOverlay); + } + } else { + btn.classList.remove('unplayable'); + btn.classList.add('playable'); + if (overlay) { + overlay.remove(); + } + } + } + }); + + // Update relic affordability + const relicBtn = this.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 (this.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 = 'Need 100 gold'; + relicContainer.appendChild(newOverlay); + } + } else { + relicBtn.classList.remove('unaffordable'); + relicBtn.classList.add('affordable'); + if (overlay) { + overlay.remove(); + } + } + } + } + + /** + * Handle leaving the shop + */ + handleLeaveShop(element, event) { + this.root.afterNode(); + } + /** * Handle relic selection */ @@ -248,6 +383,26 @@ export class InputManager { this.root.selectStartingRelic(relicId); } + /** + * Handle code review card selection + */ + handleCodeReviewPick(element, event) { + const selectedIndex = parseInt(element.dataset.codeReviewPick, 10); + + if (this.root._codeReviewCallback && this.root._codeReviewCards) { + try { + // Execute the callback with selected index + this.root._codeReviewCallback(selectedIndex); + + // Clean up state + this.root._codeReviewCards = null; + this.root._codeReviewCallback = null; + } catch (error) { + console.error('Error handling code review selection:', error); + } + } + } + /** * Handle action buttons (like show-messages, end) */ @@ -373,6 +528,15 @@ export class InputManager { * Handle Escape key presses */ handleEscapeKey(event) { + // Handle code review modal cancellation + if (this.root._codeReviewCards) { + this.root._codeReviewCards = null; + this.root._codeReviewCallback = null; + // Return to battle without making a choice + this.root.render(); + return; + } + // Close any open modals const modals = document.querySelectorAll('.messages-modal-overlay'); modals.forEach(modal => modal.remove()); diff --git a/src/main.js b/src/main.js index e313a31..e44e762 100644 --- a/src/main.js +++ b/src/main.js @@ -2,9 +2,9 @@ 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 { makePlayer, initDeck, draw, peekTopCards, putCardOnBottomOfDeck, addCardToHand } 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, renderUpgrade, updateCardSelection, showDamageNumber } 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 { CommandInvoker } from "./commands/CommandInvoker.js"; @@ -23,6 +23,8 @@ const root = { currentEvent: null, // For event handling currentShopCards: null, // For shop handling currentShopRelic: null, // For shop relic handling + _codeReviewCards: null, // For code review card selection + _codeReviewCallback: null, // For code review completion log(m) { this.logs.push(m); this.logs = this.logs.slice(-200); }, async render() { await renderBattle(this); }, @@ -556,7 +558,7 @@ root.inputManager.initGlobalListeners(); // Make modules available globally for InputManager window.gameModules = { cards: { CARDS }, - render: { renderMap, renderUpgrade, updateCardSelection } + render: { renderMap, renderUpgrade, updateCardSelection, renderCodeReviewSelection } }; initializeGame(); diff --git a/src/ui/render.js b/src/ui/render.js index 1380a9b..040a8a4 100644 --- a/src/ui/render.js +++ b/src/ui/render.js @@ -906,6 +906,10 @@ export function renderShop(root) { const availableRelics = START_RELIC_CHOICES.filter(id => !ownedRelicIds.includes(id)); const shopRelic = availableRelics.length > 0 ? RELICS[availableRelics[0]] : null; + // Store shop cards for InputManager access + root.currentShopCards = shopCards; + root.currentShopRelic = shopRelic; + root.app.innerHTML = `
@@ -996,62 +1000,11 @@ export function renderShop(root) { 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"; + // Note: Card purchase events are now handled by InputManager - // 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()); + // Note: Shop purchase events are now handled by InputManager + // Note: Leave shop event is handled by InputManager }); }); } @@ -1071,56 +1024,7 @@ export function updateCardSelection(root) { } } -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 = 'Need 50 gold'; - 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 = 'Need 100 gold'; - relicContainer.appendChild(newOverlay); - } - } else { - relicBtn.classList.remove('unaffordable'); - relicBtn.classList.add('affordable'); - if (overlay) { - overlay.remove(); - } - } - } -} +// updateShopAffordability function moved to InputManager function shuffle(array) { for (let i = array.length - 1; i > 0; i--) { @@ -1553,6 +1457,59 @@ export async function renderWin(root) { root.app.querySelector("[data-replay]").addEventListener("click", () => root.reset()); } +export async function renderCodeReviewSelection(root, cards) { + const { CARDS } = await import("../data/cards.js"); + + if (!cards || cards.length === 0) { + root.log("No cards available for code review."); + return; + } + + root.app.innerHTML = ` +
+
+
+

🔍 Code Review

+

Choose 1 card to add to your hand. The rest will go to the bottom of your deck.

+
+ +
+ ${cards.map((card, index) => { + const cardType = card.type === 'attack' ? 'attack' : card.type === 'skill' ? 'skill' : 'power'; + return ` +
+
+
+
+
+
${card.name}
+
${card.cost}
+
+ +
+
${getCardArt(card.id, CARDS)}
+
${card.type}
+
+ +
+
${card.text}
+
+
+
+
Click to choose
+
+ `; + }).join('')} +
+ + +
+
+ `; +} + export async function renderLose(root) { const { RELICS } = await import("../data/relics.js"); const finalStats = { diff --git a/style.css b/style.css index d9437c2..5a4b6b3 100644 --- a/style.css +++ b/style.css @@ -2116,6 +2116,7 @@ h3 { display: flex; align-items: center; justify-content: center; + pointer-events: none; /* Allow clicks to pass through to elements behind */ color: #dc3545; font-size: 12px; font-weight: bold; @@ -4210,6 +4211,7 @@ h3 { align-items: center; justify-content: center; border-radius: 16px; + pointer-events: none; /* Allow clicks to pass through to elements behind */ color: #ff6b6b; font-weight: bold; font-size: 14px; @@ -5825,3 +5827,132 @@ h3 { padding: 15px; } } + +/* Code Review Modal Styles */ +.code-review-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(5px); +} + +.code-review-modal { + background: linear-gradient(145deg, #2a2a2a, #1a1a1a); + border: 2px solid #4a4a4a; + border-radius: 15px; + padding: 30px; + max-width: 900px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); +} + +.code-review-header { + text-align: center; + margin-bottom: 30px; +} + +.code-review-header h2 { + color: #fff; + font-size: 1.8em; + margin: 0 0 10px 0; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +.code-review-header p { + color: #ccc; + font-size: 1.1em; + margin: 0; +} + +.code-review-cards-container { + display: flex; + gap: 20px; + justify-content: center; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.code-review-card { + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + padding: 10px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.05); + border: 2px solid transparent; +} + +.code-review-card:hover { + transform: translateY(-5px) scale(1.02); + box-shadow: 0 8px 25px rgba(0, 150, 255, 0.3); + border-color: #4a9eff; + background: rgba(74, 158, 255, 0.1); +} + +.code-review-card .battle-card { + transform: scale(0.9); + margin-bottom: 10px; +} + +.code-review-card-label { + color: #ccc; + font-size: 0.9em; + text-align: center; + padding: 5px 10px; + background: rgba(0, 0, 0, 0.3); + border-radius: 15px; + border: 1px solid #666; + transition: all 0.2s ease; +} + +.code-review-card:hover .code-review-card-label { + background: rgba(74, 158, 255, 0.2); + border-color: #4a9eff; + color: #fff; +} + +.code-review-footer { + text-align: center; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #444; +} + +.code-review-footer p { + color: #888; + font-size: 0.9em; + margin: 0; +} + +/* Responsive adjustments for code review modal */ +@media (max-width: 768px) { + .code-review-modal { + padding: 20px; + width: 95%; + max-height: 90vh; + } + + .code-review-cards-container { + flex-direction: column; + align-items: center; + } + + .code-review-card .battle-card { + transform: scale(0.8); + } + + .code-review-header h2 { + font-size: 1.5em; + } +}