feat(swdbg): rework TIE Fighter, Vader, Cassian, Luke; add capital gate & force SFX
- Add `requiresCapital` ability gate to card definitions - Rework TIE Fighter: constant boost → action that draws a card if you control a capital ship - Rework Darth Vader: constant +4 attack while Force is with you (removed damage ability) - Rework Cassian Andor: onBounty trigger forces opponent discard on galaxy row kills - Rework Luke Skywalker: onPlay destroy enemy capital ship if Force is with you - Centralize ability requirement checks via `abilityGatePasses()` helper - Apply gate checks to onBounty, onPlay, and onReveal triggers - Add Force track shift SFX (light/dark variations) with randomized playback - Update verification tests for new card behaviors
This commit is contained in:
parent
2287b3cafe
commit
afd1681245
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -25,7 +25,7 @@
|
||||||
" ability machine-readable effect (see OPS below) or null",
|
" ability machine-readable effect (see OPS below) or null",
|
||||||
" text display-only rules text — keep in sync with `ability`",
|
" text display-only rules text — keep in sync with `ability`",
|
||||||
"",
|
"",
|
||||||
"ABILITY SHAPE: { timing, op, ...params, cost?, requiresForce? }",
|
"ABILITY SHAPE: { timing, op, ...params, cost?, requiresForce?, requiresCapital? }",
|
||||||
" timing 'action' = use once per turn while in play (the default)",
|
" timing 'action' = use once per turn while in play (the default)",
|
||||||
" 'constant' = always on while in play ('While ...')",
|
" 'constant' = always on while in play ('While ...')",
|
||||||
" 'onPlay' = triggers when the card is played",
|
" 'onPlay' = triggers when the card is played",
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
" 'onReveal' = bases only: when the base is revealed",
|
" 'onReveal' = bases only: when the base is revealed",
|
||||||
" cost { exileSelf: true } and/or { exileFromHand: 1 } paid before the op",
|
" cost { exileSelf: true } and/or { exileFromHand: 1 } paid before the op",
|
||||||
" requiresForce: true = usable only while the Force is with you",
|
" requiresForce: true = usable only while the Force is with you",
|
||||||
|
" requiresCapital: true = usable only while you have a capital ship in play",
|
||||||
" nForce / amountForce = upgraded number while the Force is with you",
|
" nForce / amountForce = upgraded number while the Force is with you",
|
||||||
"",
|
"",
|
||||||
"SUPPORTED OPS (the engine understands exactly these):",
|
"SUPPORTED OPS (the engine understands exactly these):",
|
||||||
|
|
@ -111,9 +112,9 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"cards": [
|
"cards": [
|
||||||
{ "id": "tiefighter", "name": "TIE Fighter", "faction": "empire", "type": "unit", "unique": false, "traits": ["Fighter"], "cost": 1, "attack": 1, "resources": 0, "force": 0, "count": 5, "target": 2, "reward": { "resources": 1 },
|
{ "id": "tiefighter", "name": "TIE Fighter", "faction": "empire", "type": "unit", "unique": false, "traits": ["Fighter"], "cost": 2, "attack": 2, "resources": 0, "force": 0, "count": 5, "target": 1, "reward": { "resources": 1 },
|
||||||
"ability": { "timing": "constant", "op": "selfBoostWithTrait", "trait": "Fighter", "amount": 1 },
|
"ability": { "timing": "action", "op": "draw", "n": 1, "requiresCapital": true },
|
||||||
"text": "TIE Fighter gains 1 attack while you have another Fighter in play." },
|
"text": "If you have a capital ship in play, draw 1 card." },
|
||||||
{ "id": "scouttrooper", "name": "Scout Trooper", "faction": "empire", "type": "unit", "unique": false, "traits": ["Trooper"], "cost": 2, "attack": 0, "resources": 2, "force": 0, "count": 3, "target": 3, "reward": { "resources": 1 },
|
{ "id": "scouttrooper", "name": "Scout Trooper", "faction": "empire", "type": "unit", "unique": false, "traits": ["Trooper"], "cost": 2, "attack": 0, "resources": 2, "force": 0, "count": 3, "target": 3, "reward": { "resources": 1 },
|
||||||
"ability": { "timing": "action", "op": "revealTop" },
|
"ability": { "timing": "action", "op": "revealTop" },
|
||||||
"text": "Reveal the top card of the galaxy deck. If it is an Empire card, gain 1 resource. If it is a Rebel card, you may discard it." },
|
"text": "Reveal the top card of the galaxy deck. If it is an Empire card, gain 1 resource. If it is a Rebel card, you may discard it." },
|
||||||
|
|
@ -142,8 +143,8 @@
|
||||||
"ability": { "timing": "action", "op": "draw", "n": 1, "nBase": { "base": "deathstar", "n": 2 } },
|
"ability": { "timing": "action", "op": "draw", "n": 1, "nBase": { "base": "deathstar", "n": 2 } },
|
||||||
"text": "Draw 1 card (2 cards instead if your base is the Death Star)." },
|
"text": "Draw 1 card (2 cards instead if your base is the Death Star)." },
|
||||||
{ "id": "darthvader", "name": "Darth Vader", "faction": "empire", "type": "unit", "unique": true, "traits": ["Sith"], "cost": 8, "attack": 6, "resources": 0, "force": 2, "count": 1, "target": 9, "reward": { "resources": 3, "force": 3 },
|
{ "id": "darthvader", "name": "Darth Vader", "faction": "empire", "type": "unit", "unique": true, "traits": ["Sith"], "cost": 8, "attack": 6, "resources": 0, "force": 2, "count": 1, "target": 9, "reward": { "resources": 3, "force": 3 },
|
||||||
"ability": { "timing": "action", "op": "damageBase", "n": 2, "requiresForce": true },
|
"ability": { "timing": "constant", "op": "selfBoostWhileForce", "amount": 4 },
|
||||||
"text": "If the Force is with you, deal 2 damage to the enemy base." },
|
"text": "Darth Vader gains 4 attack while the Force is with you." },
|
||||||
{ "id": "emperorpalpatine", "name": "Emperor Palpatine", "faction": "empire", "type": "unit", "unique": true, "traits": ["Sith"], "cost": 8, "attack": 3, "resources": 0, "force": 3, "count": 1, "target": 9, "reward": { "resources": 3, "force": 3 },
|
{ "id": "emperorpalpatine", "name": "Emperor Palpatine", "faction": "empire", "type": "unit", "unique": true, "traits": ["Sith"], "cost": 8, "attack": 3, "resources": 0, "force": 3, "count": 1, "target": 9, "reward": { "resources": 3, "force": 3 },
|
||||||
"ability": { "timing": "action", "op": "choose", "options": [ { "op": "draw", "n": 2 }, { "op": "oppDiscard", "n": 1, "randomForce": true } ] },
|
"ability": { "timing": "action", "op": "choose", "options": [ { "op": "draw", "n": 2 }, { "op": "oppDiscard", "n": 1, "randomForce": true } ] },
|
||||||
"text": "Choose one: draw 2 cards, or your opponent discards a card from their hand (at random while the Force is with you)." },
|
"text": "Choose one: draw 2 cards, or your opponent discards a card from their hand (at random while the Force is with you)." },
|
||||||
|
|
@ -175,9 +176,9 @@
|
||||||
{ "id": "rebelcommando", "name": "Rebel Commando", "faction": "rebel", "type": "unit", "unique": false, "traits": ["Trooper"], "cost": 3, "attack": 3, "resources": 0, "force": 0, "count": 3, "target": 3, "reward": { "resources": 2 },
|
{ "id": "rebelcommando", "name": "Rebel Commando", "faction": "rebel", "type": "unit", "unique": false, "traits": ["Trooper"], "cost": 3, "attack": 3, "resources": 0, "force": 0, "count": 3, "target": 3, "reward": { "resources": 2 },
|
||||||
"ability": { "timing": "action", "op": "oppDiscard", "n": 1, "randomForce": true },
|
"ability": { "timing": "action", "op": "oppDiscard", "n": 1, "randomForce": true },
|
||||||
"text": "Your opponent discards a card from their hand (at random while the Force is with you)." },
|
"text": "Your opponent discards a card from their hand (at random while the Force is with you)." },
|
||||||
{ "id": "cassianandor", "name": "Cassian Andor", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Trooper", "Spy"], "cost": 4, "attack": 3, "resources": 1, "force": 0, "count": 1, "target": 5, "reward": { "resources": 2, "force": 1 },
|
{ "id": "cassianandor", "name": "Cassian Andor", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Trooper", "Spy"], "cost": 5, "attack": 5, "resources": 0, "force": 0, "count": 1, "target": 5, "reward": { "resources": 3, "force": 2 },
|
||||||
"ability": { "timing": "action", "op": "lookTop", "mayDiscard": true },
|
"ability": { "timing": "onBounty", "op": "oppDiscard", "n": 1 },
|
||||||
"text": "Look at the top card of the galaxy deck. You may discard it." },
|
"text": "When Cassian Andor defeats a target in the galaxy row, your opponent discards 1 card from their hand." },
|
||||||
{ "id": "k2so", "name": "K-2SO", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Droid"], "cost": 4, "attack": 3, "resources": 0, "force": 0, "count": 1, "target": 5, "reward": { "resources": 2, "force": 1 },
|
{ "id": "k2so", "name": "K-2SO", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Droid"], "cost": 4, "attack": 3, "resources": 0, "force": 0, "count": 1, "target": 5, "reward": { "resources": 2, "force": 1 },
|
||||||
"ability": { "timing": "action", "op": "oppDiscard", "n": 1, "randomForce": true },
|
"ability": { "timing": "action", "op": "oppDiscard", "n": 1, "randomForce": true },
|
||||||
"text": "Your opponent discards a card from their hand (at random while the Force is with you)." },
|
"text": "Your opponent discards a card from their hand (at random while the Force is with you)." },
|
||||||
|
|
@ -197,8 +198,8 @@
|
||||||
"ability": { "timing": "action", "op": "freePurchase", "faction": "rebel", "forceTopdeck": true },
|
"ability": { "timing": "action", "op": "freePurchase", "faction": "rebel", "forceTopdeck": true },
|
||||||
"text": "Purchase a Rebel card from the galaxy row for free. If the Force is with you, place that card on top of your deck." },
|
"text": "Purchase a Rebel card from the galaxy row for free. If the Force is with you, place that card on top of your deck." },
|
||||||
{ "id": "lukeskywalker", "name": "Luke Skywalker", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Jedi"], "cost": 8, "attack": 6, "resources": 0, "force": 2, "count": 1, "target": 8, "reward": { "resources": 3, "force": 3 },
|
{ "id": "lukeskywalker", "name": "Luke Skywalker", "faction": "rebel", "type": "unit", "unique": true, "traits": ["Jedi"], "cost": 8, "attack": 6, "resources": 0, "force": 2, "count": 1, "target": 8, "reward": { "resources": 3, "force": 3 },
|
||||||
"ability": { "timing": "action", "op": "gainAttack", "n": 2, "requiresForce": true },
|
"ability": { "timing": "onPlay", "op": "destroyCapital", "requiresForce": true },
|
||||||
"text": "If the Force is with you, Luke Skywalker gains 2 attack." },
|
"text": "If the Force is with you, destroy an enemy capital ship." },
|
||||||
{ "id": "blockaderunner", "name": "Blockade Runner", "faction": "rebel", "type": "capital", "unique": false, "traits": ["Capital Ship"], "cost": 4, "attack": 1, "resources": 1, "force": 0, "hp": 4, "count": 2, "target": null, "reward": null,
|
{ "id": "blockaderunner", "name": "Blockade Runner", "faction": "rebel", "type": "capital", "unique": false, "traits": ["Capital Ship"], "cost": 4, "attack": 1, "resources": 1, "force": 0, "hp": 4, "count": 2, "target": null, "reward": null,
|
||||||
"ability": null,
|
"ability": null,
|
||||||
"text": "Though designed as a diplomatic courier, the Corellian CR90 corvette is often used to run planetary blockades." },
|
"text": "Though designed as a diplomatic courier, the Corellian CR90 corvette is often used to run planetary blockades." },
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import * as Phaser from 'phaser';
|
import * as Phaser from 'phaser';
|
||||||
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
import { GAME_WIDTH, GAME_HEIGHT } from '../../config.js';
|
||||||
import { Button } from '../../ui/Button.js';
|
import { Button } from '../../ui/Button.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, playForceMove, SFX } from '../../ui/Sounds.js';
|
||||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
import { api } from '../../services/api.js';
|
import { api } from '../../services/api.js';
|
||||||
import { createPlayerPortrait, createOpponentPortrait } from '../../ui/Portrait.js';
|
import { createPlayerPortrait, createOpponentPortrait } from '../../ui/Portrait.js';
|
||||||
|
|
@ -2106,7 +2106,7 @@ export default class SWDBGGame extends Phaser.Scene {
|
||||||
this.showBanner(`${this.seatName(e.seat)} regroups at ${baseDef(this.gs.players[e.seat].faction, e.id).name}`.toUpperCase());
|
this.showBanner(`${this.seatName(e.seat)} regroups at ${baseDef(this.gs.players[e.seat].faction, e.id).name}`.toUpperCase());
|
||||||
break;
|
break;
|
||||||
case 'forceMove': {
|
case 'forceMove': {
|
||||||
this.sfx(SFX.SCIFI_PLINK ?? SFX.CARD_SHOW);
|
playForceMove(this, e.to > e.from);
|
||||||
if (this._forceMarkerPos) this.shakeAt(this._forceMarkerPos.x, this._forceMarkerPos.y);
|
if (this._forceMarkerPos) this.shakeAt(this._forceMarkerPos.x, this._forceMarkerPos.y);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,12 @@ export function forcePegged(state, seat) {
|
||||||
return state.force * forceSign(p.faction) >= FORCE_MAX;
|
return state.force * forceSign(p.faction) >= FORCE_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function abilityGatePasses(state, seat, ab) {
|
||||||
|
if (ab.requiresForce && !forceWith(state, seat)) return false;
|
||||||
|
if (ab.requiresCapital && P(state, seat).capitals.length === 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function moveForce(state, seat, n) {
|
function moveForce(state, seat, n) {
|
||||||
if (!n) return;
|
if (!n) return;
|
||||||
const dir = forceSign(P(state, seat).faction);
|
const dir = forceSign(P(state, seat).faction);
|
||||||
|
|
@ -255,7 +261,7 @@ function abilityCandidates(state, seat, ab, ctx = {}) {
|
||||||
function abilityUsable(state, seat, ab, { costPoolSize = null, ctx = {} } = {}) {
|
function abilityUsable(state, seat, ab, { costPoolSize = null, ctx = {} } = {}) {
|
||||||
const p = P(state, seat);
|
const p = P(state, seat);
|
||||||
const o = P(state, opp(seat));
|
const o = P(state, opp(seat));
|
||||||
if (ab.requiresForce && !forceWith(state, seat)) return false;
|
if (!abilityGatePasses(state, seat, ab)) return false;
|
||||||
if (ab.cost?.exileFromHand) {
|
if (ab.cost?.exileFromHand) {
|
||||||
const spare = costPoolSize != null ? costPoolSize : p.hand.length;
|
const spare = costPoolSize != null ? costPoolSize : p.hand.length;
|
||||||
if (spare < ab.cost.exileFromHand) return false;
|
if (spare < ab.cost.exileFromHand) return false;
|
||||||
|
|
@ -519,7 +525,7 @@ function fireOnBounty(state, seat) {
|
||||||
const p = P(state, seat);
|
const p = P(state, seat);
|
||||||
for (const e of [...p.inPlay, ...p.capitals]) {
|
for (const e of [...p.inPlay, ...p.capitals]) {
|
||||||
const ab = cardDef(e.card).ability;
|
const ab = cardDef(e.card).ability;
|
||||||
if (ab && ab.timing === 'onBounty') queueOp(state, seat, ab, { entry: e });
|
if (ab && ab.timing === 'onBounty' && abilityGatePasses(state, seat, ab)) queueOp(state, seat, ab, { entry: e });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -565,7 +571,7 @@ function playCard(state, seat, card) {
|
||||||
emit(state, { type: 'attackBoost', seat, uid: card.uid, n: b.ability.amount, from: 'base' });
|
emit(state, { type: 'attackBoost', seat, uid: card.uid, n: b.ability.amount, from: 'base' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (def.ability?.timing === 'onPlay') queueOp(state, seat, def.ability, { entry });
|
if (def.ability?.timing === 'onPlay' && abilityGatePasses(state, seat, def.ability)) queueOp(state, seat, def.ability, { entry });
|
||||||
}
|
}
|
||||||
|
|
||||||
function useAbility(state, seat, action) {
|
function useAbility(state, seat, action) {
|
||||||
|
|
@ -728,7 +734,7 @@ export function actChooseBase(state, seat, baseId) {
|
||||||
state.effectQueue.shift();
|
state.effectQueue.shift();
|
||||||
emit(state, { type: 'baseChosen', seat, id: baseId });
|
emit(state, { type: 'baseChosen', seat, id: baseId });
|
||||||
const b = baseDef(p.faction, baseId);
|
const b = baseDef(p.faction, baseId);
|
||||||
if (b.ability?.timing === 'onReveal') queueOp(state, seat, b.ability, {});
|
if (b.ability?.timing === 'onReveal' && abilityGatePasses(state, seat, b.ability)) queueOp(state, seat, b.ability, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actChooseTarget(state, seat, ref) {
|
export function actChooseTarget(state, seat, ref) {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,12 @@ export default class PreloadScene extends Phaser.Scene {
|
||||||
this.load.audio('sfx-gem-drop', 'assets/fx/gem-drop.mp3');
|
this.load.audio('sfx-gem-drop', 'assets/fx/gem-drop.mp3');
|
||||||
this.load.audio('sfx-gem-big-drop','assets/fx/gem-big-drop.mp3');
|
this.load.audio('sfx-gem-big-drop','assets/fx/gem-big-drop.mp3');
|
||||||
this.load.audio('sfx-firework', 'assets/fx/firework.mp3');
|
this.load.audio('sfx-firework', 'assets/fx/firework.mp3');
|
||||||
|
this.load.audio('sfx-sw-light-1', 'assets/fx/sw-light-1.mp3');
|
||||||
|
this.load.audio('sfx-sw-light-2', 'assets/fx/sw-light-2.mp3');
|
||||||
|
this.load.audio('sfx-sw-light-3', 'assets/fx/sw-light-3.mp3');
|
||||||
|
this.load.audio('sfx-sw-dark-1', 'assets/fx/sw-dark-1.mp3');
|
||||||
|
this.load.audio('sfx-sw-dark-2', 'assets/fx/sw-dark-2.mp3');
|
||||||
|
this.load.audio('sfx-sw-dark-3', 'assets/fx/sw-dark-3.mp3');
|
||||||
|
|
||||||
this.load.spritesheet('catan-special-cards', 'assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('catan-special-cards', 'assets/images/catan-special-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,31 @@ export const SFX = {
|
||||||
GEM_DROP: 'sfx-gem-drop',
|
GEM_DROP: 'sfx-gem-drop',
|
||||||
GEM_BIG_DROP: 'sfx-gem-big-drop',
|
GEM_BIG_DROP: 'sfx-gem-big-drop',
|
||||||
FIREWORK: 'sfx-firework',
|
FIREWORK: 'sfx-firework',
|
||||||
|
SW_LIGHT_1: 'sfx-sw-light-1',
|
||||||
|
SW_LIGHT_2: 'sfx-sw-light-2',
|
||||||
|
SW_LIGHT_3: 'sfx-sw-light-3',
|
||||||
|
SW_DARK_1: 'sfx-sw-dark-1',
|
||||||
|
SW_DARK_2: 'sfx-sw-dark-2',
|
||||||
|
SW_DARK_3: 'sfx-sw-dark-3',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function playSound(scene, key) {
|
export function playSound(scene, key) {
|
||||||
try { scene.sound.play(key); } catch (_) {}
|
try { scene.sound.play(key); } catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Picks and plays one of several keys at random (e.g. variations of the same cue).
|
||||||
|
export function playRandomSound(scene, keys) {
|
||||||
|
playSound(scene, keys[Math.floor(Math.random() * keys.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SW_LIGHT_KEYS = [SFX.SW_LIGHT_1, SFX.SW_LIGHT_2, SFX.SW_LIGHT_3];
|
||||||
|
const SW_DARK_KEYS = [SFX.SW_DARK_1, SFX.SW_DARK_2, SFX.SW_DARK_3];
|
||||||
|
|
||||||
|
// SWDBG: Force track shifting toward the Rebel (light) or Empire (dark) side.
|
||||||
|
export function playForceMove(scene, towardLight) {
|
||||||
|
playRandomSound(scene, towardLight ? SW_LIGHT_KEYS : SW_DARK_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
// chip-bet is limited to one simultaneous instance.
|
// chip-bet is limited to one simultaneous instance.
|
||||||
let _chipBetSound = null;
|
let _chipBetSound = null;
|
||||||
export function playChipBet(scene) {
|
export function playChipBet(scene) {
|
||||||
|
|
|
||||||
|
|
@ -325,19 +325,49 @@ function give(st, seat, id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Constant boosts: Imperial Carrier aura + TIE swarm + Rodian row bonus.
|
// Constant boosts: Imperial Carrier aura (Fighter trait) + Rodian row bonus.
|
||||||
const st = freshGame(505);
|
const st = freshGame(505);
|
||||||
const emp = seatOf(st, 'empire');
|
const emp = seatOf(st, 'empire');
|
||||||
give(st, emp, 'imperialcarrier');
|
give(st, emp, 'imperialcarrier');
|
||||||
const tie1 = give(st, emp, 'tiefighter');
|
const tie1 = give(st, emp, 'tiefighter');
|
||||||
const tie2 = give(st, emp, 'tiefighter');
|
// TIE Fighter: 2 base + 1 carrier aura = 3
|
||||||
// TIE: 1 base + 1 swarm (another Fighter) + 1 carrier aura = 3
|
ok(entryAttack(st, emp, tie1) === 3, `TIE Fighter attack with carrier aura is 3 (got ${entryAttack(st, emp, tie1)})`);
|
||||||
ok(entryAttack(st, emp, tie1) === 3, `TIE Fighter attack with swarm+carrier is 3 (got ${entryAttack(st, emp, tie1)})`);
|
|
||||||
const rodian = give(st, emp, 'rodiangunslinger');
|
const rodian = give(st, emp, 'rodiangunslinger');
|
||||||
ok(entryAttack(st, emp, rodian) === 2, 'Rodian is 2 attack vs bases');
|
ok(entryAttack(st, emp, rodian) === 2, 'Rodian is 2 attack vs bases');
|
||||||
ok(entryAttack(st, emp, rodian, { vsRow: true }) === 4, 'Rodian is 4 attack vs the galaxy row');
|
ok(entryAttack(st, emp, rodian, { vsRow: true }) === 4, 'Rodian is 4 attack vs the galaxy row');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// TIE Fighter: action ability gated on having a capital ship in play.
|
||||||
|
const st = freshGame(508);
|
||||||
|
const emp = seatOf(st, 'empire');
|
||||||
|
const tie = give(st, emp, 'tiefighter');
|
||||||
|
let threw = false;
|
||||||
|
try { actTurn(st, emp, { type: 'ability', zone: 'play', uid: tie.card.uid }); } catch { threw = true; }
|
||||||
|
ok(threw, 'TIE Fighter draw ability unavailable without a capital ship in play');
|
||||||
|
give(st, emp, 'gozanticruiser');
|
||||||
|
const handBefore = st.players[emp].hand.length;
|
||||||
|
actTurn(st, emp, { type: 'ability', zone: 'play', uid: tie.card.uid });
|
||||||
|
ok(st.players[emp].hand.length === handBefore + 1, 'TIE Fighter draws a card once a capital ship is in play');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Cassian Andor: onBounty trigger forces an opponent discard.
|
||||||
|
const st = freshGame(509);
|
||||||
|
const reb = seatOf(st, 'rebel');
|
||||||
|
const emp = 1 - reb;
|
||||||
|
st.turnSeat = reb;
|
||||||
|
const cassian = give(st, reb, 'cassianandor'); // 5 attack
|
||||||
|
const target = { uid: 888002, id: 'tiefighter' }; // target 1
|
||||||
|
st.galaxy.row[0] = target;
|
||||||
|
st.players[emp].hand = [{ uid: 'h1', id: 'stormtrooper' }];
|
||||||
|
actTurn(st, reb, { type: 'attackRow', targetUid: target.uid, uids: [cassian.card.uid] });
|
||||||
|
const d = pendingDecision(st);
|
||||||
|
ok(d.kind === 'oppDiscard' && d.seat === emp, "Cassian Andor's onBounty queues a forced opponent discard");
|
||||||
|
actOppDiscard(st, emp, 'h1');
|
||||||
|
ok(st.players[emp].hand.length === 0, 'the forced discard resolves');
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
// Force mechanics: pegged bonus resource, spy choice restrictions.
|
// Force mechanics: pegged bonus resource, spy choice restrictions.
|
||||||
const st = freshGame(606);
|
const st = freshGame(606);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue