| import { useMemo, useState } from "react"; |
|
|
| import { eventIcon, severityClass } from "../eventDecor"; |
| import type { GameLogEventSnapshot, WorldSnapshot } from "../types"; |
|
|
| type GameOverOverlayProps = { |
| snapshot: WorldSnapshot; |
| events: GameLogEventSnapshot[]; |
| }; |
|
|
| const RESULT_COPY: Record<string, { title: string; subtitle: string; tone: string }> = { |
| win_population: { |
| title: "THE WORLD PREVAILS", |
| subtitle: "The countries reached the population goal.", |
| tone: "win", |
| }, |
| win_survival: { |
| title: "THE WORLD ENDURES", |
| subtitle: "The countries survived the pressure of the world.", |
| tone: "win", |
| }, |
| lose: { |
| title: "THE WORLD HAS FALLEN", |
| subtitle: "No citizen drew breath at the end.", |
| tone: "lose", |
| }, |
| }; |
|
|
| export function GameOverOverlay({ snapshot, events }: GameOverOverlayProps) { |
| const [isDismissed, setIsDismissed] = useState(false); |
| const status = snapshot.simulation.game_status ?? "running"; |
| const copy = RESULT_COPY[status]; |
| const stats = snapshot.simulation.stats; |
| const totalDeaths = Object.values(stats?.deaths_by_cause ?? {}).reduce( |
| (sum, count) => sum + count, |
| 0, |
| ); |
| const betrayals = useMemo( |
| () => events.filter((event) => event.type === "steal" || event.type === "treasury_stolen").length, |
| [events], |
| ); |
| const storyEvents = useMemo( |
| () => |
| events |
| .filter((event) => |
| [ |
| "npc_born", |
| "npc_died", |
| "build_completed", |
| "house_destroyed", |
| "beast_killed", |
| "election_completed", |
| "policy_updated", |
| "cannon_crafted", |
| "cannon_fired", |
| "game_over", |
| ].includes(event.type), |
| ) |
| .slice(-14), |
| [events], |
| ); |
|
|
| if (!copy || isDismissed) { |
| return null; |
| } |
|
|
| return ( |
| <div className={`gameOverOverlay gameOver-${copy.tone}`} role="dialog" aria-label="Game over"> |
| <div className="gameOverCard"> |
| <h1>{copy.title}</h1> |
| <p className="gameOverSubtitle">{copy.subtitle}</p> |
| |
| <div className="gameOverStats"> |
| <Stat label="Countries" value={snapshot.countries?.length ?? 0} /> |
| <Stat label="Births" value={stats?.total_births ?? 0} /> |
| <Stat label="Deaths" value={totalDeaths} /> |
| <Stat label="Houses built" value={stats?.houses_built ?? 0} /> |
| <Stat label="Beasts slain" value={stats?.beasts_killed ?? 0} /> |
| <Stat label="Treasury raids" value={betrayals} /> |
| <Stat label="Peak population" value={snapshot.simulation.peak_population ?? 0} /> |
| </div> |
| |
| {Object.keys(stats?.deaths_by_cause ?? {}).length > 0 ? ( |
| <p className="gameOverCauses"> |
| Causes of death:{" "} |
| {Object.entries(stats?.deaths_by_cause ?? {}) |
| .map(([cause, count]) => `${cause.replaceAll("_", " ")} x${count}`) |
| .join(", ")} |
| </p> |
| ) : null} |
| |
| {storyEvents.length > 0 ? ( |
| <div className="gameOverStory"> |
| <h2>The world's story</h2> |
| <div className="gameOverStoryList"> |
| {storyEvents.map((event, index) => ( |
| <div className={`eventLine ${severityClass(event.severity)}`} key={index}> |
| <span className="eventTick">{event.tick}</span> |
| <span className="eventIcon">{eventIcon(event.type)}</span> |
| <span className="eventText">{event.summary}</span> |
| </div> |
| ))} |
| </div> |
| </div> |
| ) : null} |
| |
| <button type="button" className="gameOverDismiss" onClick={() => setIsDismissed(true)}> |
| Inspect the aftermath |
| </button> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function Stat({ label, value }: { label: string; value: number }) { |
| return ( |
| <div className="gameOverStat"> |
| <span className="gameOverStatValue">{value}</span> |
| <span className="gameOverStatLabel">{label}</span> |
| </div> |
| ); |
| } |
|
|