Add render module for state-to-DOM projection

This commit is contained in:
Jared Miller 2026-02-23 17:40:16 -05:00
parent e0f4916bce
commit 5a73afa22b
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

117
src/render.js Normal file
View file

@ -0,0 +1,117 @@
import { getCard } from "./cards.js";
import { resolveEnemyAction } from "./enemies.js";
export function render(state) {
renderEnemy(state);
renderInfoBar(state);
renderHand(state);
renderOverlay(state);
}
function renderEnemy(state) {
const { enemy, combat } = state;
document.getElementById("enemy-name").textContent = enemy.name;
document.getElementById("enemy-hp").textContent = `${enemy.hp}/${enemy.maxHp}`;
document.getElementById("enemy-hp").style.width =
`${(enemy.hp / enemy.maxHp) * 100}%`;
document.getElementById("enemy-block").textContent =
enemy.block > 0 ? `block: ${enemy.block}` : "";
const statusEl = document.getElementById("enemy-status");
const tokens = [];
if (enemy.vulnerable > 0) tokens.push(`vuln ${enemy.vulnerable}`);
if (enemy.weak > 0) tokens.push(`weak ${enemy.weak}`);
if (enemy.strength > 0) tokens.push(`str ${enemy.strength}`);
statusEl.textContent = tokens.join(" | ");
const intentEl = document.getElementById("enemy-intent");
if (combat.dieResult && combat.phase === "player_turn") {
const action = resolveEnemyAction(
enemy,
combat.dieResult,
enemy.trackPosition,
);
if (action) {
intentEl.textContent = formatIntent(action, enemy);
}
} else {
intentEl.textContent = "";
}
}
function formatIntent(action, enemy) {
if (!action?.effects) return "?";
const parts = action.effects.map((e) => {
if (e.type === "hit") {
const damage = e.value + (enemy.strength || 0);
return `attack ${damage}`;
}
if (e.type === "block") return `block ${e.value}`;
if (e.type === "strength") return `str +${e.value}`;
return e.type;
});
return parts.join(", ");
}
function renderInfoBar(state) {
const { player } = state;
document.getElementById("energy").textContent =
`energy: ${player.energy}/${player.maxEnergy}`;
document.getElementById("player-hp").textContent =
`${player.hp}/${player.maxHp}`;
document.getElementById("player-hp").style.width =
`${(player.hp / player.maxHp) * 100}%`;
document.getElementById("player-block").textContent =
player.block > 0 ? `block: ${player.block}` : "";
document.getElementById("player-strength").textContent =
player.strength > 0 ? `str: ${player.strength}` : "";
const playerStatus = document.getElementById("player-status");
if (playerStatus) {
const tokens = [];
if (player.vulnerable > 0) tokens.push(`vuln ${player.vulnerable}`);
if (player.weak > 0) tokens.push(`weak ${player.weak}`);
playerStatus.textContent = tokens.join(" | ");
}
document.getElementById("draw-count").textContent = player.drawPile.length;
document.getElementById("discard-count").textContent =
player.discardPile.length;
}
function renderHand(state) {
const handEl = document.getElementById("hand");
const { player, combat } = state;
handEl.innerHTML = "";
player.hand.forEach((cardId, index) => {
const card = getCard(cardId);
const img = document.createElement("img");
img.src = card.image || "";
img.alt = `${card.name} (${card.cost})`;
img.title = card.description;
img.className = "card";
img.dataset.index = index;
if (index === combat.selectedCard) {
img.classList.add("selected");
}
if (player.energy < card.cost) {
img.classList.add("no-energy");
}
handEl.appendChild(img);
});
}
function renderOverlay(state) {
const overlay = document.getElementById("overlay");
const result = state.combat.phase === "ended" ? state.combat.result : null;
if (result === "victory") {
overlay.hidden = false;
overlay.textContent = "victory";
} else if (result === "defeat") {
overlay.hidden = false;
overlay.textContent = "defeat";
} else {
overlay.hidden = true;
}
}