From 981ae6ec9e37551828599f23ac8bb4d2d9c07721 Mon Sep 17 00:00:00 2001 From: Anthony Bronico Date: Wed, 5 Mar 2025 16:32:45 -0500 Subject: [PATCH 1/4] Han Solo, Never Tell Me the Odds --- .../leaders/HanSoloNeverTellMeTheOdds.ts | 63 ++++++ .../leaders/HanSoloNeverTellMeTheOdds.spec.ts | 201 ++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts create mode 100644 test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts diff --git a/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts b/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts new file mode 100644 index 000000000..5ba1412f0 --- /dev/null +++ b/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts @@ -0,0 +1,63 @@ +import AbilityHelper from '../../../AbilityHelper'; +import type { Attack } from '../../../core/attack/Attack'; +import { LeaderUnitCard } from '../../../core/card/LeaderUnitCard'; +import { AbilityType, DeployType, WildcardZoneName } from '../../../core/Constants'; + +export default class HanSoloNeverTellMeTheOdds extends LeaderUnitCard { + protected override getImplementationId() { + return { + id: '0616724418', + internalName: 'han-solo#never-tell-me-the-odds', + }; + } + + protected override setupLeaderSideAbilities() { + this.addPilotDeploy(); + + this.addActionAbility({ + title: 'Reveal the top card of your deck', + immediateEffect: AbilityHelper.immediateEffects.reveal((context) => ({ + target: context.player.getTopCardOfDeck() + })), + then: (thenContext) => ({ + title: 'Attack with a unit', + initiateAttack: { + attackerLastingEffects: { + effect: AbilityHelper.ongoingEffects.modifyStats({ power: 1, hp: 0 }), + condition: (attack: Attack) => { + // This means that the deck was empty or no card could be revealed + if (thenContext.events.length === 0) { + return false; + } + const attackerCost = attack.attacker.cost; + const revealedCardCost = thenContext.events[0].card[0].cost; + return this.isOdd(attackerCost) && this.isOdd(revealedCardCost) && attackerCost !== revealedCardCost; + } + } + } + }) + }); + } + + protected override setupLeaderUnitSideAbilities() { + this.addPilotingAbility({ + title: 'For each friendly unit or upgrade that has an odd cost, ready a resource.', + type: AbilityType.Triggered, + when: { + onLeaderDeployed: (event, context) => event.card === context.source && event.type === DeployType.LeaderUpgrade + }, + zoneFilter: WildcardZoneName.AnyArena, + immediateEffect: AbilityHelper.immediateEffects.readyResources((context) => { + const friendlyUnits = context.player.getArenaUnits().filter((unit) => unit.isUnit() && this.isOdd(unit.cost)).length; + const friendlyUpgrades = context.player.getArenaUpgrades().filter((upgrade) => this.isOdd(upgrade.cost)).length; + return { + amount: friendlyUnits + friendlyUpgrades + }; + }) + }); + } + + private isOdd(num: number) { + return num % 2 === 1; + } +} \ No newline at end of file diff --git a/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts b/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts new file mode 100644 index 000000000..5812309f1 --- /dev/null +++ b/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts @@ -0,0 +1,201 @@ + +describe('Han Solo, Never Tell Me the Odds', function() { + integration(function(contextRef) { + describe('Han Solo, Never Tell Me the Odds\'s undeployed ability', function() { + it('should reveal a 1-cost card from the top of his deck and attack with a 3-cost unit with +1/+0 for the attack', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + deck: ['daring-raid'], + groundArena: ['echo-base-defender'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.hanSolo); + context.player1.clickPrompt('Reveal the top card of your deck'); + expect(context.getChatLogs(1)[0]).toContain(context.daringRaid.title); + expect(context.player1).toBeAbleToSelectExactly([context.echoBaseDefender]); + context.player1.clickCard(context.echoBaseDefender); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(5); + }); + + it('should not give +1/+0 when the revealed card is the same odd cost as the attacker', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + deck: ['hello-there'], + groundArena: ['echo-base-defender'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.hanSolo); + context.player1.clickPrompt('Reveal the top card of your deck'); + expect(context.getChatLogs(1)[0]).toContain(context.helloThere.title); + expect(context.player1).toBeAbleToSelectExactly([context.echoBaseDefender]); + context.player1.clickCard(context.echoBaseDefender); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(4); + }); + + it('should not give +1/+0 when the revealed card is a different even cost than the attacker', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + deck: ['surprise-strike'], + groundArena: ['modded-cohort'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.hanSolo); + context.player1.clickPrompt('Reveal the top card of your deck'); + expect(context.getChatLogs(1)[0]).toContain(context.surpriseStrike.title); + expect(context.player1).toBeAbleToSelectExactly([context.moddedCohort]); + context.player1.clickCard(context.moddedCohort); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(4); + }); + + it('should only reveal a card if there are no friendly units', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + deck: ['hello-there'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.hanSolo); + context.player1.clickPrompt('Reveal the top card of your deck'); + expect(context.getChatLogs(1)[0]).toContain(context.helloThere.title); + expect(context.player2).toBeActivePlayer(); + }); + + it('should only attack if there are no cards in the deck', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + groundArena: ['echo-base-defender'], + deck: [] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.hanSolo); + context.player1.clickPrompt('Reveal the top card of your deck'); + expect(context.getChatLogs(1)[0]).toContain('player1 uses Han Solo'); + expect(context.getChatLogs(1)[0]).not.toContain('reveal'); + expect(context.player1).toBeAbleToSelectExactly([context.echoBaseDefender]); + context.player1.clickCard(context.echoBaseDefender); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(4); + expect(context.player2).toBeActivePlayer(); + }); + }); + + describe('Han Solo, Never Tell Me the Odds\'s When Deployed ability', function() { + it('will ready resources equal to the number of odds units and upgrades', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + hand: ['republic-attack-pod'], + spaceArena: [{ card: 'concord-dawn-interceptors', upgrades: ['snapshot-reflexes'] }], + resources: 6 + } + }); + + const { context } = contextRef; + + // Spend all 6 resources + context.player1.clickCard(context.republicAttackPod); + expect(context.player1.readyResourceCount).toBe(0); + context.player2.passAction(); + + // Attach Han Solo to a unit + context.player1.clickCard(context.hanSolo); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Reveal the top card of your deck', 'Deploy Han Solo', 'Deploy Han Solo as a Pilot']); + context.player1.clickPrompt('Deploy Han Solo as a Pilot'); + expect(context.player2).not.toBeActivePlayer(); + expect(context.player1).toBeAbleToSelectExactly([context.concordDawnInterceptors, context.republicAttackPod]); + context.player1.clickCard(context.concordDawnInterceptors); + + // Han should ready 4 resources + expect(context.player1.readyResourceCount).toBe(3); + }); + + it('does not count friendly even-costed units or upgrades', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + hand: ['republic-attack-pod'], + groundArena: ['wampa'], + spaceArena: ['alliance-xwing'], + resources: 6 + } + }); + + const { context } = contextRef; + + // Spend all 6 resources + context.player1.clickCard(context.republicAttackPod); + expect(context.player1.readyResourceCount).toBe(0); + context.player2.passAction(); + + // Attach Han Solo to a unit + context.player1.clickCard(context.hanSolo); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Reveal the top card of your deck', 'Deploy Han Solo', 'Deploy Han Solo as a Pilot']); + context.player1.clickPrompt('Deploy Han Solo as a Pilot'); + context.player1.clickCard(context.republicAttackPod); + + // Han should ready 1 reesource - for just himself + expect(context.player1.readyResourceCount).toBe(1); + }); + + it('does not count enemy odd-costed units or upgrades', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + hand: ['republic-attack-pod'], + resources: 6 + }, + player2: { + spaceArena: ['concord-dawn-interceptors'], + groundArena: [{ card: 'moisture-farmer', upgrades: ['snapshot-reflexes'] }] + } + }); + + const { context } = contextRef; + + // Spend all 6 resources + context.player1.clickCard(context.republicAttackPod); + expect(context.player1.readyResourceCount).toBe(0); + context.player2.passAction(); + + // Attach Han Solo to a unit + context.player1.clickCard(context.hanSolo); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Reveal the top card of your deck', 'Deploy Han Solo', 'Deploy Han Solo as a Pilot']); + context.player1.clickPrompt('Deploy Han Solo as a Pilot'); + context.player1.clickCard(context.republicAttackPod); + + // Han should ready 1 reesource - for just himself + expect(context.player1.readyResourceCount).toBe(1); + }); + }); + }); +}); \ No newline at end of file From 5f52c163701c2ef74f80e90576d3d96cb5fa5b19 Mon Sep 17 00:00:00 2001 From: Anthony Bronico Date: Wed, 5 Mar 2025 16:45:24 -0500 Subject: [PATCH 2/4] Finishing up Han Solo --- .../leaders/HanSoloNeverTellMeTheOdds.ts | 2 +- .../leaders/HanSoloNeverTellMeTheOdds.spec.ts | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts b/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts index 5ba1412f0..173c9c1d5 100644 --- a/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts +++ b/server/game/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.ts @@ -57,7 +57,7 @@ export default class HanSoloNeverTellMeTheOdds extends LeaderUnitCard { }); } - private isOdd(num: number) { + private isOdd(num: number): boolean { return num % 2 === 1; } } \ No newline at end of file diff --git a/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts b/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts index 5812309f1..f35f1521d 100644 --- a/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts +++ b/test/server/cards/04_JTL/leaders/HanSoloNeverTellMeTheOdds.spec.ts @@ -107,6 +107,62 @@ describe('Han Solo, Never Tell Me the Odds', function() { }); describe('Han Solo, Never Tell Me the Odds\'s When Deployed ability', function() { + it('does nothing if deployed as a unit', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + hand: ['republic-attack-pod'], + resources: 6 + } + }); + + const { context } = contextRef; + + // Spend all 6 resources + context.player1.clickCard(context.republicAttackPod); + expect(context.player1.readyResourceCount).toBe(0); + context.player2.passAction(); + + // Attach Han Solo to a unit + context.player1.clickCard(context.hanSolo); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Reveal the top card of your deck', 'Deploy Han Solo', 'Deploy Han Solo as a Pilot']); + context.player1.clickPrompt('Deploy Han Solo'); + expect(context.hanSolo.getPower()).toBe(3); + expect(context.hanSolo.getHp()).toBe(7); + expect(context.player1.readyResourceCount).toBe(0); + expect(context.player2).toBeActivePlayer(); + }); + + it('can attach as a pilot leader and give proper resource boost', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'han-solo#never-tell-me-the-odds', + hand: ['republic-attack-pod'], + spaceArena: ['concord-dawn-interceptors'], + resources: 6 + } + }); + + const { context } = contextRef; + + // Spend all 6 resources + context.player1.clickCard(context.republicAttackPod); + expect(context.player1.readyResourceCount).toBe(0); + context.player2.passAction(); + + // Attach Han Solo to a unit + context.player1.clickCard(context.hanSolo); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Reveal the top card of your deck', 'Deploy Han Solo', 'Deploy Han Solo as a Pilot']); + context.player1.clickPrompt('Deploy Han Solo as a Pilot'); + expect(context.player2).not.toBeActivePlayer(); + expect(context.player1).toBeAbleToSelectExactly([context.concordDawnInterceptors, context.republicAttackPod]); + context.player1.clickCard(context.concordDawnInterceptors); + expect(context.concordDawnInterceptors.getPower()).toBe(4); + expect(context.concordDawnInterceptors.getHp()).toBe(8); + }); + it('will ready resources equal to the number of odds units and upgrades', async function () { await contextRef.setupTestAsync({ phase: 'action', From 70972b1b624f6cf67b54e63bdb4e9dd02d425b2d Mon Sep 17 00:00:00 2001 From: Anthony Bronico Date: Thu, 6 Mar 2025 00:47:49 -0500 Subject: [PATCH 3/4] Add Wedge leader --- .../WedgeAntillesLeaderOfRedSquadron.ts | 49 ++++ .../WedgeAntillesLeaderOfRedSquadron.spec.ts | 269 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 server/game/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.ts create mode 100644 test/server/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.spec.ts diff --git a/server/game/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.ts b/server/game/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.ts new file mode 100644 index 000000000..4720dde99 --- /dev/null +++ b/server/game/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.ts @@ -0,0 +1,49 @@ +import AbilityHelper from '../../../AbilityHelper'; +import { LeaderUnitCard } from '../../../core/card/LeaderUnitCard'; +import { AbilityType, CardType, KeywordName, PlayType, RelativePlayer, Trait, ZoneName } from '../../../core/Constants'; +import { CostAdjustType } from '../../../core/cost/CostAdjuster'; + +export default class WedgeAntillesLeaderOfRedSquadron extends LeaderUnitCard { + protected override getImplementationId() { + return { + id: '0011262813', + internalName: 'wedge-antilles#leader-of-red-squadron', + }; + } + + protected override setupLeaderSideAbilities() { + this.addPilotDeploy(); + + this.addActionAbility({ + title: 'Play a card from your hand using Piloting. It costs 1 less.', + cost: AbilityHelper.costs.exhaustSelf(), + targetResolver: { + controller: RelativePlayer.Self, + zoneFilter: ZoneName.Hand, + cardCondition: (card) => card.hasSomeKeyword(KeywordName.Piloting), // This helps prevent a prompt error + immediateEffect: AbilityHelper.immediateEffects.playCard({ + playType: PlayType.Piloting, + adjustCost: { costAdjustType: CostAdjustType.Decrease, amount: 1 } + }) + } + }); + } + + protected override setupLeaderUnitSideAbilities() { + this.addPilotingGainAbilityTargetingAttached({ + type: AbilityType.Triggered, + title: 'The next Pilot you play this phase costs 1 less', + when: { + onAttackDeclared: (event, context) => event.attack.attacker === context.source + }, + immediateEffect: AbilityHelper.immediateEffects.forThisPhasePlayerEffect({ + effect: AbilityHelper.ongoingEffects.decreaseCost({ + cardTypeFilter: CardType.BasicUnit, // TODO: does this need to allow upgrades? + match: (card) => card.hasSomeTrait(Trait.Pilot), + limit: AbilityHelper.limit.perGame(1), + amount: 1 + }) + }) + }); + } +} \ No newline at end of file diff --git a/test/server/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.spec.ts b/test/server/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.spec.ts new file mode 100644 index 000000000..0d134e2d8 --- /dev/null +++ b/test/server/cards/04_JTL/leaders/WedgeAntillesLeaderOfRedSquadron.spec.ts @@ -0,0 +1,269 @@ + +describe('Wedge Antilles, Leader of Red Squadron', function () { + integration(function (contextRef) { + describe('Wedge Antilles\'s undeployed ability', function () { + it('should play a card with Piloting from hand for 1 less', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['hopeful-volunteer', 'wampa', 'determined-recruit'], + spaceArena: ['concord-dawn-interceptors'], + resources: 3 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + const resourcesBefore = context.player1.readyResourceCount; + context.player1.clickCard(context.wedgeAntilles); + expect(context.player1).toBeAbleToSelectExactly([context.hopefulVolunteer, context.determinedRecruit]); + + // Hopeful Volunteer should be automatically played as a pilot upgrade + context.player1.clickCard(context.hopefulVolunteer); + expect(context.player1).toBeAbleToSelectExactly([context.concordDawnInterceptors]); + context.player1.clickCard(context.concordDawnInterceptors); + + // Hopeful Volunteer should cost 1 with Wedge's cost reduction + expect(context.player1.readyResourceCount).toBe(resourcesBefore - 1); + expect(context.concordDawnInterceptors).toHaveExactUpgradeNames([context.hopefulVolunteer.internalName]); + expect(context.concordDawnInterceptors.getPower()).toBe(2); + expect(context.concordDawnInterceptors.getHp()).toBe(7); + expect(context.player2).toBeActivePlayer(); + }); + + it('should only exhaust if there is no unit to play', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['wampa'], + spaceArena: ['concord-dawn-interceptors'], + resources: 4 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + context.player1.clickCard(context.wedgeAntilles); + expect(context.player2).toBeActivePlayer(); + }); + + it('should only exhaust if the hand is empty', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + spaceArena: ['concord-dawn-interceptors'], + resources: 4 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + context.player1.clickCard(context.wedgeAntilles); + expect(context.player2).toBeActivePlayer(); + }); + }); + + describe('Wedge Antilles\'s deployed ability', function () { + it('should be able to deploy as a ground unit and have correct stats', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + resources: 5 + } + }); + + const { context } = contextRef; + + // Deploy Wedge Antilles as a ground unit + context.player1.clickCard(context.wedgeAntilles); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Play a card from your hand using Piloting. It costs 1 less.', 'Deploy Wedge Antilles']); + context.player1.clickPrompt('Deploy Wedge Antilles'); + expect(context.wedgeAntilles.getPower()).toBe(3); + expect(context.wedgeAntilles.getHp()).toBe(6); + }); + + it('should not reduce the cost of the next Pilot when attacking as a ground unit', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['hopeful-volunteer', 'wampa', 'determined-recruit'], + resources: 5 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + + // Deploy Wedge Antilles as a Pilot + context.player1.clickCard(context.wedgeAntilles); + context.player1.clickPrompt('Deploy Wedge Antilles'); + + context.player2.passAction(); + + // Attack with Concord Dawn Interceptors + context.player1.clickCard(context.wedgeAntilles); + context.player1.clickCard(context.p2Base); + + context.player2.passAction(); + + context.player1.clickCard(context.hopefulVolunteer); + expect(context.player1.exhaustedResourceCount).toBe(2); + }); + + it('should reduce the cost of the next Pilot unit you play this phase by 1', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['hopeful-volunteer', 'wampa', 'determined-recruit'], + spaceArena: ['concord-dawn-interceptors'], + resources: 5 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + + // Trigger Wedge's on Attack ability + context.player1.clickCard(context.wedgeAntilles); + expect(context.player1).toHaveExactPromptButtons(['Cancel', 'Play a card from your hand using Piloting. It costs 1 less.', 'Deploy Wedge Antilles', 'Deploy Wedge Antilles as a Pilot']); + context.player1.clickPrompt('Deploy Wedge Antilles as a Pilot'); + expect(context.player1).toBeAbleToSelectExactly([context.concordDawnInterceptors]); + context.player1.clickCard(context.concordDawnInterceptors); + context.player2.passAction(); + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + + context.player2.passAction(); + + context.player1.clickCard(context.hopefulVolunteer); + expect(context.player1.exhaustedResourceCount).toBe(1); + }); + + it('should reduce the cost of the next Pilot upgrade you play this phase by 1', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['hopeful-volunteer', 'wampa', 'determined-recruit'], + spaceArena: ['concord-dawn-interceptors', 'cartel-turncoat'], + resources: 5 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + + // Trigger Wedge's on Attack ability + context.player1.clickCard(context.wedgeAntilles); + context.player1.clickPrompt('Deploy Wedge Antilles as a Pilot'); + context.player1.clickCard(context.concordDawnInterceptors); + + context.player2.passAction(); + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + + context.player2.passAction(); + + context.player1.clickCard(context.hopefulVolunteer); + context.player1.clickPrompt('Play Hopeful Volunteer with Piloting'); + context.player1.clickCard(context.cartelTurncoat); + + expect(context.cartelTurncoat).toHaveExactUpgradeNames([context.hopefulVolunteer.internalName]); + expect(context.player1.exhaustedResourceCount).toBe(1); + }); + + it('should not reduce both the first pilot unit and first pilot upgrade', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['hopeful-volunteer', 'wampa', 'determined-recruit'], + spaceArena: ['concord-dawn-interceptors', 'cartel-turncoat'], + resources: 5 + }, + player2: { + hand: ['dagger-squadron-pilot'] + } + }); + + const { context } = contextRef; + + // Trigger Wedge's on Attack ability + context.player1.clickCard(context.wedgeAntilles); + context.player1.clickPrompt('Deploy Wedge Antilles as a Pilot'); + context.player1.clickCard(context.concordDawnInterceptors); + context.player2.passAction(); + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + + context.player2.passAction(); + + context.player1.clickCard(context.hopefulVolunteer); + context.player1.clickPrompt('Play Hopeful Volunteer with Piloting'); + context.player1.clickCard(context.cartelTurncoat); + + expect(context.cartelTurncoat).toHaveExactUpgradeNames([context.hopefulVolunteer.internalName]); + expect(context.player1.exhaustedResourceCount).toBe(1); + + context.player2.passAction(); + + context.player1.clickCard(context.determinedRecruit); + expect(context.player1.exhaustedResourceCount).toBe(3); + }); + + it('should not reduce the cost of the next non-Pilot card', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + leader: 'wedge-antilles#leader-of-red-squadron', + base: 'kestro-city', + hand: ['wampa'], + spaceArena: ['concord-dawn-interceptors'], + resources: 5 + } + }); + + const { context } = contextRef; + + // Trigger Wedge's on Attack ability + context.player1.clickCard(context.wedgeAntilles); + context.player1.clickPrompt('Deploy Wedge Antilles as a Pilot'); + context.player1.clickCard(context.concordDawnInterceptors); + context.player2.passAction(); + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + + context.player2.passAction(); + + context.player1.clickCard(context.wampa); + expect(context.player1.exhaustedResourceCount).toBe(4); + }); + }); + }); +}); \ No newline at end of file From e6dba79593b275ddcff3e0f7ffc8d4042bdbeeec Mon Sep 17 00:00:00 2001 From: Anthony Bronico Date: Thu, 6 Mar 2025 01:12:09 -0500 Subject: [PATCH 4/4] Add Cassian Andor unit --- .../units/CassianAndorThreadingTheEye.ts | 31 +++++++ .../units/CassianAndorThreadingTheEye.spec.ts | 86 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 server/game/cards/04_JTL/units/CassianAndorThreadingTheEye.ts create mode 100644 test/server/cards/04_JTL/units/CassianAndorThreadingTheEye.spec.ts diff --git a/server/game/cards/04_JTL/units/CassianAndorThreadingTheEye.ts b/server/game/cards/04_JTL/units/CassianAndorThreadingTheEye.ts new file mode 100644 index 000000000..72be346a8 --- /dev/null +++ b/server/game/cards/04_JTL/units/CassianAndorThreadingTheEye.ts @@ -0,0 +1,31 @@ +import AbilityHelper from '../../../AbilityHelper'; +import { NonLeaderUnitCard } from '../../../core/card/NonLeaderUnitCard'; +import { AbilityType } from '../../../core/Constants'; + +export default class CassianAndorThreadingTheEye extends NonLeaderUnitCard { + protected override getImplementationId() { + return { + id: '3475471540', + internalName: 'cassian-andor#threading-the-eye' + }; + } + + public override setupCardAbilities() { + this.addPilotingGainAbilityTargetingAttached({ + type: AbilityType.Triggered, + title: 'Discard a card from the defending player\'s deck', + when: { + onAttackDeclared: (event, context) => event.attack.attacker === context.source + }, + immediateEffect: AbilityHelper.immediateEffects.discardFromDeck((context) => ({ + amount: 1, + target: context.source.activeAttack.target.controller + })), + ifYouDo: { + title: 'Draw a card', + ifYouDoCondition: (ifYouDoContext) => ifYouDoContext.events[0].card.cost <= 3, + immediateEffect: AbilityHelper.immediateEffects.draw() + } + }); + } +} diff --git a/test/server/cards/04_JTL/units/CassianAndorThreadingTheEye.spec.ts b/test/server/cards/04_JTL/units/CassianAndorThreadingTheEye.spec.ts new file mode 100644 index 000000000..4933da22d --- /dev/null +++ b/test/server/cards/04_JTL/units/CassianAndorThreadingTheEye.spec.ts @@ -0,0 +1,86 @@ +describe('Cassian Andor, Threading the Eye', function() { + integration(function(contextRef) { + describe('Cassian Andor\'s on-attack ability', function () { + it('should not trigger if he is a unit', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + groundArena: ['cassian-andor#threading-the-eye'] + }, + player2: { + deck: ['wampa'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.cassianAndor); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(3); + expect(context.wampa).toBeInZone('deck'); + }); + + it('should discard a card from the opponent\'s deck and draw a card because it costed 3 or less', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + deck: ['wampa'], + spaceArena: [{ card: 'concord-dawn-interceptors', upgrades: ['cassian-andor#threading-the-eye'] }] + }, + player2: { + deck: ['confiscate'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(2); + expect(context.confiscate).toBeInZone('discard'); + expect(context.wampa).toBeInZone('hand'); + }); + + it('should discard a card from the opponent\'s deck but not draw a card because it costed more than 3', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + deck: ['confiscate'], + spaceArena: [{ card: 'concord-dawn-interceptors', upgrades: ['cassian-andor#threading-the-eye'] }] + }, + player2: { + deck: ['wampa'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(2); + expect(context.wampa).toBeInZone('discard'); + expect(context.confiscate).toBeInZone('deck'); + }); + + it('should not draw a card if no card is discarded from the opponent\'s deck', async function () { + await contextRef.setupTestAsync({ + phase: 'action', + player1: { + deck: ['confiscate'], + spaceArena: [{ card: 'concord-dawn-interceptors', upgrades: ['cassian-andor#threading-the-eye'] }] + }, + player2: { + deck: [] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.concordDawnInterceptors); + context.player1.clickCard(context.p2Base); + expect(context.p2Base.damage).toBe(2); + expect(context.confiscate).toBeInZone('deck'); + }); + }); + }); +}); \ No newline at end of file