Preserve Basic shared arrival reactions through Semuta
Admit the original ordered Ecaz/Moritani entry in bare Basic Richese games, retain both native opportunities during fresh discard recovery, and bind each suspended child to its actual arrival. Show the current discard response in the table banner. Cover real five-seat setup and placements, both owner orders, physical payouts/draws/gifts, persistent controls and minimal legal bot participation.
This commit is contained in:
1 parent
7b9938219d
commit
4b2f4f3fa3
12 files changed
+815
-23
No files matched your search
@@ -2,6 +2,8 @@ import type { Game } from './engine';
|
||||
|
||||
type Overlap = NonNullable<Game['pendingArrivalOverlap']>;
|
||||
type TerrorEntry = NonNullable<Game['pendingTerrorEntry']>;
|
||||
type AmbassadorEntry = NonNullable<Game['pendingAmbassador']>;
|
||||
type DiscardContext = Pick<Game, 'pendingArrivalOverlap' | 'turn' | 'phase' | 'order' | 'players'>;
|
||||
|
||||
/** The immutable trigger, not later owner choices or child stages. */
|
||||
export function arrivalOverlapSignature(record: Overlap): string {
|
||||
@@ -11,6 +13,42 @@ export function arrivalOverlapSignature(record: Overlap): string {
|
||||
record.ambassadorToken, record.terrorTokens]);
|
||||
}
|
||||
|
||||
/** A fresh discard belongs to the currently active owner of one ordinary entry.
|
||||
* The parent keeps the other owner's opportunity while the child is suspended. */
|
||||
function ordinaryDiscardOverlap(g: DiscardContext, owner: Overlap['first']): Overlap | null {
|
||||
const overlap = g.pendingArrivalOverlap;
|
||||
if (!overlap || overlap.signature !== arrivalOverlapSignature(overlap) ||
|
||||
overlap.turn !== g.turn || overlap.phase !== g.phase || g.phase !== 5 ||
|
||||
overlap.resume !== 'none' || !['shipment', 'movement'].includes(overlap.cause) ||
|
||||
!['first', 'second'].includes(overlap.stage)) return null;
|
||||
const ecaz = g.order.indexOf(overlap.ecaz), moritani = g.order.indexOf(overlap.moritani);
|
||||
if (ecaz < 0 || moritani < 0 || ecaz === moritani ||
|
||||
overlap.first !== (ecaz < moritani ? 'ambassador' : 'terror') ||
|
||||
!g.players.some(p => p.id === overlap.ecaz && p.faction === 'ecaz') ||
|
||||
!g.players.some(p => p.id === overlap.moritani && p.faction === 'moritani')) return null;
|
||||
const active = overlap.stage === 'first' ? overlap.first :
|
||||
overlap.first === 'ambassador' ? 'terror' : 'ambassador';
|
||||
return active === owner ? overlap : null;
|
||||
}
|
||||
|
||||
export function overlapOwnsAmbassadorDiscard(g: DiscardContext, entry: AmbassadorEntry): boolean {
|
||||
const overlap = ordinaryDiscardOverlap(g, 'ambassador');
|
||||
return !!overlap && entry.stage === 'cards' && entry.resume === 'none' &&
|
||||
entry.event === overlap.ambassadorEvent && entry.owner === overlap.ecaz &&
|
||||
entry.token === overlap.ambassadorToken && entry.entrant === overlap.entrant &&
|
||||
entry.territory === overlap.territory && entry.sector === overlap.sector &&
|
||||
entry.arrivalCause === overlap.cause && entry.turn === overlap.turn && entry.phase === overlap.phase;
|
||||
}
|
||||
|
||||
export function overlapOwnsTerrorDiscard(g: DiscardContext, entry: TerrorEntry, owner: string): boolean {
|
||||
const overlap = ordinaryDiscardOverlap(g, 'terror');
|
||||
return !!overlap && owner === overlap.moritani && entry.resume === 'none' &&
|
||||
overlap.terrorTokens.includes(entry.token) && entry.entrant === overlap.entrant &&
|
||||
entry.territory === overlap.territory && entry.sector === overlap.sector &&
|
||||
entry.amount === overlap.amount && entry.elite === overlap.elite &&
|
||||
entry.cause === overlap.cause && entry.turn === overlap.turn && entry.phase === overlap.phase;
|
||||
}
|
||||
|
||||
/** The overlap owns the worm return while either serialized Terror prompt is live. */
|
||||
export function overlapOwnsWormTerror(
|
||||
g: Pick<Game, 'pendingArrivalOverlap' | 'turn' | 'phase'>,
|
||||
|
||||
Reference in new issue
Block a user