Show completed core actions and summarize notice bursts

Tag committed ordinary movement, shipping, revival, collection, battle,
native Guild transport and HMS relocation for the existing house notices
and optional sounds. Keep pending and canceled actions free of false
completion notices and preserve richer Smuggler and card-specific labels.

Bound feedback to three 1.5-second notices including the active notice.
Summarize older pending events with an exact count and retain individual
chronicle entries. Consume stale, duplicate, muted and hidden updates
without replay; clear visible and queued feedback when the page hides.
These changes add log metadata and cosmetic presentation only.

Validation: types, lint, 5137 offline tests and production build passed.
Independent review resolved one duplicate Smuggler notice. Nine engine
regressions and six queue/cursor/component tests cover the new behavior.
Two Basic/Advanced four-player games completed with 821 legal actions,
21 JSON restores and zero rejected actions; no AI tuning was done.

Browser QA 76CMLL8Z used genuine Basic setup and private real-action batches.
A seven-event burst displayed Revival plus four earlier actions, then
drained. Chronicle navigation and silent toggle/refresh were checked;
refresh kept the saved row identical. Phone notice bounds fit without
overflow. Transient phone text and actual background visibility were not
captured; screenshot and full visual acceptance remain open.
All 1,585 prior rooms, 3,353 seats and auxiliary records stayed unchanged.
One QA room and human seat were added; no reset, restart or automation.
This commit is contained in:
Quintine committed 2026-09-21 16:06:35 +10:00
1 parent 7136c8dd19
commit a3bc0ac837
12 files changed
+495 -48

No files matched your search

+10 -5
View File
@@ -9889,6 +9889,7 @@ function finishMovedGroup(g: Game, move: CompletedMovement) {
move.noField
? `${p.name} moved a concealed No-Field${n > 1 ? ` with ${n - 1} physical forces` : ''} from ${territory(origin).name} to ${territory(to).name}. Its value remains concealed.`
: `${p.name} moved ${n} forces from ${movementOrigins(move).map(source => territory(source).name).join(' and ')} to ${territory(to).name}.`,
{ faction: p.faction, name: 'Movement' },
);
intrusion(g, p, to);
if (movementOrigins(move).some(source => source !== to)) openTerritoryEntry(g, p, to, s, n, elite, 'movement');
@@ -13445,6 +13446,7 @@ function commitCollection(
log(
g,
`${faction(p.faction).name} collected ${receipt.collected} spice.`,
{ faction: p.faction, name: 'Spice collection' },
);
}
for (const effect of discovery.effects) {
@@ -15851,7 +15853,7 @@ function resolveBattle(g: Game) {
: `${faction(winner!.faction).name} won in ${combatLocationName(g, b.territory)} (${av}–${dv}${av === dv ? (strongholdEffect(g, winner!.id) === 'habbanya_ridge_sietch' ? ', Habbanya Stronghold advantage wins ties' : ', aggressor wins ties') : ''}).`,
stoneResult
? { faction: (isStoneBurner(aw) ? a : d).faction, name: 'Stone Burner' }
: undefined,
: { faction: winner!.faction, name: 'Battle' },
);
}
if (jacurutuIncome?.kind === 'income' && jacurutuIncome.amount > 0) {
@@ -17215,14 +17217,15 @@ function finishRevival(
`${rule.name} accrued ${token.spice} spice for ${getPlayer(g, income.owner).name}, payable at phase end.`,
);
}
log(g, `${p.name} revived ${n} forces.${g.homeworlds ? ` ${revival.free} were free; ${n - revival.free} were paid. ${payer.name} paid ${revival.cost} spice. Future revival requests use the resulting Homeworld population.` : ''}`);
log(g, `${p.name} revived ${n} forces.${g.homeworlds ? ` ${revival.free} were free; ${n - revival.free} were paid. ${payer.name} paid ${revival.cost} spice. Future revival requests use the resulting Homeworld population.` : ''}`,
{ faction: p.faction, name: 'Revival' });
stageHomeworldRevivalReturn(g, p, source, group, deploymentGrant);
} else {
if (revival.kind === 'kwisatz') {
p.kwisatz!.dead = false;
p.kwisatz!.revivalCycle =
Math.max(p.revivalCycle, p.kwisatz!.revivalCycle ?? 1) + 1;
log(g, `${p.name} revived Kwisatz Haderach.`);
log(g, `${p.name} revived Kwisatz Haderach.`, { faction: p.faction, name: 'Revival' });
} else {
const leader = g.players
.flatMap((owner) => owner.leaders)
@@ -17242,6 +17245,7 @@ function finishRevival(
log(
g,
`${p.name} revived ${leader.name}${revival.kind === 'foreignGhola' ? ' as a ghola' : ''}.`,
{ faction: p.faction, name: 'Revival' },
);
}
p.leaderRevived = true;
@@ -19092,7 +19096,7 @@ function commitShipment(g: Game, shipment: PendingShipment) {
: `${p.name} shipped ${n} forces to ${territory(to).name}, sector ${s}.${shipment.guildSecretEvent ? ` Guild Secret Ally is spent; Guild shipping prices cost ${cost} spice paid to the bank.` : ''}${shipment.nexusEvent ? ` Richese Secret Ally priced the ${n} physical forces as one: ${cost} spice.` : ''}${homeworldOrigins ? ` Homeworld sources: ${homeworldOrigins}. Total shipment cost: ${cost} spice.` : ''}`,
shipment.noField || shipment.alliedNoField
? { faction: 'richese', name: 'No-Field shipment' }
: shipment.guildSecretEvent ? {faction:'guild',name:'Secret Ally shipment'} : shipment.nexusEvent ? {faction:'richese',name:'Secret Ally shipment'} : undefined,
: shipment.guildSecretEvent ? {faction:'guild',name:'Secret Ally shipment'} : shipment.nexusEvent ? {faction:'richese',name:'Secret Ally shipment'} : shipment.smuggler ? undefined : { faction: p.faction, name: 'Shipment' },
);
if (shipment.smuggler) log(g,
`${p.name}'s Smuggler included one free accompanying force: ${n} physical forces left reserves, priced as ${n - 1} for ${cost} spice. The destination was empty before arrival.`,
@@ -19181,6 +19185,7 @@ function relocateMobileStronghold(
log(
g,
`${p.name} moved the Hidden Mobile Stronghold to ${territory(loc.territory).name}, sector ${loc.sector}, collecting ${collected} spice.`,
{ faction: p.faction, name: 'Hidden Mobile Stronghold' },
);
}
function choamWorthlessBlocked(g: Game, card: string) {
@@ -23829,7 +23834,7 @@ function applyActionInner(
p.shipped = true;
g.karamaShipping = null;
log(g, `${p.name} used Guild transport for ${n} forces.${guildSecretEvent ? ` Guild Secret Ally is spent; ${cost} spice was paid to the bank. Their ordinary movement remains available.` : ''}`,
guildSecretEvent ? {faction:'guild',name:'Secret Ally transport'} : undefined);
guildSecretEvent ? {faction:'guild',name:'Secret Ally transport'} : { faction: p.faction, name: 'Guild transport' });
if (to !== 'reserves') {
intrusion(g, p, to);
if (origin !== to)
+5 -3
View File
@@ -1362,8 +1362,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'After a battle resolves, the server calculates casualty allocations consistent with the revealed wheel and spice support. If exactly one allocation is legal, it applies that allocation without asking for an identical confirmation.',
'If several physical casualty combinations are legal, the winner still chooses. Automatic casualties preserve later optional Ixian substitutions, winner card cleanup and Moritani ally retention; they do not choose those benefits for you.',
'Newly received tagged events queue in order, with the acting house and event name shown for approximately 1.5 seconds each. Use Automatic action notices to turn the display on or off. The preference belongs to this browser; hiding notices does not disable automatic rule settlement or change AI timing.',
'The notice is cosmetic and does not stop game progression. Reduced-motion settings suppress its fade. Reloading treats existing events as a baseline rather than replaying them. Events received while notices are off are consumed; turning notices on does not replay them. The bounded queue keeps the newest pending events if an unusually large backlog exceeds its capacity.',
'Current notice events cover automatic battle casualties, successful Ecaz Ambassador placement, Emperor auction income, Harkonnen bonus-card draws and private full-plan inspection availability and supported successful Ambassador effects. The Harkonnen event reports the draw without exposing its private card identity. The inspection notice reveals no plan details; the entitled player reads those in the persistent private panel. Uncancelable responses now settle automatically as described in Automatic allowance of powers. Successful powers without a notice tag remain outside the current notice coverage.',
'The notice is cosmetic and does not stop game progression. Reduced-motion settings suppress its fade. Reloading treats existing events as a baseline rather than replaying them. Events received while notices are off are consumed; turning notices on does not replay them. At most three notices, including the visible one, remain queued. Bursts summarize older pending actions with a count while their details remain in the table chronicle. Hiding the page clears old notices and consumes updates silently.',
'Current notice events include committed ordinary movement, shipment, revival, collection and battle results, native Guild transport and Hidden Mobile Stronghold relocation. Declared or canceled actions do not announce an uncompleted result. Existing notices also cover automatic casualties, successful Ecaz Ambassador placement, supported successful Ambassador effects, Emperor auction income, Harkonnen bonus-card draws and private full-plan inspection availability. The Harkonnen draw is announced without exposing its private card identity. Notices reveal no private card identities or plan details. Uncancelable responses now settle automatically as described in Automatic allowance of powers. Successful powers without a notice tag remain outside the current coverage.',
],
checklist: [
{
@@ -1376,7 +1376,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Player controls',
status: 'Implemented',
detail:
'Multiple allocations and optional continuations retain controls. The cosmetic 1.5-second house notice has an on/off button and reduced-motion behavior.',
'Multiple allocations and optional continuations retain controls. The cosmetic 1.5-second house notice has an on/off button, reduced-motion behavior and bounded burst summaries.',
},
{
area: 'AI',
@@ -1399,6 +1399,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/automatic-casualties.test.ts',
'tests/moritani-retention.test.ts',
'tests/duke-vidal-engine.test.ts',
'tests/completed-action-events.test.ts',
'tests/action-notice-queue.test.tsx',
],
},
],