world-simulator / frontend /src /components /ScoreboardHeader.tsx
kikikita's picture
Refactor action handling and update UI elements
81070c7
Raw
History Blame Contribute Delete
4.35 kB
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>
);
}