more card rule tweaks
This commit is contained in:
@@ -41,6 +41,14 @@ function getColumnPresence(state: GameState, column: number) {
|
||||
return [...owners];
|
||||
}
|
||||
|
||||
function addRoundedHalfBonus(scores: number[], counts: number[]) {
|
||||
counts.forEach((count, playerId) => {
|
||||
if (count > 0) {
|
||||
scores[playerId] += Math.ceil(count * 0.5);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyColumnBaseEnergy(state: GameState, scores: number[], ownerId: PlayerId, playersPresent: PlayerId[]) {
|
||||
const contested = playersPresent.length > 1;
|
||||
|
||||
@@ -105,11 +113,25 @@ function applyWeatherEffects(state: GameState, scores: number[], energySimulatio
|
||||
}
|
||||
|
||||
if (effectId === "tall_reward") {
|
||||
tallestLeaves.forEach((row, playerId) => {
|
||||
if (row !== null) {
|
||||
scores[playerId] += 2;
|
||||
const bestRow = tallestLeaves.reduce<number | null>((currentBest, row) => {
|
||||
if (row === null) {
|
||||
return currentBest;
|
||||
}
|
||||
});
|
||||
|
||||
if (currentBest === null || row < currentBest) {
|
||||
return row;
|
||||
}
|
||||
|
||||
return currentBest;
|
||||
}, null);
|
||||
|
||||
if (bestRow !== null) {
|
||||
tallestLeaves.forEach((row, playerId) => {
|
||||
if (row === bestRow) {
|
||||
scores[playerId] += 2;
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -123,25 +145,39 @@ function applyWeatherEffects(state: GameState, scores: number[], energySimulatio
|
||||
return;
|
||||
}
|
||||
|
||||
const westLightCounts = state.players.map(() => 0);
|
||||
const eastLightCounts = state.players.map(() => 0);
|
||||
const highNoonCounts = state.players.map(() => 0);
|
||||
|
||||
energySimulation.columns.forEach((column) => {
|
||||
if (!column.intercepted || column.ownerId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const region = getColumnRegion(state, column.column);
|
||||
if (effectId === "west_light" && region === "left") {
|
||||
scores[column.ownerId] += 1;
|
||||
if (region === "left") {
|
||||
westLightCounts[column.ownerId] += 1;
|
||||
}
|
||||
if (effectId === "east_light" && region === "right") {
|
||||
scores[column.ownerId] += 1;
|
||||
if (region === "right") {
|
||||
eastLightCounts[column.ownerId] += 1;
|
||||
}
|
||||
if (effectId === "high_noon" && region === "center") {
|
||||
scores[column.ownerId] += 1;
|
||||
if (region === "center") {
|
||||
highNoonCounts[column.ownerId] += 1;
|
||||
}
|
||||
if (effectId === "edge_bloom" && (column.column === 0 || column.column === state.config.columns - 1)) {
|
||||
scores[column.ownerId] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (effectId === "west_light") {
|
||||
addRoundedHalfBonus(scores, westLightCounts);
|
||||
}
|
||||
if (effectId === "east_light") {
|
||||
addRoundedHalfBonus(scores, eastLightCounts);
|
||||
}
|
||||
if (effectId === "high_noon") {
|
||||
addRoundedHalfBonus(scores, highNoonCounts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ export const WEATHER_CARDS: WeatherCardDefinition[] = [
|
||||
{ id: "branching_season", title: "Branching Season", description: "Each leaf after your first gives +1." },
|
||||
{ id: "storehouse", title: "Storehouse", description: "Banking is enabled this round." },
|
||||
{ id: "sun_ladder", title: "Sun Ladder", description: "Your first 3 vertical growths cost 0." },
|
||||
{ id: "west_light", title: "West Light", description: "Left third columns give +1." },
|
||||
{ id: "east_light", title: "East Light", description: "Right third columns give +1." },
|
||||
{ id: "high_noon", title: "High Noon", description: "Center third columns give +1." },
|
||||
{ id: "west_light", title: "West Light", description: "Left third energy gets +50%, rounded up." },
|
||||
{ id: "east_light", title: "East Light", description: "Right third energy gets +50%, rounded up." },
|
||||
{ id: "high_noon", title: "High Noon", description: "Center third energy gets +50%, rounded up." },
|
||||
{ id: "edge_bloom", title: "Edge Bloom", description: "Edge columns give +1." },
|
||||
{ id: "wide_reach", title: "Wide Reach", description: "Most columns gets +2." },
|
||||
{ id: "tall_reward", title: "Tall Reward", description: "Your tallest leaf gives +2." },
|
||||
{ id: "tall_reward", title: "Tall Reward", description: "Tallest leaf on the board gives +2." },
|
||||
{ id: "stalemate", title: "Stalemate", description: "Contested columns give no energy." },
|
||||
{ id: "split_light", title: "Split Light", description: "Contested columns give half to each player there." },
|
||||
];
|
||||
@@ -38,6 +38,7 @@ export function createWeatherDraft(state: GameState): WeatherDraftState {
|
||||
offers: shuffleArray(WEATHER_OFFER_PAIRS).slice(0, rowSize),
|
||||
drafted: [],
|
||||
banned: [],
|
||||
locked: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +52,10 @@ export function isWeatherCardAvailable(draft: WeatherDraftState, offerId: string
|
||||
return false;
|
||||
}
|
||||
|
||||
return offer.options.includes(cardId) && !draft.banned.includes(cardId) && !draft.drafted.includes(cardId);
|
||||
return offer.options.includes(cardId)
|
||||
&& !draft.banned.includes(cardId)
|
||||
&& !draft.drafted.includes(cardId)
|
||||
&& !draft.locked.includes(cardId);
|
||||
}
|
||||
|
||||
export function isWeatherOfferResolved(draft: WeatherDraftState, offerId: string) {
|
||||
@@ -60,7 +64,7 @@ export function isWeatherOfferResolved(draft: WeatherDraftState, offerId: string
|
||||
return true;
|
||||
}
|
||||
|
||||
return offer.options.every((cardId) => draft.banned.includes(cardId) || draft.drafted.includes(cardId));
|
||||
return offer.options.some((cardId) => draft.banned.includes(cardId) || draft.drafted.includes(cardId));
|
||||
}
|
||||
|
||||
export function getWeatherCard(cardId: WeatherCardId) {
|
||||
@@ -222,6 +222,7 @@ export type WeatherDraftState = {
|
||||
offers: WeatherOfferPair[];
|
||||
drafted: WeatherCardId[];
|
||||
banned: WeatherCardId[];
|
||||
locked: WeatherCardId[];
|
||||
};
|
||||
|
||||
export type GameState = {
|
||||
39
src/main.ts
39
src/main.ts
@@ -1404,24 +1404,33 @@ function renderSidebar() {
|
||||
|
||||
return `
|
||||
<aside class="sidebar">
|
||||
<section class="panel status-panel status-panel--weather">
|
||||
<h2>In Effect</h2>
|
||||
${activeEffectsMarkup}
|
||||
${state.roundSummary?.event ? `<p class="event-note">${state.roundSummary.event}</p>` : ""}
|
||||
<section class="panel accordion-panel accordion-panel--top">
|
||||
<details class="accordion">
|
||||
<summary>In Effect</summary>
|
||||
<div class="accordion__content">
|
||||
${activeEffectsMarkup}
|
||||
${state.roundSummary?.event ? `<p class="event-note">${state.roundSummary.event}</p>` : ""}
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="panel controls-panel">
|
||||
<div class="active-turn" style="--player-color: ${player.color}; --player-glow: ${player.glow};">
|
||||
<p class="eyebrow">Round ${state.round}</p>
|
||||
<h2>${getTurnLabel()}</h2>
|
||||
<p>${phaseHint}</p>
|
||||
${!state.gameOver ? `<p>${state.turnMoves.length} pending move${state.turnMoves.length === 1 ? "" : "s"}. Energy ${player.growthPoints}. Bank ${player.bankedPoints}.</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 || !isBankingEnabled() ? "disabled" : ""}>Bank Remaining</button>
|
||||
</div>
|
||||
<details class="accordion" open>
|
||||
<summary>End Turn</summary>
|
||||
<div class="accordion__content">
|
||||
<div class="active-turn" style="--player-color: ${player.color}; --player-glow: ${player.glow};">
|
||||
<p class="eyebrow">Round ${state.round}</p>
|
||||
<h2>${getTurnLabel()}</h2>
|
||||
<p>${phaseHint}</p>
|
||||
${!state.gameOver ? `<p>${state.turnMoves.length} pending move${state.turnMoves.length === 1 ? "" : "s"}. Energy ${player.growthPoints}. Bank ${player.bankedPoints}.</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 || !isBankingEnabled() ? "disabled" : ""}>Bank Remaining</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="panel accordion-panel">
|
||||
|
||||
@@ -110,11 +110,8 @@ html, body {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-game-launch {
|
||||
align-self: flex-end;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Bottom bar - Fixed height player scores */
|
||||
@@ -164,6 +161,30 @@ html, body {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-panel--weather {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.accordion-panel {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.accordion-panel--top {
|
||||
flex: 0 0 auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.finish-panel {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.finish-panel__actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.log-panel {
|
||||
max-height: 88px;
|
||||
flex-shrink: 0;
|
||||
@@ -588,12 +609,31 @@ button:disabled {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.accordion[open] {
|
||||
background: rgba(255, 255, 255, 0.045);
|
||||
}
|
||||
|
||||
.accordion summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
padding: 0.7rem 0.85rem;
|
||||
font-weight: 700;
|
||||
color: #f4f7fb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.accordion summary::after {
|
||||
content: "+";
|
||||
color: rgba(231, 238, 247, 0.62);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.accordion[open] summary::after {
|
||||
content: "-";
|
||||
}
|
||||
|
||||
.accordion summary::-webkit-details-marker {
|
||||
@@ -1105,11 +1145,20 @@ input[type="range"] {
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.weather-pair__divider {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(231, 238, 247, 0.42);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.weather-pair__option {
|
||||
padding: 0.75rem 0.8rem;
|
||||
border-radius: 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: transform 120ms ease, opacity 120ms ease, padding 120ms ease;
|
||||
}
|
||||
|
||||
.weather-pair__option--drafted {
|
||||
@@ -1121,6 +1170,20 @@ input[type="range"] {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.weather-pair__option--locked {
|
||||
padding: 0.5rem 0.65rem;
|
||||
opacity: 0.56;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.weather-pair__option--locked h2 {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.weather-pair__option--locked p {
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.weather-card--drafted {
|
||||
border-color: rgba(130, 224, 182, 0.55);
|
||||
}
|
||||
Reference in New Issue
Block a user