Initial commit
This commit is contained in:
commit
8c709fb52f
771 files changed
+222983
No files matched your search
@@ -0,0 +1,142 @@
|
||||
import type { Card } from './cards';
|
||||
import { richeseCardDefinition } from './richese-cards';
|
||||
|
||||
export type BattleWeaponSide = 'attacker' | 'defender';
|
||||
export type EffectiveBattleWeapon = {
|
||||
physicalId: string | null;
|
||||
copiedFrom: string | null;
|
||||
kind: Card['kind'] | 'stoneBurner' | null;
|
||||
choice: 'poisonTooth' | 'stoneBurner' | null;
|
||||
};
|
||||
/** Public name used by effective attack/defense integration. */
|
||||
export type EffectiveWeapon = EffectiveBattleWeapon;
|
||||
type SelectedBattleCards = { weapon?: Card; defense?: Card };
|
||||
export type BattleWeaponResolution = {
|
||||
attacker: EffectiveBattleWeapon;
|
||||
defender: EffectiveBattleWeapon;
|
||||
choiceOrder: {
|
||||
side: BattleWeaponSide;
|
||||
kind: 'poisonTooth' | 'stoneBurner';
|
||||
}[];
|
||||
error: string | null;
|
||||
};
|
||||
const mirrorId = 'richese-mirror-weapon';
|
||||
const isMirror = (card?: Card) =>
|
||||
!!card && richeseCardDefinition(card)?.card.effect === 'mirrorWeapon';
|
||||
const claimsMirror = (card: Card) =>
|
||||
card.id === mirrorId ||
|
||||
card.name === 'Mirror Weapon' ||
|
||||
card.effect === 'mirrorWeapon';
|
||||
const empty = (card?: Card): EffectiveBattleWeapon => ({
|
||||
physicalId: card?.id ?? null,
|
||||
copiedFrom: null,
|
||||
kind: null,
|
||||
choice: null,
|
||||
});
|
||||
|
||||
/** The caller validates whole plans. Here Chemistry's selected weapon role must
|
||||
* have its original second defense; a copied effect does not require that card.
|
||||
*/
|
||||
function originalWeapon({
|
||||
weapon,
|
||||
defense,
|
||||
}: SelectedBattleCards): EffectiveBattleWeapon {
|
||||
const result = empty(weapon);
|
||||
if (!weapon || isMirror(weapon)) return result;
|
||||
const canonical = richeseCardDefinition(weapon);
|
||||
if (canonical) {
|
||||
if (canonical.card.effect === 'stoneBurner') {
|
||||
result.kind = 'stoneBurner';
|
||||
result.choice = 'stoneBurner';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// A tampered Richese physical identity cannot acquire a base attack by changing kind.
|
||||
if (weapon.id.startsWith('richese-')) return result;
|
||||
switch (weapon.kind) {
|
||||
case 'projectile':
|
||||
case 'poison':
|
||||
case 'lasgun':
|
||||
case 'poisonBlade':
|
||||
case 'weirdingWay':
|
||||
case 'artillery':
|
||||
case 'poisonTooth':
|
||||
result.kind = weapon.kind;
|
||||
break;
|
||||
case 'chemistry': {
|
||||
const validDefense =
|
||||
defense &&
|
||||
defense.id !== weapon.id &&
|
||||
([
|
||||
'shield',
|
||||
'snooper',
|
||||
'worthless',
|
||||
'shieldSnooper',
|
||||
'weirdingWay',
|
||||
'chemistry',
|
||||
].includes(defense.kind) ||
|
||||
richeseCardDefinition(defense)?.card.effect === 'portableSnooper');
|
||||
if (validDefense) result.kind = weapon.kind;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.kind === 'poisonTooth') result.choice = 'poisonTooth';
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Resolve only revealed selected weapon roles; never replace physical cards.
|
||||
* This establishes a copy-first dependency, not general battle turn order.
|
||||
* Without that dependency choiceOrder follows input attacker/defender order;
|
||||
* callers retain the existing ordinary storm-order choice policy as required.
|
||||
* This helper decides neither activation answers nor physical-card retention.
|
||||
*/
|
||||
export function resolveBattleWeapons(input: {
|
||||
attacker: SelectedBattleCards;
|
||||
defender: SelectedBattleCards;
|
||||
}): BattleWeaponResolution {
|
||||
const result: BattleWeaponResolution = {
|
||||
attacker: empty(input.attacker.weapon),
|
||||
defender: empty(input.defender.weapon),
|
||||
choiceOrder: [],
|
||||
error: null,
|
||||
};
|
||||
const cards = [
|
||||
input.attacker.weapon,
|
||||
input.attacker.defense,
|
||||
input.defender.weapon,
|
||||
input.defender.defense,
|
||||
].filter((card): card is Card => !!card);
|
||||
if (cards.some((card) => claimsMirror(card) && !isMirror(card))) {
|
||||
result.error = 'Mirror Weapon must match its canonical physical identity.';
|
||||
return result;
|
||||
}
|
||||
if (cards.filter(isMirror).length > 1) {
|
||||
result.error =
|
||||
'Only one physical Mirror Weapon exists; duplicated custody is invalid.';
|
||||
return result;
|
||||
}
|
||||
result.attacker = originalWeapon(input.attacker);
|
||||
result.defender = originalWeapon(input.defender);
|
||||
for (const side of ['attacker', 'defender'] as const) {
|
||||
if (!isMirror(input[side].weapon)) continue;
|
||||
const other = side === 'attacker' ? 'defender' : 'attacker';
|
||||
const original = result[other];
|
||||
if (original.kind !== null) {
|
||||
result[side] = {
|
||||
physicalId: input[side].weapon!.id,
|
||||
copiedFrom: original.physicalId,
|
||||
kind: original.kind,
|
||||
choice: original.choice,
|
||||
};
|
||||
}
|
||||
}
|
||||
const order: BattleWeaponSide[] =
|
||||
result.defender.copiedFrom && result.defender.choice
|
||||
? ['defender', 'attacker']
|
||||
: ['attacker', 'defender'];
|
||||
for (const side of order) {
|
||||
const choice = result[side].choice;
|
||||
if (choice) result.choiceOrder.push({ side, kind: choice });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user