File size: 4,348 Bytes
81070c7
d0c158f
 
 
 
 
 
 
 
 
 
81070c7
d0c158f
81070c7
d0c158f
 
 
 
 
81070c7
 
 
d0c158f
 
 
 
 
81070c7
d0c158f
81070c7
 
d0c158f
 
81070c7
d0c158f
 
 
 
81070c7
 
 
 
 
 
 
 
 
 
d0c158f
81070c7
 
 
 
 
 
 
d0c158f
81070c7
d0c158f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81070c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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>
  );
}