Polish map & UI into a command-center war-room; rewire live LLM endpoint
Map (WorldMap): - Keep the near-black OpenFreeMap `dark` basemap (verified HTTP 200; the suggested dark-matter / dark-outdoors / liberty-dark variants 404 on that host) so the choropleth nation colors pop - Stable per-nation fills at 0.8 opacity, thin subtle borders, and a brighter thicker amber border for the player nation so "this is me" is always obvious - Hover brightens a region and shows a color-chipped owner + key stats popup; click opens a full dossier popup and focuses the left panel on that nation - Transient, self-cleaning flash (pulsing amber outline) over regions whose ownership changed this turn - Bottom-left legend auto-generated from the region map; per-scenario initial camera (Europe for the WWII scenarios, wider for Cold War / Modern / Aegis) Command-center UI: - New header bar: scenario title, prominent turn + date, the player nation (name + color chip), and a live Idle / "Simulating…" status indicator - 3-column operations-console grid: nation dossier (left) / dominant map (center) / command deck + event feed (right); stacks map-first on mobile - EventFeed: one card per event with a category icon, a colored outcome badge (success/failure/partial/neutral), a nation-colored accent edge, and the turn + date; new events slide in and flash at the top - NationPanel: value-colored animated stat bars (red → green), territory as color-coded region chips, and diplomacy as a list with a colored indicator per relation (ally/rival/war/neutral) - ActionPanel: a "Your move" composer with one prominent Send button and a spinner + "turns can take a few seconds" hint while a turn resolves - sonner error toasts: a 502 / model-down surfaces as "AI temporarily unavailable — try again" Live LLM rewire (server, previously uncommitted): - Default endpoint https://ai.kkhost.pl/v1 (vLLM, no API key); reasoning model qwen3.8-27b; always send a generous max_tokens (4000) so it finishes its chain-of-thought before emitting content; read only `content`, ignore `reasoning`. docker-compose + AGENTS.md updated to match. Tests: the deterministic Playwright e2e gains assertions for the dossier stat bars, the map legend (all nations), and the event-card type icon + outcome badge; it stays fully deterministic under LLM_STUB=1.
This commit is contained in:
@@ -3,7 +3,16 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/
|
||||
import StatBar from './StatBar';
|
||||
import { nationColor } from '@shared/protocol';
|
||||
import { RELATION_COLORS } from '@/lib/colors';
|
||||
import type { Nation } from '@shared/types';
|
||||
import type { Nation, RelationStatus } from '@shared/types';
|
||||
|
||||
/** Human labels for a diplomatic status (the raw value is kept in data-attrs). */
|
||||
const RELATION_LABEL: Record<RelationStatus, string> = {
|
||||
ally: 'Ally',
|
||||
neutral: 'Neutral',
|
||||
rival: 'Rival',
|
||||
hostile: 'Hostile',
|
||||
war: 'At war',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
nation: Nation;
|
||||
@@ -13,10 +22,11 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* A single nation's dossier: identity, the four 0–100 stat bars (colored by the
|
||||
* nation's stable choropleth color), territory count, and its diplomatic posture
|
||||
* toward every other nation. Used for the player (highlighted) or any inspected
|
||||
* nation.
|
||||
* A single nation's dossier: identity + color chip, the four 0–100 stat bars
|
||||
* (colored by value: low = red → high = green, animated on change), territory as
|
||||
* color-coded region chips, and diplomacy as a list with a colored indicator per
|
||||
* relation (ally = green, rival = amber, war = red, neutral = slate). Used for
|
||||
* the player (highlighted) or any inspected nation.
|
||||
*/
|
||||
export default function NationPanel({ nation, nations, isPlayer }: Props) {
|
||||
const color = nationColor(nation.id);
|
||||
@@ -24,67 +34,109 @@ export default function NationPanel({ nation, nations, isPlayer }: Props) {
|
||||
const regions = nation.territory;
|
||||
|
||||
return (
|
||||
<Card data-testid="nation-panel" className={isPlayer ? 'gap-4 border-primary/40' : 'gap-4'}>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="inline-block size-3.5 rounded-full" style={{ background: color }} />
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-lg">{nation.name}</CardTitle>
|
||||
<Card data-testid="nation-panel" className={isPlayer ? 'border-primary/40' : undefined}>
|
||||
<CardHeader className="space-y-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className="inline-block size-4 shrink-0 rounded-md ring-2 ring-white/10"
|
||||
style={{ background: color }}
|
||||
data-testid="nation-color-chip"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-0.5">
|
||||
<CardTitle className="truncate text-lg">{nation.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{nation.type} · {regions.length} region{regions.length === 1 ? '' : 's'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{isPlayer && (
|
||||
<Badge variant="default" className="shrink-0">
|
||||
Your nation
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant={isPlayer ? 'default' : 'secondary'} className="shrink-0">
|
||||
{isPlayer ? 'Your nation' : 'Other'}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-x-6 gap-y-2 sm:grid-cols-2">
|
||||
<StatBar label="Military" value={nation.stats.military} color={color} testId={isPlayer ? 'stat-military' : undefined} />
|
||||
<StatBar label="Economy" value={nation.stats.economy} color={color} testId={isPlayer ? 'stat-economy' : undefined} />
|
||||
<StatBar label="Stability" value={nation.stats.stability} color={color} testId={isPlayer ? 'stat-stability' : undefined} />
|
||||
<StatBar label="Population" value={nation.stats.population} color={color} testId={isPlayer ? 'stat-population' : undefined} />
|
||||
</div>
|
||||
<CardContent className="space-y-5">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Status
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
<StatBar
|
||||
label="Military"
|
||||
value={nation.stats.military}
|
||||
testId={isPlayer ? 'stat-military' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Economy"
|
||||
value={nation.stats.economy}
|
||||
testId={isPlayer ? 'stat-economy' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Stability"
|
||||
value={nation.stats.stability}
|
||||
testId={isPlayer ? 'stat-stability' : undefined}
|
||||
/>
|
||||
<StatBar
|
||||
label="Population"
|
||||
value={nation.stats.population}
|
||||
testId={isPlayer ? 'stat-population' : undefined}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{regions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Territory</h3>
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Territory
|
||||
</h3>
|
||||
{regions.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{regions.map((code) => (
|
||||
<span
|
||||
key={code}
|
||||
className="rounded border px-1.5 py-0.5 text-xs tabular-nums"
|
||||
style={{ borderColor: `${color}66`, color }}
|
||||
style={{ borderColor: `${color}66`, color, background: `${color}12` }}
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No territory.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{relations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Diplomacy</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<section>
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Diplomacy
|
||||
</h3>
|
||||
{relations.length > 0 ? (
|
||||
<ul className="space-y-1.5" data-testid="relations-list">
|
||||
{relations.map(([id, status]) => {
|
||||
const other = nations[id];
|
||||
const c = RELATION_COLORS[status] ?? '#94a3b8';
|
||||
return (
|
||||
<span
|
||||
<li
|
||||
key={id}
|
||||
className="rounded-md border px-2 py-0.5 text-xs"
|
||||
style={{ borderColor: `${c}66`, color: c }}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
data-testid="relation-row"
|
||||
data-status={status}
|
||||
>
|
||||
{other?.name ?? id}: {status}
|
||||
</span>
|
||||
<span className="inline-block size-2.5 shrink-0 rounded-full" style={{ background: c }} />
|
||||
<span className="min-w-0 flex-1 truncate text-foreground/85">
|
||||
{other?.name ?? id}
|
||||
</span>
|
||||
<span className="shrink-0 text-xs font-medium" style={{ color: c }}>
|
||||
{RELATION_LABEL[status] ?? status}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">No diplomatic relations yet.</p>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user