big version with draft order and weather cards

This commit is contained in:
2026-04-08 17:46:42 -04:00
parent 4034ca55cf
commit a99a1f32e3
8 changed files with 966 additions and 59 deletions

View File

@@ -22,6 +22,15 @@ import {
maybeRollSunbeam as maybeRollSunbeamForState,
scoreColumns as scoreColumnsForState,
} from "./rules-scoring";
import {
createInitiativeDraft,
} from "./rules-initiative";
import {
createWeatherDraft,
getCurrentWeatherPlayerId,
getWeatherCard,
isWeatherCardAvailable,
} from "./rules-weather";
import {
createInitialState,
createPlayers,
@@ -38,6 +47,7 @@ import type {
SetupState,
ShiftMove,
TurnMove,
WeatherCardId,
} from "./types";
import { keyFor, parseKey, tint, wait } from "./utils";
@@ -53,12 +63,47 @@ let state: GameState = createInitialState(setup);
let isNewGameModalOpen = false;
let previousScoreSnapshot: ScoreSnapshot[] | null = null;
function getLiveExposureScores() {
return buildEnergySimulation(state).scores;
}
function getTopLeafCount() {
const childrenMap = buildChildrenMap();
let count = 0;
state.nodes.forEach((_, nodeKey) => {
const { row } = parseKey(nodeKey);
if (row === 0 && !(childrenMap.get(nodeKey)?.length)) {
count += 1;
}
});
return count;
}
function hasReachedWinCondition() {
if (state.config.winCondition === "rounds") {
return state.round >= state.config.maxRounds;
}
return getTopLeafCount() >= state.config.topLeafTarget;
}
function getWinConditionSummary() {
if (state.config.winCondition === "rounds") {
return `Game ends after round ${state.config.maxRounds}.`;
}
return `Game ends when ${state.config.topLeafTarget} top-row leaf${state.config.topLeafTarget === 1 ? " is" : "s are"} occupied.`;
}
function getScoreSnapshot() {
return state.players.map((player) => ({
totalScore: player.totalScore,
roundScore: player.roundScore,
const exposureScores = getLiveExposureScores();
return state.players.map((player, index) => ({
currentExposure: exposureScores[index],
growthPoints: player.growthPoints,
bankedPoints: player.bankedPoints,
lifetimeGrowthIncome: player.lifetimeGrowthIncome,
}));
}
@@ -66,6 +111,47 @@ function getCurrentPlayer() {
return state.players[state.activePlayerId];
}
function getOrderedPlayers(playerIds: number[]) {
return playerIds.map((playerId) => state.players[playerId]);
}
function getTurnLabel() {
if (state.phase === "initiative" && state.initiativeDraft) {
return `${getCurrentPlayer().name} drafts initiative`;
}
if (state.phase === "weather" && state.weatherDraft) {
return `${getCurrentPlayer().name} drafts weather`;
}
return state.gameOver ? "Game Over" : `${getCurrentPlayer().name}'s turn`;
}
function awardGrowth(player: Player, amount: number) {
if (amount <= 0) {
return;
}
player.growthPoints += amount;
player.lifetimeGrowthIncome += amount;
}
function getCurrentBiddingPlayer() {
if (!state.initiativeDraft) {
return null;
}
return state.players[state.initiativeDraft.biddingOrder[state.initiativeDraft.biddingIndex]];
}
function getCurrentWeatherDraftPlayer() {
if (!state.weatherDraft) {
return null;
}
return state.players[getCurrentWeatherPlayerId(state.weatherDraft)];
}
function getNodeOwner(row: number, column: number) {
return getNodeOwnerForState(state, row, column);
}
@@ -232,7 +318,160 @@ function updateSelection(sourceKey: NodeKey | null = null) {
}
function isInteractionLocked() {
return state.gameOver || Boolean(state.animation);
return state.gameOver || state.phase !== "turn" || Boolean(state.animation);
}
function startWeatherDraft() {
if (!state.config.weatherDraftEnabled) {
moveToFirstPlayableTurn();
return;
}
state.phase = "weather";
state.turnMoves = [];
updateSelection(null);
state.weatherDraft = createWeatherDraft(state);
state.activePlayerId = state.weatherDraft.playerOrder[0];
state.history.unshift(`Round ${state.round} weather draft begins.`);
}
function startInitiativeDraft() {
state.phase = "initiative";
state.turnMoves = [];
updateSelection(null);
state.initiativeDraft = createInitiativeDraft(state);
state.activePlayerId = state.initiativeDraft.biddingOrder[0];
state.history.unshift(`Round ${state.round} initiative draft begins.`);
}
function moveToFirstPlayableTurn() {
const nextPlayerId = state.turnOrder.find((playerId) => playerHasLegalMove(state.players[playerId]));
if (nextPlayerId === undefined) {
state.gameOver = true;
state.phase = "game_over";
state.history.unshift("No player can grow further. Final totals are locked.");
return false;
}
state.phase = "turn";
state.activePlayerId = nextPlayerId;
state.turnMoves = [];
updateSelection(null);
return true;
}
function finalizeInitiativeDraft() {
const draft = state.initiativeDraft;
if (!draft) {
return;
}
const nextTurnOrder = draft.seatAssignments.filter((playerId): playerId is number => playerId !== null);
state.turnOrder = nextTurnOrder;
state.players.forEach((player) => {
player.passed = false;
});
nextTurnOrder.forEach((playerId, seatIndex) => {
const bonus = draft.seatBonuses[seatIndex] ?? 0;
if (bonus > 0) {
awardGrowth(state.players[playerId], bonus);
}
});
const seatSummary = nextTurnOrder
.map((playerId, seatIndex) => {
const bonus = draft.seatBonuses[seatIndex] ?? 0;
return `${state.players[playerId].name}: Seat ${seatIndex + 1}${bonus > 0 ? ` (+${bonus})` : ""}`;
})
.join(" | ");
state.initiativeDraft = null;
state.history.unshift(`Round ${state.round} initiative set. ${seatSummary}`);
startWeatherDraft();
}
function chooseInitiativeSeat(seatIndex: number) {
const draft = state.initiativeDraft;
if (!draft || seatIndex < 0 || seatIndex >= draft.seatAssignments.length || draft.seatAssignments[seatIndex] !== null) {
return;
}
const playerId = draft.biddingOrder[draft.biddingIndex];
draft.seatAssignments[seatIndex] = playerId;
state.history.unshift(`${state.players[playerId].name} claimed seat ${seatIndex + 1} for round ${state.round}.`);
if (draft.biddingIndex >= draft.biddingOrder.length - 1) {
finalizeInitiativeDraft();
render();
return;
}
draft.biddingIndex += 1;
state.activePlayerId = draft.biddingOrder[draft.biddingIndex];
render();
}
function beginRound() {
if (state.gameOver) {
state.phase = "game_over";
return;
}
state.players.forEach((player) => {
player.passed = false;
});
if (state.config.initiativeMode === "bid") {
startInitiativeDraft();
return;
}
startWeatherDraft();
}
function finalizeWeatherDraft() {
if (!state.weatherDraft) {
moveToFirstPlayableTurn();
return;
}
state.activeRoundEffects = [...state.weatherDraft.drafted];
const draftedSummary = state.activeRoundEffects.map((cardId) => getWeatherCard(cardId)?.title ?? cardId).join(", ");
const bannedSummary = state.weatherDraft.banned.map((cardId) => getWeatherCard(cardId)?.title ?? cardId).join(", ");
state.history.unshift(
`Round ${state.round} weather set.${draftedSummary ? ` Active: ${draftedSummary}.` : ""}${bannedSummary ? ` Banned: ${bannedSummary}.` : ""}`
);
state.weatherDraft = null;
moveToFirstPlayableTurn();
}
function chooseWeatherAction(cardId: WeatherCardId, action: "draft" | "ban") {
const draft = state.weatherDraft;
if (!draft || !isWeatherCardAvailable(draft, cardId)) {
return;
}
const playerId = getCurrentWeatherPlayerId(draft);
const card = getWeatherCard(cardId);
if (action === "draft") {
draft.drafted.push(cardId);
state.history.unshift(`${state.players[playerId].name} drafted ${card?.title ?? cardId}.`);
} else {
draft.banned.push(cardId);
state.history.unshift(`${state.players[playerId].name} banned ${card?.title ?? cardId}.`);
}
if (draft.draftIndex >= draft.playerOrder.length - 1) {
finalizeWeatherDraft();
render();
return;
}
draft.draftIndex += 1;
state.activePlayerId = draft.playerOrder[draft.draftIndex];
render();
}
function advanceTurn() {
@@ -241,9 +480,10 @@ function advanceTurn() {
return;
}
const currentOrderIndex = Math.max(0, state.turnOrder.indexOf(state.activePlayerId));
let nextPlayerId = state.activePlayerId;
for (let step = 0; step < state.players.length; step += 1) {
nextPlayerId = (nextPlayerId + 1) % state.players.length;
for (let step = 1; step <= state.turnOrder.length; step += 1) {
nextPlayerId = state.turnOrder[(currentOrderIndex + step) % state.turnOrder.length];
const candidate = state.players[nextPlayerId];
if (!candidate.passed && playerHasLegalMove(candidate)) {
@@ -258,21 +498,6 @@ function advanceTurn() {
endRound();
}
function moveToFirstPlayableTurn() {
const nextPlayerId = state.players.findIndex((player) => playerHasLegalMove(player));
if (nextPlayerId === -1) {
state.gameOver = true;
state.history.unshift("No player can grow further. Final totals are locked.");
return false;
}
state.activePlayerId = nextPlayerId;
state.turnMoves = [];
updateSelection(null);
return true;
}
function undoMovesThrough(targetKey: NodeKey) {
const player = getCurrentPlayer();
const moveIndex = findTurnMoveIndex(targetKey);
@@ -386,6 +611,7 @@ function bankGrowthAndEndTurn() {
}
async function endRound() {
state.phase = "round_end";
const { scores, columnResults, energySimulation } = scoreColumns();
const { nextGrowth, event: sunbeamEvent, awardedPlayer } = maybeRollSunbeam(scores);
const { killedKeys, event: diseaseEvent } = maybeRollDisease();
@@ -431,7 +657,9 @@ async function endRound() {
player.roundScore = scores[index];
player.totalScore += scores[index];
player.bonusPoints = nextGrowth[index] - scores[index];
player.growthPoints = nextGrowth[index] + player.bankedPoints;
player.growthPoints = player.bankedPoints;
player.lifetimeGrowthIncome += nextGrowth[index];
player.growthPoints += nextGrowth[index];
player.bankedPoints = 0;
player.passed = false;
});
@@ -450,25 +678,27 @@ async function endRound() {
});
state.animation = null;
state.activeRoundEffects = [];
const boardFull = state.nodes.size >= state.config.columns * state.config.rows || state.players.every((player) => !playerHasLegalMove(player));
if (boardFull) {
if (boardFull || hasReachedWinCondition()) {
state.gameOver = true;
state.history.unshift("The canopy is complete. Final totals are locked.");
state.phase = "game_over";
state.history.unshift(boardFull ? "The canopy is complete. Final totals are locked." : `Win condition reached. ${getWinConditionSummary()}`);
render();
return;
}
state.round += 1;
state.history.unshift(`Round ${state.round} begins.`);
moveToFirstPlayableTurn();
beginRound();
render();
}
function resetGame() {
roundAnimationToken += 1;
state = createInitialState(setup);
moveToFirstPlayableTurn();
beginRound();
render();
}
@@ -494,6 +724,7 @@ function finishGameNow() {
roundAnimationToken += 1;
state.animation = null;
state.phase = "game_over";
const { scores, columnResults } = scoreColumns();
state.players.forEach((player, index) => {
player.roundScore = scores[index];
@@ -573,10 +804,44 @@ function renderNewGameModal() {
<span>Disease %</span>
<input id="disease-chance" type="number" min="0" max="100" step="5" value="${setup.diseaseChance}" />
</label>
<label class="toggle-row">
<span>Shuffle turn order</span>
<input id="shuffle-order-toggle" type="checkbox" ${setup.shuffleTurnOrder ? "checked" : ""} />
<label>
<span>Initiative</span>
<select id="initiative-mode">
<option value="fixed" ${setup.initiativeMode === "fixed" ? "selected" : ""}>Fixed Order</option>
<option value="bid" ${setup.initiativeMode === "bid" ? "selected" : ""}>Seat Draft</option>
</select>
</label>
${setup.initiativeMode === "bid" ? `
<label>
<span>Bid Order</span>
<select id="bidding-order-rule">
<option value="rotating" ${setup.biddingOrderRule === "rotating" ? "selected" : ""}>Rotating</option>
<option value="lowest_growth_income" ${setup.biddingOrderRule === "lowest_growth_income" ? "selected" : ""}>Lowest Growth</option>
</select>
</label>
` : ""}
<label class="toggle-row">
<span>Weather Draft</span>
<input id="weather-draft-toggle" type="checkbox" ${setup.weatherDraftEnabled ? "checked" : ""} />
</label>
<label>
<span>Win Condition</span>
<select id="win-condition">
<option value="rounds" ${setup.winCondition === "rounds" ? "selected" : ""}>Round Limit</option>
<option value="top_leaves" ${setup.winCondition === "top_leaves" ? "selected" : ""}>Top Leaves</option>
</select>
</label>
${setup.winCondition === "rounds" ? `
<label>
<span>Max rounds</span>
<input id="max-rounds" type="number" min="1" max="99" step="1" value="${setup.maxRounds}" />
</label>
` : `
<label>
<span>Top leaf target</span>
<input id="top-leaf-target" type="number" min="1" max="${setup.columns}" step="1" value="${setup.topLeafTarget}" />
</label>
`}
</div>
<div class="modal-grid">
<div class="seed-editor">
@@ -626,28 +891,128 @@ function renderNewGameModal() {
`;
}
function renderWeatherDraftModal() {
if (state.phase !== "weather" || !state.weatherDraft) {
return "";
}
const draft = state.weatherDraft;
const currentPlayer = getCurrentWeatherDraftPlayer();
return `
<section class="draft-panel panel" role="dialog" aria-modal="true" aria-labelledby="weather-title">
<div class="panel__title-row">
<div>
<p class="eyebrow">Round ${state.round}</p>
<h1 id="weather-title">Weather Draft</h1>
</div>
</div>
<div class="seed-editor">
<p class="seed-help">${currentPlayer?.name ?? "A player"} must draft one card for this round or ban one to deny it.</p>
<div class="weather-key" aria-label="Weather action key">
<span><strong>☀ Draft</strong>: apply to the board for 1 round</span>
<span><strong>✕ Ban</strong>: remove it this round</span>
</div>
<div class="initiative-order-row">
${getOrderedPlayers(draft.playerOrder).map((player, index) => `<span class="initiative-pill${index === draft.draftIndex ? " initiative-pill--active" : ""}" style="--player-color: ${player.color};">${player.name}</span>`).join("")}
</div>
</div>
<div class="weather-grid">
${draft.row.map((cardId) => {
const card = getWeatherCard(cardId);
const drafted = draft.drafted.includes(cardId);
const banned = draft.banned.includes(cardId);
const available = !drafted && !banned;
return `
<article class="weather-card${drafted ? " weather-card--drafted" : ""}${banned ? " weather-card--banned" : ""}">
<div>
<p class="eyebrow">${drafted ? "Drafted" : banned ? "Banned" : "Open"}</p>
<h2>${card?.title ?? cardId}</h2>
<p>${card?.description ?? ""}</p>
</div>
${available ? `
<div class="weather-card__actions">
<button class="weather-action weather-action--draft" data-weather-action="draft" data-weather-card="${cardId}">
<span class="weather-action__icon" aria-hidden="true">☀</span>
<span><strong>Draft</strong></span>
</button>
<button class="weather-action weather-action--ban" data-weather-action="ban" data-weather-card="${cardId}">
<span class="weather-action__icon" aria-hidden="true">✕</span>
<span><strong>Ban</strong></span>
</button>
</div>
` : ""}
</article>
`;
}).join("")}
</div>
</section>
`;
}
function renderInitiativeModal() {
if (state.phase !== "initiative" || !state.initiativeDraft) {
return "";
}
const draft = state.initiativeDraft;
const currentBidder = getCurrentBiddingPlayer();
const orderedBidders = getOrderedPlayers(draft.biddingOrder);
return `
<section class="draft-panel panel" role="dialog" aria-modal="true" aria-labelledby="initiative-title">
<div class="panel__title-row">
<div>
<p class="eyebrow">Round ${state.round}</p>
<h1 id="initiative-title">Initiative Draft</h1>
</div>
</div>
<div class="seed-editor">
<p class="seed-help">${currentBidder?.name ?? "A player"} chooses a seat. Earlier seats gain extra growth before the round begins.</p>
<div class="initiative-order-row">
${orderedBidders.map((player, index) => `<span class="initiative-pill${index === draft.biddingIndex ? " initiative-pill--active" : ""}" style="--player-color: ${player.color};">${player.name}</span>`).join("")}
</div>
</div>
<div class="initiative-seat-grid">
${draft.seatAssignments.map((playerId, seatIndex) => {
const assignedPlayer = playerId === null ? null : state.players[playerId];
const bonus = draft.seatBonuses[seatIndex] ?? 0;
const disabled = playerId !== null ? "disabled" : "";
return `
<button class="initiative-seat${playerId === null ? "" : " initiative-seat--taken"}" data-seat-choice="${seatIndex}" ${disabled}>
<strong>Seat ${seatIndex + 1}</strong>
<span>${bonus > 0 ? `+${bonus} growth` : "+0 growth"}</span>
<span>${assignedPlayer ? assignedPlayer.name : "Open"}</span>
</button>
`;
}).join("")}
</div>
</section>
`;
}
function renderScoreboard() {
const liveExposureScores = getLiveExposureScores();
return state.players.map((player, index) => {
const isActive = index === state.activePlayerId && !state.gameOver;
const isActive = player.id === state.activePlayerId && !state.gameOver;
const seatIndex = state.turnOrder.indexOf(player.id);
const previous = previousScoreSnapshot?.[index];
const totalChanged = previous && previous.totalScore !== player.totalScore;
const sunlightChanged = previous && previous.roundScore !== player.roundScore;
const sunlightChanged = previous && previous.currentExposure !== liveExposureScores[index];
const growthChanged = previous && previous.growthPoints !== player.growthPoints;
const bankChanged = previous && previous.bankedPoints !== player.bankedPoints;
return `
<article class="score-card${isActive ? " active" : ""}" style="--player-color: ${player.color}; --player-glow: ${player.glow};">
<div class="score-card__head">
<span class="player-dot"></span>
<h2>${player.name}</h2>
<div class="score-card__identity">
<span class="player-dot"></span>
<h2>${player.name}</h2>
</div>
<span class="score-card__meta">Lifetime ${player.lifetimeGrowthIncome}</span>
</div>
<div class="score-card__numbers">
<div>
<span>Total</span>
<strong class="${totalChanged ? "score-value changed" : "score-value"}">${player.totalScore}</strong>
</div>
<div>
<span>Sunlight</span>
<strong class="${sunlightChanged ? "score-value changed" : "score-value"}">${player.roundScore}</strong>
<span>Current</span>
<strong class="${sunlightChanged ? "score-value changed" : "score-value"}">${liveExposureScores[index]}</strong>
</div>
<div>
<span>Energy</span>
@@ -658,6 +1023,9 @@ function renderScoreboard() {
<strong class="${bankChanged ? "score-value changed" : "score-value"}">${player.bankedPoints}</strong>
</div>
</div>
<div class="score-card__footer">
<span>Seat ${seatIndex === -1 ? "-" : seatIndex + 1}</span>
</div>
</article>
`;
}).join("");
@@ -826,6 +1194,13 @@ function renderSidebar() {
const player = getCurrentPlayer();
const rootShiftMoves = getSelectedRootShiftMoves();
const boardLocked = isInteractionLocked();
const turnOrderSummary = getOrderedPlayers(state.turnOrder).map((orderedPlayer, index) => `${index + 1}. ${orderedPlayer.name}`).join(" | ");
const activeEffectsMarkup = state.activeRoundEffects.length > 0
? `<div class="active-effects">${state.activeRoundEffects.map((cardId) => {
const card = getWeatherCard(cardId);
return `<div class="effect-chip" title="${card?.description ?? ""}"><span class="effect-chip__title">${card?.title ?? cardId}</span></div>`;
}).join("")}</div>`
: `<p class="effect-empty">No weather effects active.</p>`;
const nextGrowthText = state.roundSummary
? state.players.map((current) => `${current.name}: ${current.growthPoints}`).join(" | ")
: "Next round growth = 1 + columns owned + any banked growth.";
@@ -842,22 +1217,24 @@ function renderSidebar() {
</div>
<div class="active-turn" style="--player-color: ${player.color}; --player-glow: ${player.glow};">
<p class="eyebrow">Round ${state.round}</p>
<h2>${state.gameOver ? "Game Over" : `${player.name}'s turn`}</h2>
<p>${state.gameOver ? "Tallies are final." : "Spend growth by extending upward. Vertical costs 1. Diagonal costs 2. Click a glowing new node to undo back to that point before you commit the turn."}</p>
<h2>${getTurnLabel()}</h2>
<p>${state.phase === "initiative" ? "Choose a public seat for this round. Earlier seats gain more growth, later seats act later." : state.gameOver ? "Tallies are final." : "Spend growth by extending upward. Vertical costs 1. Diagonal costs 2. Click a glowing new node to undo back to that point before you commit the turn."}</p>
${!state.gameOver ? `<p>${state.turnMoves.length} pending move${state.turnMoves.length === 1 ? "" : "s"} this turn. ${player.bankedPoints} banked for the next round.</p>` : ""}
${state.round === 1 ? `<p>Select a root on the bottom row to shift it left or right for ${ROOT_SHIFT_COST} point during round 1.</p>` : ""}
<p>Turn order: ${turnOrderSummary}</p>
<p>${getWinConditionSummary()}</p>
${rootShiftMoves.length > 0 ? `<div class="root-shift-row">${rootShiftMoves.map((move) => `<button class="ghost-button root-shift-button" data-root-shift="${move.direction === "left" ? -1 : 1}" ${boardLocked ? "disabled" : ""}>${move.direction === "left" ? "<- Root" : "Root ->"}</button>`).join("")}</div>` : ""}
</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="finish-game" class="ghost-button" ${boardLocked ? "disabled" : ""}>Score Now</button>
</div>
</section>
<section class="panel status-panel">
<h2>Round economy</h2>
<p>${nextGrowthText}</p>
${activeEffectsMarkup}
${state.roundSummary?.event ? `<p class="event-note">${state.roundSummary.event}</p>` : ""}
</section>
@@ -867,6 +1244,10 @@ function renderSidebar() {
${state.history.slice(0, 8).map((entry) => `<p>${entry}</p>`).join("")}
</div>
</section>
<section class="panel finish-panel">
<button id="finish-game" class="ghost-button finish-game-button" ${boardLocked ? "disabled" : ""}>End Game / Score Now</button>
</section>
</aside>
`;
}
@@ -890,13 +1271,15 @@ function attachEvents() {
return;
}
if (ownerId === currentPlayer.id && undoMovesThrough(nodeKey)) {
return;
}
if (ownerId === currentPlayer.id) {
const sourceKey = nodeKey;
const isPendingNode = isPendingTurnNode(row, column);
if (state.selectedSource === sourceKey) {
if (isPendingNode && undoMovesThrough(nodeKey)) {
return;
}
updateSelection(null);
} else {
updateSelection(sourceKey);
@@ -924,7 +1307,7 @@ function attachEvents() {
if (output) {
output.textContent = input.value;
}
setup = createSetupState(Number(input.value), setup.columns, setup.rows, setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder);
setup = createSetupState(Number(input.value), setup.columns, setup.rows, setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder, setup.initiativeMode, setup.biddingOrderRule, setup.weatherDraftEnabled, setup.winCondition, setup.maxRounds, setup.topLeafTarget);
render();
});
document.querySelector<HTMLInputElement>("#column-count")?.addEventListener("change", (event) => {
@@ -933,7 +1316,7 @@ function attachEvents() {
return;
}
setup = createSetupState(setup.playerCount, Math.max(6, Math.min(24, columns)), setup.rows, setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder);
setup = createSetupState(setup.playerCount, Math.max(6, Math.min(24, columns)), setup.rows, setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder, setup.initiativeMode, setup.biddingOrderRule, setup.weatherDraftEnabled, setup.winCondition, setup.maxRounds, setup.topLeafTarget);
render();
});
document.querySelector<HTMLInputElement>("#row-count")?.addEventListener("change", (event) => {
@@ -942,7 +1325,7 @@ function attachEvents() {
return;
}
setup = createSetupState(setup.playerCount, setup.columns, Math.max(6, Math.min(24, rows)), setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder);
setup = createSetupState(setup.playerCount, setup.columns, Math.max(6, Math.min(24, rows)), setup.startingNodesPerPlayer, setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder, setup.initiativeMode, setup.biddingOrderRule, setup.weatherDraftEnabled, setup.winCondition, setup.maxRounds, setup.topLeafTarget);
render();
});
document.querySelector<HTMLInputElement>("#starting-nodes")?.addEventListener("change", (event) => {
@@ -951,7 +1334,7 @@ function attachEvents() {
return;
}
setup = createSetupState(setup.playerCount, setup.columns, setup.rows, Math.max(1, nextValue), setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder);
setup = createSetupState(setup.playerCount, setup.columns, setup.rows, Math.max(1, nextValue), setup.sunbeamChance, setup.diseaseChance, setup.seedInputs, setup.paletteOrder, setup.shuffleTurnOrder, setup.initiativeMode, setup.biddingOrderRule, setup.weatherDraftEnabled, setup.winCondition, setup.maxRounds, setup.topLeafTarget);
render();
});
document.querySelector<HTMLInputElement>("#sunbeam-chance")?.addEventListener("change", (event) => {
@@ -962,8 +1345,25 @@ function attachEvents() {
setup.diseaseChance = Math.max(0, Math.min(100, Number((event.currentTarget as HTMLInputElement).value) || 0));
state.randomEffects.diseaseChance = setup.diseaseChance;
});
document.querySelector<HTMLInputElement>("#shuffle-order-toggle")?.addEventListener("change", (event) => {
setup.shuffleTurnOrder = (event.currentTarget as HTMLInputElement).checked;
document.querySelector<HTMLSelectElement>("#initiative-mode")?.addEventListener("change", (event) => {
setup.initiativeMode = (event.currentTarget as HTMLSelectElement).value as SetupState["initiativeMode"];
render();
});
document.querySelector<HTMLSelectElement>("#bidding-order-rule")?.addEventListener("change", (event) => {
setup.biddingOrderRule = (event.currentTarget as HTMLSelectElement).value as SetupState["biddingOrderRule"];
});
document.querySelector<HTMLInputElement>("#weather-draft-toggle")?.addEventListener("change", (event) => {
setup.weatherDraftEnabled = (event.currentTarget as HTMLInputElement).checked;
});
document.querySelector<HTMLSelectElement>("#win-condition")?.addEventListener("change", (event) => {
setup.winCondition = (event.currentTarget as HTMLSelectElement).value as SetupState["winCondition"];
render();
});
document.querySelector<HTMLInputElement>("#max-rounds")?.addEventListener("change", (event) => {
setup.maxRounds = Math.max(1, Number((event.currentTarget as HTMLInputElement).value) || 1);
});
document.querySelector<HTMLInputElement>("#top-leaf-target")?.addEventListener("change", (event) => {
setup.topLeafTarget = Math.max(1, Math.min(setup.columns, Number((event.currentTarget as HTMLInputElement).value) || 1));
});
document.querySelectorAll<HTMLInputElement>(".seed-input").forEach((input) => {
input.addEventListener("input", (event) => {
@@ -985,6 +1385,16 @@ function attachEvents() {
shiftSelectedRoot(Number(button.dataset.rootShift));
});
});
document.querySelectorAll<HTMLElement>("[data-seat-choice]").forEach((button) => {
button.addEventListener("click", () => {
chooseInitiativeSeat(Number(button.dataset.seatChoice));
});
});
document.querySelectorAll<HTMLElement>("[data-weather-card]").forEach((button) => {
button.addEventListener("click", () => {
chooseWeatherAction(button.dataset.weatherCard as WeatherCardId, button.dataset.weatherAction as "draft" | "ban");
});
});
}
function render() {
@@ -999,6 +1409,8 @@ function render() {
</footer>
</main>
${renderNewGameModal()}
${renderInitiativeModal()}
${renderWeatherDraftModal()}
`;
attachEvents();