Allow Ecaz Ambassadors on the placed mobile stronghold

Share dynamic stronghold legality across placement, cancellation, bots and controls. Keep the HMS sheltered, preserve original fees and entry effects, and display its public Ambassador attached to the stronghold interior.

Cover genuine Basic/Advanced placement, paid movement entry, card continuations, prevention and native relocation. No Terror or direct-shipment permission is added.
This commit is contained in:
Quintine committed 2026-10-24 17:55:16 +11:00
1 parent 659e2a4a9a
commit 52832dc0fd
16 files changed
+1129 -55

No files matched your search

+6 -18
View File
@@ -143,6 +143,7 @@ import {
import { homeworldRevivalDestinations, quoteHomeworldRevivalDestination } from './homeworld-revival-destinations';
import { terrorEntrySignature, terrorSelectionSignature, validateTerrorEntrySignature } from './terror-entry-receipt';
import { arrivalOverlapSignature, overlapOwnsAmbassadorDiscard, overlapOwnsTerrorDiscard, overlapOwnsWormTerror } from './arrival-overlap';
import { ambassadorPlacementDestination, ambassadorPlacementDestinations, quoteEcazAmbassadorPlacement } from './ecaz-ambassador-placement';
import { GuildAmbassadorMarkerError, markerDestinations, markerOwner, quoteGuildAmbassadorMarker,
type GuildAmbassadorMarker, type GuildAmbassadorMarkerQuote } from './guild-ambassador-no-field';
import {
@@ -468,7 +469,6 @@ import {
import {
validateAmbassadors,
createAmbassadors,
placeAmbassador,
triggerAmbassador,
destroyAmbassador,
replenishAmbassadors,
@@ -7119,12 +7119,9 @@ function treacheryDiscardIntegrity(g: Game) {
['choam', 'ixians'].includes(entry.effect!) &&
['none', 'wormRide'].includes(entry.resume) &&
validAmbassadorResume(g, entry) &&
TERRITORIES.some(
(t) =>
t.id === entry.territory &&
t.type === 'stronghold' &&
t.sectors.includes(entry.sector),
) &&
ambassadorPlacementDestination(g, entry.territory).allowed &&
ambassadorPlacementDestination(g, entry.territory).stronghold &&
validGameLocation(g, entry.territory, entry.sector) &&
owner?.faction === 'ecaz' &&
beneficiary &&
(beneficiary.id === owner.id ||
@@ -21167,19 +21164,9 @@ function ambassadorPlacementQuote(
token: string,
to: string,
) {
const destination = TERRITORIES.find((t) => t.id === to);
requireRule(g.ecazAmbassadors, 'No Ambassador supply is available.');
try {
return placeAmbassador(g.ecazAmbassadors, token, {
turn: g.turn,
availableSpice: owner.spice,
destination: {
id: to,
stronghold: destination?.type === 'stronghold',
allowed: !!destination,
inStorm: !destination || destination.sectors.includes(g.storm),
},
});
return quoteEcazAmbassadorPlacement(g, owner, token, to);
} catch (error) {
throw new RuleError(
error instanceof Error ? error.message : 'Invalid Ambassador placement.',
@@ -36277,6 +36264,7 @@ export function viewGame(state: Game, id: string) {
ambassadors: g.ecazAmbassadors
? {
tokens: g.ecazAmbassadors.tokens.map((token) => ({ ...token })),
destinations: ambassadorPlacementDestinations(g),
nextCost:
(g.ecazAmbassadors.placement?.turn === g.turn
? g.ecazAmbassadors.placement.count