feat(game): race exit warnings in the turn report (#12)
Surface the inactivity-removal countdown the rules promise but the engine never reported. A race within five turns of being auto-removed for inactivity gets a personal warning in its own report; every race within three turns is listed publicly to all participants. - model: Report.PersonalExitWarning + RacesLeavingSoon ([]RaceExitNotice) - fbs: RaceExitNotice table + Report.personal_exit_warning / races_leaving_soon (regenerated Go + TS bindings) - transcoder: encode/decode both fields - engine: ReportExitWarnings fills the recipient's TTL (1..5) and lists other non-extinct races with TTL 1..3, excluding the recipient itself - ui: danger-styled personal banner + "races leaving soon" section (hidden when empty), wired into the report view, EN/RU i18n - docs: rules.txt report-section list, FUNCTIONAL.md 6.4 + RU mirror Voluntary quit and idle timeout share the TTL countdown and are not distinguished, per the agreed scope.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
Phase 23 turn-report active view.
|
||||
|
||||
Composes the table of contents (`report/report-toc.svelte`) and the
|
||||
twenty section components that render each `GameReport` array. Each
|
||||
section components that render each `GameReport` array. Each
|
||||
section is its own component under `lib/active-view/report/` — the
|
||||
data shapes are too varied for one generic table, and the
|
||||
component-per-section seam matches Phase 23's targeted-test contract.
|
||||
@@ -11,8 +11,10 @@ Active-section highlighting lands here: an `IntersectionObserver`
|
||||
rooted on the viewport watches every `<section id="report-<slug>">`
|
||||
and updates a local `activeSlug` rune that drives the TOC highlight.
|
||||
|
||||
The 20-section list lives here as a single source of truth so the
|
||||
TOC and the body iterate the same data.
|
||||
The section list lives here as a single source of truth so the
|
||||
TOC and the body iterate the same data. One entry —
|
||||
`race-exit-warnings` — renders nothing when its list is empty, so its
|
||||
TOC item resolves to a no-op scroll on the rare turns it is hidden.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
@@ -20,7 +22,9 @@ TOC and the body iterate the same data.
|
||||
import ReportToc, {
|
||||
type TocEntry,
|
||||
} from "./report/report-toc.svelte";
|
||||
import PersonalExitBanner from "./report/personal-exit-banner.svelte";
|
||||
import SectionGalaxySummary from "./report/section-galaxy-summary.svelte";
|
||||
import SectionRaceExitWarnings from "./report/section-race-exit-warnings.svelte";
|
||||
import SectionVotes from "./report/section-votes.svelte";
|
||||
import SectionPlayerStatus from "./report/section-player-status.svelte";
|
||||
import SectionMySciences from "./report/section-my-sciences.svelte";
|
||||
@@ -43,6 +47,7 @@ TOC and the body iterate the same data.
|
||||
|
||||
const ENTRIES: readonly TocEntry[] = [
|
||||
{ slug: "galaxy-summary", titleKey: "game.report.section.galaxy_summary.title" },
|
||||
{ slug: "race-exit-warnings", titleKey: "game.report.section.race_exit_warnings.title" },
|
||||
{ slug: "votes", titleKey: "game.report.section.votes.title" },
|
||||
{ slug: "player-status", titleKey: "game.report.section.player_status.title" },
|
||||
{ slug: "my-sciences", titleKey: "game.report.section.my_sciences.title" },
|
||||
@@ -107,10 +112,12 @@ TOC and the body iterate the same data.
|
||||
</script>
|
||||
|
||||
<div class="report-view" data-testid="active-view-report">
|
||||
<PersonalExitBanner />
|
||||
<ReportToc entries={ENTRIES} {activeSlug} />
|
||||
|
||||
<div class="report-body" bind:this={bodyEl}>
|
||||
<SectionGalaxySummary />
|
||||
<SectionRaceExitWarnings />
|
||||
<SectionVotes />
|
||||
<SectionPlayerStatus />
|
||||
<SectionMySciences />
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
Personal exit-warning banner for the turn-report view. Mirrors the
|
||||
`lib/header/history-banner.svelte` alert pattern (sticky `aside`,
|
||||
design-token styling) but carries the local race's own inactivity
|
||||
countdown rather than a history notice.
|
||||
|
||||
Renders only when `report.personalExitWarning > 0`: the engine reports
|
||||
`1..5` turns remaining before the local race is auto-removed for
|
||||
inactivity, `0` when there is no warning. The copy is a personal alarm
|
||||
("you will be removed in N turns unless you submit orders"), distinct
|
||||
from the public `section-race-exit-warnings.svelte` list of other
|
||||
races leaving soon. Uses the danger token rather than the warning
|
||||
token because it is the recipient's own removal that is at stake.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import {
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
type RenderedReportSource,
|
||||
} from "$lib/rendered-report.svelte";
|
||||
|
||||
const rendered = getContext<RenderedReportSource | undefined>(
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
);
|
||||
const report = $derived(rendered?.report ?? null);
|
||||
const turnsLeft = $derived(report?.personalExitWarning ?? 0);
|
||||
</script>
|
||||
|
||||
{#if turnsLeft > 0}
|
||||
<aside
|
||||
class="exit-banner"
|
||||
data-testid="report-personal-exit-banner"
|
||||
role="alert"
|
||||
>
|
||||
<span class="message">
|
||||
{i18n.t("game.report.personal_exit_warning", {
|
||||
turns: String(turnsLeft),
|
||||
})}
|
||||
</span>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.exit-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1.25rem;
|
||||
padding: 0.6rem 0.9rem;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-danger);
|
||||
border: 1px solid var(--color-danger);
|
||||
border-radius: 6px;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.message {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<!--
|
||||
Report View — races leaving soon section. Surfaces the public
|
||||
`racesLeavingSoon` projection: every other race within three turns of
|
||||
being auto-removed for inactivity, with the number of turns each has
|
||||
left. This is the public counterpart to the personal exit-warning
|
||||
banner the report view renders at the top for the local race.
|
||||
|
||||
Unlike the other report sections, this one hides entirely when the
|
||||
list is empty rather than showing an empty-state line: an absent
|
||||
notice is the normal, healthy case for an active game, so a permanent
|
||||
"no races leaving" row would be noise. The section's TOC entry stays
|
||||
registered; clicking it while the section is hidden is a silent no-op
|
||||
through the table-of-contents' existing `getElementById` guard.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
import {
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
type RenderedReportSource,
|
||||
} from "$lib/rendered-report.svelte";
|
||||
|
||||
const rendered = getContext<RenderedReportSource | undefined>(
|
||||
RENDERED_REPORT_CONTEXT_KEY,
|
||||
);
|
||||
const report = $derived(rendered?.report ?? null);
|
||||
const rows = $derived(report?.racesLeavingSoon ?? []);
|
||||
</script>
|
||||
|
||||
{#if rows.length > 0}
|
||||
<section
|
||||
id="report-race-exit-warnings"
|
||||
class="grid-section"
|
||||
data-testid="report-section-race-exit-warnings"
|
||||
>
|
||||
<h2>{i18n.t("game.report.section.race_exit_warnings.title")}</h2>
|
||||
|
||||
<ul class="notices" data-testid="race-exit-warnings-list">
|
||||
{#each rows as r (r.race)}
|
||||
<li data-testid="race-exit-warnings-row" data-race={r.race}>
|
||||
{i18n.t("game.report.section.race_exit_warnings.notice", {
|
||||
race: r.race,
|
||||
turns: String(r.turnsLeft),
|
||||
})}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.grid-section h2 {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.notices {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.notices li {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -657,11 +657,16 @@ const en = {
|
||||
"game.report.toc.title": "sections",
|
||||
"game.report.toc.open": "show section list",
|
||||
"game.report.toc.close": "hide section list",
|
||||
"game.report.personal_exit_warning":
|
||||
"Inactivity warning: your race will be removed in {turns} turn(s) unless you submit orders.",
|
||||
"game.report.section.galaxy_summary.title": "galaxy summary",
|
||||
"game.report.section.galaxy_summary.field.turn": "turn",
|
||||
"game.report.section.galaxy_summary.field.size": "map size",
|
||||
"game.report.section.galaxy_summary.field.planets": "planet count",
|
||||
"game.report.section.galaxy_summary.field.race": "your race",
|
||||
"game.report.section.race_exit_warnings.title": "races leaving soon",
|
||||
"game.report.section.race_exit_warnings.notice":
|
||||
"{race} will be removed for inactivity in {turns} turn(s).",
|
||||
"game.report.section.votes.title": "votes",
|
||||
"game.report.section.votes.mine": "my votes",
|
||||
"game.report.section.votes.target": "I vote for",
|
||||
|
||||
@@ -658,11 +658,16 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.report.toc.title": "разделы",
|
||||
"game.report.toc.open": "показать список разделов",
|
||||
"game.report.toc.close": "скрыть список разделов",
|
||||
"game.report.personal_exit_warning":
|
||||
"Предупреждение о неактивности: ваша раса будет удалена через {turns} ход(ов), если вы не отправите приказы.",
|
||||
"game.report.section.galaxy_summary.title": "общие сведения о галактике",
|
||||
"game.report.section.galaxy_summary.field.turn": "ход",
|
||||
"game.report.section.galaxy_summary.field.size": "размер карты",
|
||||
"game.report.section.galaxy_summary.field.planets": "всего планет",
|
||||
"game.report.section.galaxy_summary.field.race": "ваша раса",
|
||||
"game.report.section.race_exit_warnings.title": "расы скоро покинут игру",
|
||||
"game.report.section.race_exit_warnings.notice":
|
||||
"{race} будет удалена за неактивность через {turns} ход(ов).",
|
||||
"game.report.section.votes.title": "голоса",
|
||||
"game.report.section.votes.mine": "мои голоса",
|
||||
"game.report.section.votes.target": "голосую за",
|
||||
|
||||
Reference in New Issue
Block a user