Wire run loop with victory rewards, defeat restart, and combat chaining

This commit is contained in:
Jared Miller 2026-02-24 22:39:37 -05:00
parent 4e457b80af
commit 77f65ace98
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 132 additions and 51 deletions

View file

@ -34,7 +34,11 @@
<section id="hand"></section> <section id="hand"></section>
</div> </div>
<div id="overlay" hidden></div> <div id="overlay" hidden>
<div id="overlay-text"></div>
<div id="reward-cards"></div>
<button type="button" id="skip-btn" hidden>skip</button>
</div>
<script type="module" src="src/main.js"></script> <script type="module" src="src/main.js"></script>
</body> </body>

View file

@ -2,18 +2,76 @@ import { getCard, initCards } from "./cards.js";
import { checkCombatEnd, resolveEnemyTurn, startTurn } from "./combat.js"; import { checkCombatEnd, resolveEnemyTurn, startTurn } from "./combat.js";
import { initEnemies } from "./enemies.js"; import { initEnemies } from "./enemies.js";
import { render } from "./render.js"; import { render } from "./render.js";
import { createCombatState, endTurn, playCard } from "./state.js"; import {
createRunState,
pickReward,
revealRewards,
skipRewards,
} from "./run.js";
import { createCombatFromRun, endTurn, playCard } from "./state.js";
let state = null; let state = null;
let run = null;
let revealed = null;
async function init() { async function init() {
await Promise.all([initCards(), initEnemies()]); await Promise.all([initCards(), initEnemies()]);
state = createCombatState("ironclad", "jaw_worm"); startNewRun();
state = startTurn(state);
render(state);
bindEvents(); bindEvents();
} }
function startNewRun() {
run = createRunState("ironclad");
revealed = null;
startNextCombat();
}
function startNextCombat() {
run = { ...run, combatCount: run.combatCount + 1 };
state = createCombatFromRun(run, "jaw_worm");
state = startTurn(state);
revealed = null;
render(state, revealed);
}
function syncRunHp() {
run = { ...run, hp: state.players[0].hp };
}
function handleVictory() {
syncRunHp();
const result = revealRewards(run);
revealed = result.revealed;
run = result.run;
state = {
...state,
combat: { ...state.combat, phase: "rewards" },
};
render(state, revealed);
}
function handleDefeat() {
syncRunHp();
state = {
...state,
combat: { ...state.combat, phase: "ended", result: "defeat" },
};
render(state, revealed);
}
function checkEnd() {
const end = checkCombatEnd(state);
if (end === "victory") {
handleVictory();
return true;
}
if (end === "defeat") {
handleDefeat();
return true;
}
return false;
}
function bindEvents() { function bindEvents() {
document.getElementById("hand").addEventListener("click", (e) => { document.getElementById("hand").addEventListener("click", (e) => {
const cardEl = e.target.closest(".card"); const cardEl = e.target.closest(".card");
@ -22,7 +80,7 @@ function bindEvents() {
if (state.combat.selectedCard === index) { if (state.combat.selectedCard === index) {
state = { ...state, combat: { ...state.combat, selectedCard: null } }; state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state); render(state, revealed);
return; return;
} }
@ -32,27 +90,18 @@ function bindEvents() {
const card = getCard(cardId); const card = getCard(cardId);
if (card.type === "skill") { if (card.type === "skill") {
// auto-play skills (they target self)
const result = playCard(state, index); const result = playCard(state, index);
if (result === null) { if (result === null) {
// not enough energy
state = { ...state, combat: { ...state.combat, selectedCard: null } }; state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state); render(state, revealed);
return; return;
} }
state = { ...result, combat: { ...result.combat, selectedCard: null } }; state = { ...result, combat: { ...result.combat, selectedCard: null } };
const end = checkCombatEnd(state); if (!checkEnd()) render(state, revealed);
if (end) {
state = {
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
}
render(state);
return; return;
} }
render(state); render(state, revealed);
}); });
document.getElementById("enemy-zone").addEventListener("click", () => { document.getElementById("enemy-zone").addEventListener("click", () => {
@ -62,23 +111,12 @@ function bindEvents() {
const result = playCard(state, state.combat.selectedCard); const result = playCard(state, state.combat.selectedCard);
if (result === null) { if (result === null) {
state = { ...state, combat: { ...state.combat, selectedCard: null } }; state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state); render(state, revealed);
return; return;
} }
state = { ...result, combat: { ...result.combat, selectedCard: null } }; state = { ...result, combat: { ...result.combat, selectedCard: null } };
if (!checkEnd()) render(state, revealed);
const end = checkCombatEnd(state);
if (end) {
state = {
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
render(state);
return;
}
render(state);
}); });
document document
@ -87,24 +125,41 @@ function bindEvents() {
if (state.combat.phase !== "player_turn") return; if (state.combat.phase !== "player_turn") return;
state = endTurn(state); state = endTurn(state);
render(state); render(state, revealed);
await delay(800); await delay(800);
state = resolveEnemyTurn(state); state = resolveEnemyTurn(state);
const end = checkCombatEnd(state); if (!checkEnd()) {
if (end) { state = startTurn(state);
state = { render(state, revealed);
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
render(state);
return;
} }
state = startTurn(state);
render(state);
}); });
document.getElementById("reward-cards").addEventListener("click", (e) => {
const img = e.target.closest(".reward-card");
if (!img || !revealed) return;
const cardId = img.dataset.cardId;
const index = revealed.indexOf(cardId);
if (index === -1) return;
run = pickReward(run, revealed, index);
startNextCombat();
});
document.getElementById("skip-btn").addEventListener("click", () => {
if (!revealed) return;
run = skipRewards(run, revealed);
startNextCombat();
});
document.getElementById("overlay").addEventListener("click", (e) => {
// only restart on defeat overlay click (not reward cards or skip btn)
if (state.combat.phase !== "ended" || state.combat.result !== "defeat")
return;
if (e.target.closest("#reward-cards") || e.target.closest("#skip-btn"))
return;
startNewRun();
});
} }
function delay(ms) { function delay(ms) {

View file

@ -1,11 +1,11 @@
import { getCard } from "./cards.js"; import { getCard } from "./cards.js";
import { resolveEnemyAction } from "./enemies.js"; import { resolveEnemyAction } from "./enemies.js";
export function render(state) { export function render(state, revealed) {
renderEnemy(state); renderEnemy(state);
renderInfoBar(state); renderInfoBar(state);
renderHand(state); renderHand(state);
renderOverlay(state); renderOverlay(state, revealed);
} }
function renderEnemy(state) { function renderEnemy(state) {
@ -103,16 +103,38 @@ function renderHand(state) {
}); });
} }
function renderOverlay(state) { function renderOverlay(state, revealed) {
const overlay = document.getElementById("overlay"); const overlay = document.getElementById("overlay");
const result = state.combat.phase === "ended" ? state.combat.result : null; const overlayText = document.getElementById("overlay-text");
if (result === "victory") { const rewardCards = document.getElementById("reward-cards");
const skipBtn = document.getElementById("skip-btn");
if (state.combat.phase === "rewards" && revealed) {
overlay.hidden = false; overlay.hidden = false;
overlay.textContent = "victory"; overlayText.textContent = "card reward";
} else if (result === "defeat") { rewardCards.innerHTML = "";
for (const cardId of revealed) {
const card = getCard(cardId);
const img = document.createElement("img");
img.src = card.image || "";
img.alt = card.name;
img.title = `${card.name} (${card.cost}) - ${card.description}`;
img.dataset.cardId = cardId;
img.className = "reward-card";
rewardCards.appendChild(img);
}
skipBtn.hidden = false;
} else if (state.combat.phase === "ended") {
overlay.hidden = false; overlay.hidden = false;
overlay.textContent = "defeat"; overlayText.textContent =
state.combat.result === "defeat"
? "defeat — click to restart"
: "victory";
rewardCards.innerHTML = "";
skipBtn.hidden = true;
} else { } else {
overlay.hidden = true; overlay.hidden = true;
rewardCards.innerHTML = "";
skipBtn.hidden = true;
} }
} }