diff --git a/src/client/game/goods_table.module.css b/src/client/game/goods_table.module.css index 35bcadf7..12c9a728 100644 --- a/src/client/game/goods_table.module.css +++ b/src/client/game/goods_table.module.css @@ -19,15 +19,49 @@ gap: 4px; } -.row > *, -.column > * { +.row > * { flex: 1; text-align: center; } +/* column children should not stretch vertically; columns are fixed-width stacks */ +.column > * { + flex: none; + text-align: center; +} + +/* layout for the two groups: White and Black */ +.groupsGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; + align-items: start; + justify-items: center; +} + +.group { + display: flex; + flex-direction: column; + align-items: center; +} + +.groupHeader { + margin: 0 0 8px 0; + font-size: 18px; + text-align: center; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + .goodPlace { - aspect-ratio: 1 / 1; - background-color: var(--good-color); + width: 28px; + height: 28px; + display: block; + /* prefer the --good-color variable set by good classes, fall back to lightgrey */ + background-color: var(--good-color, lightgrey); } .empty { @@ -45,3 +79,82 @@ .gapRight { margin-right: 8px; } + +.blackColumn { + background-color: #333; + padding: 12px 6px 12px 6px; + border-radius: 4px; +} + +.blackHeader { + background-color: #333; + color: white; + padding: 6px 8px; + border-radius: 6px; +} + +.headerCell { + display: flex; + justify-content: center; + align-items: center; + padding: 2px 0; + height: 26px; +} + +.letterCell { + display: flex; + justify-content: center; + align-items: center; + padding: 2px 0; + height: 26px; +} + +/* keep header cell positioned naturally inside the black column so it has breathing room */ +.blackColumn > .headerCell { + margin-top: 0; +} + +/* Ensure letter placeholders in black columns align with others */ +.blackColumn > .letterCell { + margin-top: 0; +} + +/* layout wrappers for grouped columns (white / black) */ +.leftColumns, +.rightColumns { + display: flex; + gap: 8px; + align-items: flex-start; + flex: none; /* don't stretch to fill the row */ + flex-wrap: nowrap; + padding-top: 10px; /* breathing room above the first row of header hexes */ +} + +.rightColumns { + margin-left: 8px; +} + +/* make each column a fixed-size stack so groups line up */ +.column { + width: 52px; + flex: none; + display: flex; + flex-direction: column; + gap: 6px; + align-items: center; +} + +/* apply the same internal padding to white columns so header hexes have breathing room inside the white stacks */ +.leftColumns > .column { + padding: 12px 6px 12px 6px; + box-sizing: border-box; +} + +/* placeholder used to reserve header space when there's no visible letter header */ +.headerPlaceholder { + width: 32px; + height: 29px; +} +.headerPlaceholderHidden { + visibility: hidden; +} \ No newline at end of file diff --git a/src/client/game/goods_table.tsx b/src/client/game/goods_table.tsx index 458ff530..1009b268 100644 --- a/src/client/game/goods_table.tsx +++ b/src/client/game/goods_table.tsx @@ -16,6 +16,9 @@ import { ImmutableMap } from "../../utils/immutable"; import { assert } from "../../utils/validate"; import { Username } from "../components/username"; import { goodStyle } from "../grid/good"; +import { readGoodColor } from "../grid/readGoodColor"; +import * as hexStyles from "../grid/hex.module.css"; +import { getCorners, polygon } from "../../utils/point"; import { useAction, useEmptyAction } from "../services/action"; import { useGame, useGameVersionState } from "../services/game"; import { @@ -119,75 +122,216 @@ export function GoodsTable() { } else if (!starter.isGoodsGrowthEnabled()) { return <>>; } + // build the 12 column elements, then render them grouped (white on left, black on right) + const columns = iterate(12, (i) => { + const cityGroup = i < 6 ? CityGroup.WHITE : CityGroup.BLACK; + const onRoll = OnRoll.parse((i % 6) + 1); + const city = cities.regularCities.get(cityGroup)?.[onRoll]; + const urbanizedCity = + cities.urbanizedCities.get(cityGroup)?.[onRoll]; + const letter = i < 2 || i >= 10 ? "" : numberToLetter(i - 2); + // determine a primary good color for this onRoll column + let primaryGood: Good | undefined = undefined; + // first try to find the actual City on the map with this onRoll/group so color matches the map hex + const mapCity = grid.cities().find((c) => + c.onRoll().some((r) => r.group === cityGroup && r.onRoll === onRoll), + ); + if (mapCity != null) primaryGood = mapCity.goodColors()[0]; + // next try availableCities (new urbanized city options) + if (primaryGood == null && Array.isArray(availableCities)) { + const avail = (availableCities as any[]).find((a) => + a.onRoll.some((r: any) => r.group === cityGroup && r.onRoll === onRoll), + ); + if (avail) { + primaryGood = Array.isArray(avail.color) ? avail.color[0] : avail.color; + } + } + // final fallback: use the goods currently in the city/urbanized city if present + if (primaryGood == null && city != null) { + for (const g of city) { + if (g != null) { + primaryGood = g as Good; + break; + } + } + } + if (primaryGood == null && urbanizedCity != null) { + for (const g of urbanizedCity) { + if (g != null) { + primaryGood = g as Good; + break; + } + } + } + // For the letter headers (A..H) use the availableCities list order so they match the Available Cities + const letterIndex = i - 2; + let letterGood: Good | undefined = primaryGood; + if (letter !== "" && Array.isArray(availableCities) && availableCities[letterIndex]) { + const avail = (availableCities as any[])[letterIndex]; + const colorVal = avail.color; + letterGood = Array.isArray(colorVal) ? colorVal[0] : colorVal; + } + + return ( +