| import type { CountrySnapshot, WorldSnapshot } from "../types"; |
| import { formatModelStatus, modelStatusClass, shortModelLabel } from "./modelStatus"; |
| import { TooltipLabel } from "./TooltipLabel"; |
|
|
| const WIN_SURVIVAL_TICKS = 600; |
|
|
| type ScoreboardHeaderProps = { |
| snapshot: WorldSnapshot | null; |
| isWaitingForTick: boolean; |
| }; |
|
|
| export function ScoreboardHeader({ snapshot, isWaitingForTick }: ScoreboardHeaderProps) { |
| const simulation = snapshot?.simulation; |
| const countries = snapshot?.countries ?? []; |
| const population = simulation?.population ?? snapshot?.entities.length ?? 0; |
| const tick = snapshot?.tick ?? 0; |
| const survivalProgress = Math.min(1, tick / WIN_SURVIVAL_TICKS); |
| const famineUntil = simulation?.famine_until ?? -1; |
| const isFamineActive = famineUntil >= 0 && tick <= famineUntil; |
| const activityLabel = isWaitingForTick |
| ? "simulating..." |
| : (simulation?.last_tick_source ?? "idle"); |
|
|
| return ( |
| <header className="scoreboard" aria-label="Simulation status"> |
| <div className="scoreMeta"> |
| <span className="scoreTick" title="Current simulation tick"> |
| Tick {tick} |
| </span> |
| <span className="scoreActivity">{activityLabel}</span> |
| <span className="scoreTick">Population {population}</span> |
| {isFamineActive ? ( |
| <span className="famineBadge" title={`Food nodes halved until tick ${famineUntil}`}> |
| FAMINE ({famineUntil - tick}) |
| </span> |
| ) : null} |
| </div> |
| |
| <div className="countryGrid" aria-label="Countries"> |
| {countries.length > 0 ? ( |
| countries.map((country) => ( |
| <CountryCard |
| key={country.id} |
| country={country} |
| tick={tick} |
| rulerName={ |
| snapshot?.entities.find((entity) => entity.id === country.ruler_id)?.label ?? null |
| } |
| /> |
| )) |
| ) : ( |
| <div className="countryCard countryCardEmpty">Countries are loading</div> |
| )} |
| </div> |
| |
| <div className="winProgress" title={`Survival goal: ${WIN_SURVIVAL_TICKS} ticks`}> |
| <div className="winRow"> |
| <span className="winLabel">Survival {Math.min(tick, WIN_SURVIVAL_TICKS)}/{WIN_SURVIVAL_TICKS}</span> |
| <div className="winBar"> |
| <div |
| className="winFill winFillSurvival" |
| style={{ width: `${survivalProgress * 100}%` }} |
| /> |
| </div> |
| </div> |
| </div> |
| |
| {(simulation?.models ?? []).length > 0 ? ( |
| <div className="modelPills"> |
| {(simulation?.models ?? []).map((model) => ( |
| <TooltipLabel |
| key={model.id} |
| className={`modelPill ${modelStatusClass(model.status)}`} |
| value={`${model.label}: ${model.model ?? "unknown model"} - ${formatModelStatus(model.status)}`} |
| > |
| {`${shortModelLabel(model.label)} ${formatModelStatus(model.status)}`} |
| </TooltipLabel> |
| ))} |
| </div> |
| ) : null} |
| </header> |
| ); |
| } |
|
|
| type CountryCardProps = { |
| country: CountrySnapshot; |
| rulerName: string | null; |
| tick: number; |
| }; |
|
|
| function CountryCard({ country, rulerName, tick }: CountryCardProps) { |
| const treasury = country.treasury?.resources ?? {}; |
| const election = country.next_election_tick <= tick |
| ? "election active" |
| : `election T${country.next_election_tick}`; |
| const cannon = country.cannon |
| ? country.cannon.operator_id |
| ? `cannon: ${country.cannon.operator_id}` |
| : "cannon: no operator" |
| : "no cannon"; |
|
|
| return ( |
| <section className="countryCard" style={{ borderColor: country.color }}> |
| <div className="countryHead"> |
| <span className="countryBadge" style={{ background: country.color }}> |
| {country.badge} |
| </span> |
| <strong>{country.name}</strong> |
| <span>{country.citizen_ids.length} citizens</span> |
| </div> |
| <p className="countryPolicy" title={country.policy}> |
| {country.policy} |
| </p> |
| <div className="countryFacts"> |
| <span>Ruler: {rulerName ?? country.ruler_id ?? "none"}</span> |
| <span>{election}</span> |
| <span>treasury F{treasury.food ?? 0} H{treasury.herbs ?? 0} W{treasury.wood ?? 0} C{treasury.coins ?? 0}</span> |
| <span>{cannon}</span> |
| </div> |
| </section> |
| ); |
| } |
|
|