Add weather rules and streamline setup

This commit is contained in:
2026-04-09 17:42:19 -04:00
parent 8b50482621
commit 1cc85397bd
5 changed files with 161 additions and 51 deletions

View File

@@ -1,7 +1,5 @@
import "./styles.css";
console.log("[DEBUG] Starting main.ts import...");
import {
ROOT_SHIFT_COST,
ROUND_ANIMATION_BONUS_MS,
@@ -28,6 +26,7 @@ import {
createInitiativeDraft,
} from "./rules-initiative";
import {
WEATHER_CARDS,
createWeatherDraft,
getCurrentWeatherPlayerId,
getWeatherCard,
@@ -54,22 +53,17 @@ import type {
import { keyFor, parseKey, tint, wait } from "./utils";
const app = document.querySelector("#app");
console.log("[DEBUG] App element found:", app);
if (!(app instanceof HTMLElement)) {
console.error("[DEBUG] #app container not found or not HTMLElement");
throw new Error("#app container not found");
}
console.log("[DEBUG] Initializing state...");
let roundAnimationToken = 0;
let setup: SetupState = createSetupState();
console.log("[DEBUG] Setup created:", setup);
let state: GameState = createInitialState(setup);
console.log("[DEBUG] Initial state created:", state);
let isNewGameModalOpen = false;
let previousScoreSnapshot: ScoreSnapshot[] | null = null;
console.log("[DEBUG] State initialized, proceeding to render...");
let setupTab: "board" | "rules" | "events" | "players" = "board";
function rebuildSetup(overrides: Partial<SetupState> = {}) {
setup = createSetupState(
@@ -84,6 +78,7 @@ function rebuildSetup(overrides: Partial<SetupState> = {}) {
overrides.initiativeMode ?? setup.initiativeMode,
overrides.biddingOrderRule ?? setup.biddingOrderRule,
overrides.weatherDraftEnabled ?? setup.weatherDraftEnabled,
overrides.weatherDraftCount ?? setup.weatherDraftCount,
overrides.winCondition ?? setup.winCondition,
overrides.maxRounds ?? setup.maxRounds,
overrides.topLeafTarget ?? setup.topLeafTarget,
@@ -154,6 +149,10 @@ function getTurnLabel() {
return state.gameOver ? "Game Over" : `${getCurrentPlayer().name}'s turn`;
}
function isBankingEnabled() {
return state.activeRoundEffects.includes("storehouse");
}
function awardGrowth(player: Player, amount: number) {
if (amount <= 0) {
return;
@@ -207,7 +206,15 @@ function getSelectedRootShiftMoves() {
}
function getLegalMovesForSource(sourceKey: NodeKey, player: Player) {
return getLegalMovesForSourceForState(state, sourceKey, player);
const moves = getLegalMovesForSourceForState(state, sourceKey, player);
if (!state.activeRoundEffects.includes("sun_ladder")) {
return moves;
}
return moves.map((move) => move.direction === "vertical"
? { ...move, cost: Math.max(0, move.cost - 1) }
: move);
}
function playerHasLegalMove(player: Player) {
@@ -621,6 +628,10 @@ function endTurn() {
}
function bankGrowthAndEndTurn() {
if (!isBankingEnabled()) {
return;
}
const player = getCurrentPlayer();
if (player.growthPoints <= 0) {
endTurn();
@@ -787,6 +798,11 @@ function randomizeStartingLocations() {
render();
}
function setSetupTab(tab: typeof setupTab) {
setupTab = tab;
render();
}
function renderNewGameModal() {
if (!isNewGameModalOpen) {
return "";
@@ -794,6 +810,7 @@ function renderNewGameModal() {
const maxSeeds = getMaxStartingNodesPerPlayer(setup.playerCount, setup.columns);
const previewPlayers = createPlayers(setup.playerCount, setup.paletteOrder);
const draftCountMax = WEATHER_CARDS.length;
return `
<div class="modal-backdrop" id="new-game-modal-backdrop">
@@ -805,9 +822,15 @@ function renderNewGameModal() {
</div>
<button class="ghost-button" id="close-new-game">Close</button>
</header>
<nav class="setup-tabs" aria-label="Setup categories">
<button class="setup-tab${setupTab === "board" ? " setup-tab--active" : ""}" data-setup-tab="board"><span aria-hidden="true">▦</span><span>Board</span></button>
<button class="setup-tab${setupTab === "rules" ? " setup-tab--active" : ""}" data-setup-tab="rules"><span aria-hidden="true">⚖</span><span>Rules</span></button>
<button class="setup-tab${setupTab === "events" ? " setup-tab--active" : ""}" data-setup-tab="events"><span aria-hidden="true">☀</span><span>Events</span></button>
<button class="setup-tab${setupTab === "players" ? " setup-tab--active" : ""}" data-setup-tab="players"><span aria-hidden="true">◉</span><span>Players</span></button>
</nav>
<div class="modal-body">
<!-- Board Settings Section -->
${setupTab === "board" ? `
<section class="setup-section">
<h2 class="setup-section__title">Board Settings</h2>
<div class="setup-grid">
@@ -832,8 +855,9 @@ function renderNewGameModal() {
</label>
</div>
</section>
` : ""}
<!-- Game Rules Section -->
${setupTab === "rules" ? `
<section class="setup-section">
<h2 class="setup-section__title">Game Rules</h2>
<div class="setup-grid setup-grid--2col">
@@ -871,10 +895,21 @@ function renderNewGameModal() {
</select>
</label>
` : ""}
<label class="setup-field setup-field--checkbox">
<span class="setup-field__label">Weather Draft</span>
<input id="weather-draft-toggle" type="checkbox" ${setup.weatherDraftEnabled ? "checked" : ""} />
</label>
${setup.weatherDraftEnabled ? `
<label class="setup-field">
<span class="setup-field__label">Weather cards in draft</span>
<input id="weather-draft-count" type="number" min="1" max="${draftCountMax}" step="1" value="${setup.weatherDraftCount}" />
</label>
` : ""}
</div>
</section>
` : ""}
<!-- Random Events Section -->
${setupTab === "events" ? `
<section class="setup-section">
<h2 class="setup-section__title">Random Events</h2>
<div class="setup-grid setup-grid--3col">
@@ -886,14 +921,11 @@ function renderNewGameModal() {
<span class="setup-field__label">Disease %</span>
<input id="disease-chance" type="number" min="0" max="100" step="5" value="${setup.diseaseChance}" />
</label>
<label class="setup-field setup-field--checkbox">
<span class="setup-field__label">Weather Draft</span>
<input id="weather-draft-toggle" type="checkbox" ${setup.weatherDraftEnabled ? "checked" : ""} />
</label>
</div>
</section>
` : ""}
<!-- Players Section -->
${setupTab === "players" ? `
<section class="setup-section">
<h2 class="setup-section__title">Players & Colors</h2>
<p class="setup-section__help">Reorder players to set turn order. Starting positions are auto-assigned.</p>
@@ -912,6 +944,20 @@ function renderNewGameModal() {
`).join("")}
</div>
</section>
<section class="setup-section">
<h2 class="setup-section__title">Starting columns</h2>
<p class="setup-section__help">Use 1-based column numbers. Duplicate or invalid picks are auto-corrected.</p>
<button class="ghost-button randomize-button" id="randomize-starting-locations">Randomize Starting Locations</button>
<div class="player-list">
${previewPlayers.map((currentPlayer, index) => `
<label class="setup-field">
<span class="setup-field__label" style="color: ${currentPlayer.color};">${currentPlayer.name}</span>
<input class="seed-input" data-player-id="${index}" type="text" value="${setup.seedInputs[index] ?? ""}" placeholder="e.g. 2, 5" />
</label>
`).join("")}
</div>
</section>
` : ""}
</div>
<footer class="modal-footer">
@@ -1259,7 +1305,7 @@ function renderSidebar() {
</div>
<div class="button-row">
<button id="end-turn" ${boardLocked ? "disabled" : ""}>End Turn</button>
<button id="bank-turn" class="ghost-button" ${boardLocked ? "disabled" : ""}>Bank Remaining</button>
<button id="bank-turn" class="ghost-button" ${boardLocked || !isBankingEnabled() ? "disabled" : ""}>Bank Remaining</button>
</div>
</section>
@@ -1386,6 +1432,11 @@ function attachEvents() {
});
document.querySelector<HTMLInputElement>("#weather-draft-toggle")?.addEventListener("change", (event) => {
setup.weatherDraftEnabled = (event.currentTarget as HTMLInputElement).checked;
render();
});
document.querySelector<HTMLInputElement>("#weather-draft-count")?.addEventListener("change", (event) => {
rebuildSetup({ weatherDraftCount: Math.max(1, Number((event.currentTarget as HTMLInputElement).value) || 1) });
render();
});
document.querySelector<HTMLSelectElement>("#win-condition")?.addEventListener("change", (event) => {
setup.winCondition = (event.currentTarget as HTMLSelectElement).value as SetupState["winCondition"];
@@ -1427,38 +1478,31 @@ function attachEvents() {
chooseWeatherAction(button.dataset.weatherCard as WeatherCardId, button.dataset.weatherAction as "draft" | "ban");
});
});
document.querySelectorAll<HTMLElement>("[data-setup-tab]").forEach((button) => {
button.addEventListener("click", () => {
setSetupTab(button.dataset.setupTab as typeof setupTab);
});
});
}
function render() {
console.log("[DEBUG] render() called, playerCount:", state?.players?.length);
try {
const playerCount = state.players.length;
console.log("[DEBUG] Building HTML...");
app.innerHTML = `
<main class="layout" style="--player-count: ${playerCount}">
<section class="game-area">
${renderBoard()}
</section>
${renderSidebar()}
<footer class="scoreboard">
${renderScoreboard()}
</footer>
</main>
${renderNewGameModal()}
${renderInitiativeModal()}
${renderWeatherDraftModal()}
`;
console.log("[DEBUG] HTML rendered, attaching events...");
attachEvents();
console.log("[DEBUG] Events attached, getting score snapshot...");
previousScoreSnapshot = getScoreSnapshot();
console.log("[DEBUG] Render complete!");
} catch (error) {
console.error("[DEBUG] Error in render():", error);
throw error;
}
const playerCount = state.players.length;
app.innerHTML = `
<main class="layout" style="--player-count: ${playerCount}">
<section class="game-area">
${renderBoard()}
</section>
${renderSidebar()}
<footer class="scoreboard">
${renderScoreboard()}
</footer>
</main>
${renderNewGameModal()}
${renderInitiativeModal()}
${renderWeatherDraftModal()}
`;
attachEvents();
previousScoreSnapshot = getScoreSnapshot();
}
console.log("[DEBUG] About to call render()...");
render();
console.log("[DEBUG] render() executed");