Support Advanced Sapho Charity suspension

This commit is contained in:
Quintine committed 2026-10-27 23:36:27 +11:00
1 parent fcd93ca252
commit 70ef142577
19 files changed
+694 -49

No files matched your search

@@ -85,6 +85,60 @@ void test('native clean Charity first and last render their scope and submit onl
}
});
void test('own started Charity pause displays the retained claim and submits its native action', () => {
const source = native(true, true), game = source.game;
// Controlled custody: transfer the one physical Sapho to the actual claimant.
const donor = saphoCharityOrderPlayer(game, source.owner);
const claimant = saphoCharityOrderPlayer(game, source.current);
const index = donor.hand.findIndex(card => card.id === source.card);
assert.ok(index >= 0);
claimant.hand.push(donor.hand.splice(index, 1)[0]);
const view = viewGame(game, source.current), option = optionFor(view, 'last');
assert.equal(option.resumeOwn, true);
const html = panel(view);
assert.match(html, /Resume your charity last and discard Juice of Sapho/);
assert.match(html, /Your completed claim and its payment remain unchanged/);
assert.deepEqual(pressSapho(source.card, option), [{ type: 'card', card: source.card,
scope: 'charity', event: option.event, mode: 'last' }]);
assert.deepEqual(pressSapho(source.card, option, true), []);
});
void test('actual v3 Charity queue enables the current player and blocks the paused player', () => {
const source = native(true, true);
let game = source.game;
// The already-paid original Sapho changes hands without creating a card.
const donor = saphoCharityOrderPlayer(game, source.owner);
const claimant = saphoCharityOrderPlayer(game, source.current);
const cardIndex = donor.hand.findIndex(card => card.id === source.card);
assert.ok(cardIndex >= 0);
claimant.hand.push(donor.hand.splice(cardIndex, 1)[0]);
const pause = optionFor(viewGame(game, source.current), 'last');
game = actSaphoCharityOrder(game, source.current, pressSapho(source.card, pause)[0]!);
assert.ok(game.charityOrder && game.charityOrder.version === 3);
const actor = game.charityOrder.remaining[0]; assert.notEqual(actor, source.current);
const recipient = game.players.find(player => player.id !== actor)!.id;
const purse = saphoCharityOrderPlayer(game, actor).spice;
if (purse) game = actSaphoCharityOrder(game, actor, { type: 'bribe', target: recipient, amount: purse });
const active = viewGame(game, actor), waiting = viewGame(game, source.current);
assert.match(charity(active), /It is your Charity turn/);
assert.match(charity(waiting), /Your turn comes later/);
assert.deepEqual(pressCharity(waiting, 'charity-claim-submit'), { sent: [], disabled: true });
assert.deepEqual(pressCharity(waiting, 'charity-turn-finish'), { sent: [], disabled: true });
const claim = pressCharity(active, 'charity-claim-submit');
assert.deepEqual(claim, { sent: [{ type: 'charity' }], disabled: false });
game = actSaphoCharityOrder(game, actor, claim.sent[0]!);
const finish = pressCharity(viewGame(game, actor), 'charity-turn-finish');
assert.deepEqual(finish, { sent: [{ type: 'ready' }], disabled: false });
game = actSaphoCharityOrder(game, actor, finish.sent[0]!);
while (game.charityOrder?.remaining[0] !== source.current) {
const current = game.charityOrder?.remaining[0]; assert.ok(current);
game = actSaphoCharityOrder(game, current, { type: 'ready' });
}
const resumed = viewGame(game, source.current);
assert.deepEqual(pressCharity(resumed, 'charity-claim-submit'), { sent: [], disabled: true });
assert.deepEqual(pressCharity(resumed, 'charity-turn-finish'), { sent: [{ type: 'ready' }], disabled: false });
});
void test('native claimed current actor stays first while Sapho first or last queues only its later tail', () => {
for (const mode of ['first', 'last'] as const) {
const source = native(true, true), game = viewGame(source.game, source.owner), option = optionFor(game, mode);
+10 -6
View File
@@ -75,18 +75,22 @@ void test('an actual accepted current Charity claim stays ahead of both Sapho ta
}
});
void test('the real Sapho holder cannot split its own started Charity or choose its existing no-op position', () => {
void test('the real Sapho holder can pause its started Charity but cannot choose a no-op position', () => {
const f = fixture(); let game = advance(reload(f.game), state => clean(state) && state.charityOrder?.remaining[0] === f.owner);
assert.equal(game.active, f.owner);
assert.equal(viewGame(game, f.owner).saphoOptions.some(option => option.scope === 'charity' && option.mode === 'first'), false);
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode: 'first' });
game = claim(game, f.owner).game;
assert.equal(player(game, f.owner).charityTurn, game.turn);
assert.deepEqual(viewGame(game, f.owner).saphoOptions.filter(option => option.scope === 'charity'), []);
for (const mode of ['first', 'last'] as const)
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode });
assert.ok(player(game, f.owner).hand.some(card => card.id === f.card));
assert.equal(game.discard.some(card => card.id === f.card), false);
reject(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: currentEvent(game), mode: 'first' });
const option = viewGame(game, f.owner).saphoOptions.find(option => option.scope === 'charity' && option.mode === 'last');
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
const before = player(game, f.owner).spice;
game = act(game, f.owner, { type: 'card', card: f.card, scope: 'charity', event: option.event, mode: 'last' });
assert.equal(game.charityOrder?.remaining.at(-1), f.owner);
assert.equal(player(game, f.owner).spice, before);
assert.equal(player(game, f.owner).charityTurn, game.turn);
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
});
void test('stale, foreign and finished Charity requests reject before cost while an accepted packet disposes Sapho once', () => {
+3 -1
View File
@@ -64,7 +64,9 @@ void test('an accepted claim, including canceled BG Charity, locks the current a
const claimed = context([], ['a']);
rejects(claimed, frame, () => reorder(claimed, frame, { event: stale, player: 'd', mode: 'first' }));
rejects(claimed, frame, () => use(claimed, frame, 'a', 'first'));
rejects(claimed, frame, () => use(claimed, frame, 'a', 'last'));
const paused = use(claimed, frame, 'a', 'last');
assert.equal(paused.suspended, 'a');
assert.deepEqual(paused.order.remaining, ['b', 'c', 'd', 'a']);
const first = use(claimed, frame, 'd', 'first');
assert.equal(first.afterCurrent, 'a');
assert.deepEqual(first.order.remaining, ['a', 'd', 'b', 'c']);
+94
View File
@@ -0,0 +1,94 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import {
actSaphoCharityOrder as act, advanceSaphoCharityOrder as advance,
createSaphoCharitySemutaFixture as fixture, reactSaphoCharitySemuta as react,
saphoCharityOrderClean as clean, saphoCharityOrderInventory as inventory,
saphoCharityOrderPlayer as player,
} from './fixture-sapho-charity-order';
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
/** CONTROLLED supplemental custody: the inherited setup conserves original
* Sapho/Semuta cards rather than earning their purchase, then only the one
* physical Sapho is transferred to the eventual native Charity claimant. If
* that hand is full, one ordinary held card is exchanged to the donor so both
* retain legal hand counts. The bribe, Charity claim and all later actions are
* executed by the real engine. */
function claimedSource() {
const f = fixture();
assert.equal(f.nativeAcquisition, false);
assert.ok(f.staging.some(row => row.includes('CONTROLLED')));
const originalQueue = f.game.charityOrder!.remaining;
const actor = originalQueue.slice(0, -1).find(id => id !== f.holder && id !== f.owner);
assert.ok(actor, 'A non-Semuta, non-donor claimant retains a later opportunity');
let game = advance(structuredClone(f.game), state => clean(state) &&
state.phase === 2 && state.charityOrder?.remaining[0] === actor);
const donor = player(game, f.owner), claimant = player(game, actor);
const index = donor.hand.findIndex(card => card.id === f.card);
assert.ok(index >= 0);
const [sapho] = donor.hand.splice(index, 1);
if (claimant.hand.length >= handLimit(claimant)) {
const swap = claimant.hand.findIndex(card => card.id !== SEMUTA_DRUG_ID);
assert.ok(swap >= 0);
donor.hand.push(...claimant.hand.splice(swap, 1));
}
claimant.hand.push(sapho);
assert.ok(donor.hand.length <= handLimit(donor));
assert.ok(claimant.hand.length <= handLimit(claimant));
assert.deepEqual(inventory(game), f.originalInventory);
const recipient = game.players.find(seat => seat.id !== actor)!.id;
const paid = claimant.spice;
assert.ok(paid > 0, 'The claimant makes an actual bribe before Charity');
game = act(game, actor, { type: 'bribe', target: recipient, amount: paid });
assert.equal(player(game, actor).spice, 0);
game = act(game, actor, { type: 'charity' });
game = advance(game, clean);
assert.equal(player(game, actor).charityTurn, game.turn);
const option = viewGame(game, actor).saphoOptions.find(row => row.scope === 'charity' && row.mode === 'last');
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
return { f, game, actor, action: { type: 'card', card: f.card,
scope: 'charity', event: option.event, mode: 'last' } satisfies Action };
}
for (const claim of [false, true]) void test(
`controlled own Charity Sapho suspension survives Semuta ${claim ? 'claim' : 'pass'} without repeating the claim`, () => {
const { f, game: original, actor, action } = claimedSource();
const before = structuredClone(player(original, actor));
const paused = act(original, actor, action);
assert.equal(paused.pendingTreacheryDiscard?.continuation?.kind, 'saphoCharityDiscard');
assert.equal(paused.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.ok(paused.charityOrder?.version === 3);
assert.deepEqual(paused.charityOrder.paused, [actor]);
assert.equal(paused.charityOrder.remaining.at(-1), actor);
assert.equal(player(paused, actor).spice, before.spice);
assert.equal(player(paused, actor).charityTurn, before.charityTurn);
assert.equal(viewGame(paused, f.holder).semutaReaction?.canCommit, true);
reject(paused, actor, { type: 'ready' });
const frame = structuredClone(paused.charityOrder), ready = [...paused.ready];
let game = react(paused, f.holder, claim);
game = advance(game, clean);
assert.deepEqual(game.charityOrder, frame);
assert.deepEqual(game.ready, ready);
assert.equal(game.active, frame.remaining[0]);
assert.equal(player(game, actor).spice, before.spice);
assert.equal(player(game, actor).charityTurn, before.charityTurn);
assert.deepEqual(inventory(game), f.originalInventory);
assert.equal(player(game, f.holder).hand.some(card => card.id === f.card), claim);
assert.equal(game.discard.some(card => card.id === f.card), !claim);
if (claim) assert.equal(game.discard.filter(card => card.id === SEMUTA_DRUG_ID).length, 1);
game = advance(game, state => state.phase === 2 && clean(state) &&
state.charityOrder?.remaining[0] === actor);
assert.ok(game.charityOrder?.version === 3 && game.charityOrder.started);
assert.equal(player(game, actor).spice, before.spice);
reject(game, actor, { type: 'charity' });
game = act(game, actor, { type: 'ready' });
assert.deepEqual(inventory(game), f.originalInventory);
},
);
+114
View File
@@ -0,0 +1,114 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
actSaphoCharityOrder as act, advanceSaphoCharityOrder as advance,
createSaphoCharityOrderFixture as fixture, claimSaphoCharityOrder as claim,
finishSaphoCharityOrder as finish, saphoCharityOrderClean as clean,
saphoCharityOrderInventory as inventory, saphoCharityOrderPlayer as player,
} from './fixture-sapho-charity-order';
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
/** The original Advanced setup and paid cache purchase lead to an actual
* bribe and Charity claim. Only the already purchased Sapho changes hands:
* the conserved physical card is transferred from Emperor to current Richese. */
function source() {
const f = fixture({ started: true });
const game = structuredClone(f.game);
const donor = player(game, f.owner);
const index = donor.hand.findIndex(card => card.id === f.card);
assert.ok(index >= 0);
player(game, f.current).hand.push(...donor.hand.splice(index, 1));
assert.ok(f.claim?.steps.some(step => step.action.type === 'bribe'));
assert.ok(f.claim?.steps.some(step => step.action.type === 'charity'));
assert.equal(player(game, f.current).charityTurn, game.turn);
assert.equal(player(game, f.current).spice, 2);
assert.deepEqual(inventory(game), f.originalInventory);
return { f, game };
}
function ownLast(game: Game, actor: string, card: string): Action {
const option = viewGame(game, actor).saphoOptions.find(row => row.scope === 'charity' && row.mode === 'last');
assert.ok(option && option.scope === 'charity' && option.resumeOwn);
return { type: 'card', card, scope: 'charity', mode: 'last', event: option.event };
}
void test('Advanced own Charity suspension retains the earned claim and resumes once after the remaining players', () => {
const { f, game: original } = source();
const before = structuredClone(player(original, f.current));
const expected = [...original.charityOrder!.remaining.slice(1), f.current];
const declaration = ownLast(original, f.current, f.card);
assert.deepEqual(Object.keys(declaration).sort(), ['card', 'event', 'mode', 'scope', 'type']);
reject(original, f.current, { ...declaration, event: 'stale-charity-event' });
reject(original, f.current, { ...declaration, mode: 'first' });
let game = act(original, f.current, declaration);
assert.equal(game.charityOrder?.version, 3);
assert.deepEqual(game.charityOrder?.remaining, expected);
assert.ok(game.charityOrder?.version === 3);
assert.deepEqual(game.charityOrder.paused, [f.current]);
assert.equal(game.charityOrder.started, false);
assert.deepEqual(game.order, original.order);
assert.equal(player(game, f.current).spice, before.spice);
assert.equal(player(game, f.current).charityTurn, before.charityTurn);
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
assert.deepEqual(inventory(game), f.originalInventory);
reject(game, f.current, declaration);
game = advance(game, state => state.phase === 2 && clean(state) && state.charityOrder?.remaining[0] === f.current);
assert.ok(game.charityOrder?.version === 3);
assert.equal(game.charityOrder.started, true);
assert.equal(player(game, f.current).spice, before.spice);
assert.equal(player(game, f.current).charityTurn, game.turn);
reject(game, f.current, { type: 'charity' });
game = act(game, f.current, { type: 'ready' });
game = finish(game);
assert.equal(game.phase, 3);
assert.deepEqual(game.order, original.order);
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
assert.deepEqual(inventory(game), f.originalInventory);
});
void test('the actual Sapho purchaser can pause its own genuinely claimed Charity turn', () => {
const f = fixture();
let game = advance(structuredClone(f.game), state => state.phase === 2 && clean(state) &&
state.charityOrder?.remaining[0] === f.owner);
assert.ok(game.charityOrder && game.charityOrder.remaining.length > 1);
assert.ok(player(game, f.owner).hand.some(card => card.id === f.card));
const result = claim(game, f.owner);
assert.ok(result.steps.some(step => step.action.type === 'bribe'));
assert.ok(result.steps.some(step => step.action.type === 'charity'));
game = result.game;
const before = structuredClone(player(game, f.owner));
game = act(game, f.owner, ownLast(game, f.owner, f.card));
assert.equal(game.charityOrder?.remaining.at(-1), f.owner);
assert.equal(player(game, f.owner).spice, before.spice);
assert.equal(player(game, f.owner).charityTurn, game.turn);
game = advance(game, state => state.phase === 2 && clean(state) && state.charityOrder?.remaining[0] === f.owner);
reject(game, f.owner, { type: 'charity' });
game = finish(act(game, f.owner, { type: 'ready' }));
assert.equal(game.phase, 3);
assert.deepEqual(inventory(game), f.originalInventory);
});
void test('all four minimal bot profiles can use a projected Advanced own Charity suspension', () => {
const { f, game } = source();
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.current);
view.players.find(seat => seat.id === f.current)!.bot = difficulty;
const proposed = botActions(view).find(row => row.type === 'card' && row.card === f.card && row.scope === 'charity');
assert.ok(proposed, `${difficulty} proposes the physical owned Sapho`);
assert.equal('afterCurrent' in proposed, false);
assert.equal('resumeOwn' in proposed, false);
const next = act(game, f.current, proposed);
assert.equal(next.charityOrder?.remaining.at(-1), f.current);
assert.equal(player(next, f.current).spice, player(game, f.current).spice);
assert.equal(player(next, f.current).charityTurn, game.turn);
assert.equal(next.discard.filter(card => card.id === f.card).length, 1);
}
});
@@ -0,0 +1,75 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
CharityOrderError, createCharityOrder, finishCharityOrder,
reorderSaphoCharityOrder, saphoCharityOrderEvent, startCharityOrder,
validateCharityOrder, type CharityOrder, type CharityOrderContext,
} from '../game/charity-order';
const order = ['a', 'b', 'c', 'd'];
function context(ready: string[] = [], claimed: string[] = []): CharityOrderContext {
return { turn: 5, order, players: order.map(id => ({ id })), ready, claimed };
}
function use(ctx: CharityOrderContext, frame: CharityOrder, player: string, mode: 'first' | 'last') {
return reorderSaphoCharityOrder(ctx, frame, { event: saphoCharityOrderEvent(ctx, frame), player, mode });
}
void test('a started Charity claim can pause last and resume the same started opportunity', () => {
const initial = context(), frame = createCharityOrder(initial), claimed = context([], ['a']);
assert.equal(JSON.stringify(frame), '{"version":1,"turn":5,"order":["a","b","c","d"],"remaining":["a","b","c","d"]}');
const result = use(claimed, frame, 'a', 'last');
assert.equal(result.suspended, 'a'); assert.equal(result.afterCurrent, undefined);
assert.ok(result.order.version === 3);
assert.deepEqual(result.order.remaining, ['b', 'c', 'd', 'a']);
assert.deepEqual(result.order.paused, ['a']); assert.equal(result.order.started, false);
validateCharityOrder(claimed, result.order);
let current = result.order as CharityOrder, ready: string[] = [];
for (const actor of ['b', 'c', 'd']) {
current = finishCharityOrder(context(ready, ['a']), current, actor);
ready = [...ready, actor];
}
assert.ok(current.version === 3); assert.equal(current.started, true);
assert.deepEqual(current.paused, ['a']);
current = finishCharityOrder(context(ready, ['a']), current, 'a');
assert.ok(current.version === 3); assert.deepEqual(current.paused, []);
assert.deepEqual(current.remaining, []); assert.equal(current.started, false);
validateCharityOrder(context([...ready, 'a'], ['a']), current);
});
void test('a prior version-two Sapho order retains its history when the current claimant suspends', () => {
const ctx = context(), original = createCharityOrder(ctx);
const prior = use(ctx, original, 'd', 'first').order;
assert.ok(prior.version === 2); const saved = JSON.stringify(prior);
const started = startCharityOrder(ctx, prior, 'd');
const claimed = context([], ['d']); validateCharityOrder(claimed, started);
const result = use(claimed, started, 'd', 'last');
assert.ok(result.order.version === 3); assert.equal(result.order.uses.length, 2);
assert.deepEqual(result.order.uses[0], prior.uses[0]);
assert.deepEqual(result.order.remaining, ['a', 'b', 'c', 'd']);
assert.equal(JSON.stringify(prior), saved);
validateCharityOrder(claimed, JSON.parse(JSON.stringify(result.order)) as CharityOrder);
});
void test('a paused claimant moved first by another genuine use resumes with its claim still spent', () => {
const claimed = context([], ['a']);
const paused = use(claimed, createCharityOrder(context()), 'a', 'last').order;
const resumed = use(claimed, paused, 'a', 'first').order;
assert.ok(resumed.version === 3); assert.equal(resumed.remaining[0], 'a');
assert.equal(resumed.started, true); assert.deepEqual(resumed.paused, ['a']);
const complete = finishCharityOrder(claimed, resumed, 'a');
assert.ok(complete.version === 3); assert.deepEqual(complete.paused, []);
validateCharityOrder(context(['a'], ['a']), complete);
});
void test('started first, stale requests, altered custody and impossible history reject without mutation', () => {
const ctx = context([], ['a']), original = createCharityOrder(context());
const old = saphoCharityOrderEvent(context(), original);
const before = structuredClone({ ctx, original });
assert.throws(() => use(ctx, original, 'a', 'first'), CharityOrderError);
assert.throws(() => reorderSaphoCharityOrder(ctx, original, { event: old, player: 'a', mode: 'last' }), CharityOrderError);
const paused = use(ctx, original, 'a', 'last').order; assert.ok(paused.version === 3);
assert.throws(() => validateCharityOrder(context(), paused), CharityOrderError);
assert.throws(() => validateCharityOrder(ctx, { ...paused, paused: [] }), CharityOrderError);
assert.throws(() => validateCharityOrder(ctx, { ...paused, remaining: ['c', 'b', 'd', 'a'] }), CharityOrderError);
assert.deepEqual({ ctx, original }, before);
});
@@ -54,6 +54,12 @@ function press(option: Option, busy = false, card = sapho!.id): Action[] {
SaphoOrderButton({ card, option, busy, act(action) { actions.push(action); } }).props.onClick();
return actions;
}
function pressFinish(game: GameView, busy = false) {
const sent: Action[] = [];
const button = FinishRevivalTurn({ game, busy, act(action) { sent.push(action); } });
button.props.onClick();
return { sent, disabled: button.props.disabled };
}
void test('clean first and last Revival choices use their own labels and preserve completed-turn guidance', () => {
for (const mode of ['first', 'last'] as const) {
@@ -106,6 +112,38 @@ void test('own started Revival pause explains retained allowances and sends the
assert.deepEqual(press(option, true), []);
});
void test('actual v3 Revival queue enables the current player and blocks the paused player', () => {
const source = createSaphoRevivalOrderFixture({ started: true });
let game = structuredClone(source.game);
// Transfer the one physical card to the real claimant; no card is created.
const donor = game.players.find(player => player.id === source.owner)!;
const claimant = game.players.find(player => player.id === source.current)!;
const cardIndex = donor.hand.findIndex(card => card.id === source.card);
assert.ok(cardIndex >= 0);
claimant.hand.push(donor.hand.splice(cardIndex, 1)[0]);
const pause = viewGame(game, source.current).saphoOptions.find(option =>
option.scope === 'revival' && option.mode === 'last');
assert.ok(pause && pause.scope === 'revival' && pause.resumeOwn);
game = actSaphoRevivalOrder(game, source.current, press(pause, false, source.card)[0]!);
assert.ok(game.revivalOrder && game.revivalOrder.version === 3);
let actor = game.revivalOrder.remaining[0];
assert.notEqual(actor, source.current);
const waiting = viewGame(game, source.current);
assert.deepEqual(pressFinish(waiting), { sent: [], disabled: true });
while (actor !== source.current) {
const active = viewGame(game, actor);
assert.match(revival(active), /Current Revival actor:/);
const finish = pressFinish(active);
assert.deepEqual(finish, { sent: [{ type: 'ready' }], disabled: false });
game = actSaphoRevivalOrder(game, actor, finish.sent[0]!);
actor = game.revivalOrder?.remaining[0] ?? '';
assert.ok(actor);
}
const resumed = viewGame(game, source.current);
assert.deepEqual(pressFinish(resumed), { sent: [{ type: 'ready' }], disabled: false });
assert.deepEqual(pressFinish(resumed, true), { sent: [], disabled: true });
});
void test('Basic unstamped and completed controlled views offer no Revival order button even with stale projected options', () => {
const basic = projected(); basic.advanced = false; basic.revivalOrder = null;
const legacy = projected(); legacy.revivalOrder = null;