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>
</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>
</body>

View file

@ -2,18 +2,76 @@ import { getCard, initCards } from "./cards.js";
import { checkCombatEnd, resolveEnemyTurn, startTurn } from "./combat.js";
import { initEnemies } from "./enemies.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 run = null;
let revealed = null;
async function init() {
await Promise.all([initCards(), initEnemies()]);
state = createCombatState("ironclad", "jaw_worm");
state = startTurn(state);
render(state);
startNewRun();
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() {
document.getElementById("hand").addEventListener("click", (e) => {
const cardEl = e.target.closest(".card");
@ -22,7 +80,7 @@ function bindEvents() {
if (state.combat.selectedCard === index) {
state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state);
render(state, revealed);
return;
}
@ -32,27 +90,18 @@ function bindEvents() {
const card = getCard(cardId);
if (card.type === "skill") {
// auto-play skills (they target self)
const result = playCard(state, index);
if (result === null) {
// not enough energy
state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state);
render(state, revealed);
return;
}
state = { ...result, combat: { ...result.combat, selectedCard: null } };
const end = checkCombatEnd(state);
if (end) {
state = {
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
}
render(state);
if (!checkEnd()) render(state, revealed);
return;
}
render(state);
render(state, revealed);
});
document.getElementById("enemy-zone").addEventListener("click", () => {
@ -62,23 +111,12 @@ function bindEvents() {
const result = playCard(state, state.combat.selectedCard);
if (result === null) {
state = { ...state, combat: { ...state.combat, selectedCard: null } };
render(state);
render(state, revealed);
return;
}
state = { ...result, combat: { ...result.combat, selectedCard: null } };
const end = checkCombatEnd(state);
if (end) {
state = {
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
render(state);
return;
}
render(state);
if (!checkEnd()) render(state, revealed);
});
document
@ -87,24 +125,41 @@ function bindEvents() {
if (state.combat.phase !== "player_turn") return;
state = endTurn(state);
render(state);
render(state, revealed);
await delay(800);
state = resolveEnemyTurn(state);
const end = checkCombatEnd(state);
if (end) {
state = {
...state,
combat: { ...state.combat, phase: "ended", result: end },
};
render(state);
return;
if (!checkEnd()) {
state = startTurn(state);
render(state, revealed);
}
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) {

View file

@ -1,11 +1,11 @@
import { getCard } from "./cards.js";
import { resolveEnemyAction } from "./enemies.js";
export function render(state) {
export function render(state, revealed) {
renderEnemy(state);
renderInfoBar(state);
renderHand(state);
renderOverlay(state);
renderOverlay(state, revealed);
}
function renderEnemy(state) {
@ -103,16 +103,38 @@ function renderHand(state) {
});
}
function renderOverlay(state) {
function renderOverlay(state, revealed) {
const overlay = document.getElementById("overlay");
const result = state.combat.phase === "ended" ? state.combat.result : null;
if (result === "victory") {
const overlayText = document.getElementById("overlay-text");
const rewardCards = document.getElementById("reward-cards");
const skipBtn = document.getElementById("skip-btn");
if (state.combat.phase === "rewards" && revealed) {
overlay.hidden = false;
overlay.textContent = "victory";
} else if (result === "defeat") {
overlayText.textContent = "card reward";
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.textContent = "defeat";
overlayText.textContent =
state.combat.result === "defeat"
? "defeat — click to restart"
: "victory";
rewardCards.innerHTML = "";
skipBtn.hidden = true;
} else {
overlay.hidden = true;
rewardCards.innerHTML = "";
skipBtn.hidden = true;
}
}