From 430d0014394686706f506081c57b88aad92eacca Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Tue, 7 Jan 2025 17:27:17 -0500 Subject: [PATCH 01/11] Add Chirrut deployed effect and tests --- .../leaders/ChirrutImweOneWithTheForce.ts | 26 +++++ server/game/core/Constants.ts | 1 + .../game/core/card/propertyMixins/Damage.ts | 7 +- .../card/propertyMixins/UnitProperties.ts | 7 +- .../ongoingEffects/OngoingEffectLibrary.ts | 1 + .../ChirrutImweOneWithTheForce.spec.ts | 101 ++++++++++++++++++ 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts create mode 100644 test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts diff --git a/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts b/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts new file mode 100644 index 000000000..f296c9b8c --- /dev/null +++ b/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts @@ -0,0 +1,26 @@ +import AbilityHelper from '../../../AbilityHelper'; +import { LeaderUnitCard } from '../../../core/card/LeaderUnitCard'; +import { PhaseName } from '../../../core/Constants'; + +export default class ChirrutImweOneWithTheForce extends LeaderUnitCard { + protected override getImplementationId() { + return { + id: '4263394087', + internalName: 'chirrut-imwe#one-with-the-force', + }; + } + + protected override setupLeaderSideAbilities() { + // test + } + + protected override setupLeaderUnitSideAbilities() { + this.addConstantAbility({ + title: 'During the action phase, this unit isn\'t defeated by having no remaining HP', + ongoingEffect: AbilityHelper.ongoingEffects.cannotBeDefeatedByDamage(), + condition: (context) => context.game.currentPhase === PhaseName.Action + }); + } +} + +ChirrutImweOneWithTheForce.implemented = true; diff --git a/server/game/core/Constants.ts b/server/game/core/Constants.ts index 4e753344a..ab4ae6383 100644 --- a/server/game/core/Constants.ts +++ b/server/game/core/Constants.ts @@ -65,6 +65,7 @@ export enum EffectName { CanAttackGroundArenaFromSpaceArena = 'canAttackGroundArenaFromSpaceArena', CanAttackSpaceArenaFromGroundArena = 'canAttackSpaceArenaFromGroundArena', CanBeTriggeredByOpponent = 'canBeTriggeredByOpponent', + CannotBeDefeatedByDamage = 'cannotBeDefeatedByDamage', CanPlayFromDiscard = 'canPlayFromDiscard', ChangeType = 'changeType', CostAdjuster = 'costAdjuster', diff --git a/server/game/core/card/propertyMixins/Damage.ts b/server/game/core/card/propertyMixins/Damage.ts index fa5129fa0..2e997d68d 100644 --- a/server/game/core/card/propertyMixins/Damage.ts +++ b/server/game/core/card/propertyMixins/Damage.ts @@ -5,6 +5,7 @@ import type Player from '../../Player'; import type { CardWithDamageProperty } from '../CardTypes'; import { WithPrintedHp } from './PrintedHp'; import type { IDamageSource } from '../../../IDamageOrDefeatSource'; +import { EffectName } from '../../Constants'; /** * Mixin function that adds the `damage` property and corresponding methods to a base class. @@ -74,8 +75,12 @@ export function WithDamage(BaseClass: TBaseC if (amount === 0) { return 0; } + // if a card can't be defeated by damage (e.g. Chirrut) we consider all damage to have been + // applied to the card, even if it goes above max hp (important for overwhelm calculation) + const damageToAdd = this.hasOngoingEffect(EffectName.CannotBeDefeatedByDamage) + ? amount + : Math.min(amount, this.remainingHp); - const damageToAdd = Math.min(amount, this.remainingHp); this.damage += damageToAdd; return damageToAdd; diff --git a/server/game/core/card/propertyMixins/UnitProperties.ts b/server/game/core/card/propertyMixins/UnitProperties.ts index 1c594ef64..80235b250 100644 --- a/server/game/core/card/propertyMixins/UnitProperties.ts +++ b/server/game/core/card/propertyMixins/UnitProperties.ts @@ -551,7 +551,12 @@ export function WithUnitProperties(Bas this.checkDefeated(DefeatSourceType.FrameworkEffect); } - private checkDefeated(source: IDamageSource | DefeatSourceType.FrameworkEffect) { + protected checkDefeated(source: IDamageSource | DefeatSourceType.FrameworkEffect) { + // if this card can't be defeated by damage (e.g. Chirrut), skip the check + if (this.hasOngoingEffect(EffectName.CannotBeDefeatedByDamage)) { + return; + } + if (this.damage >= this.getHp() && !this._pendingDefeat) { // add defeat event to window this.game.addSubwindowEvents( diff --git a/server/game/ongoingEffects/OngoingEffectLibrary.ts b/server/game/ongoingEffects/OngoingEffectLibrary.ts index 313b89b59..aafbf4e49 100644 --- a/server/game/ongoingEffects/OngoingEffectLibrary.ts +++ b/server/game/ongoingEffects/OngoingEffectLibrary.ts @@ -161,6 +161,7 @@ export = { // mustBeDeclaredAsAttackerIfType: (type = 'both') => // OngoingEffectBuilder.card.static(EffectName.MustBeDeclaredAsAttackerIfType, type), // mustBeDeclaredAsDefender: (type = 'both') => OngoingEffectBuilder.card.static(EffectName.MustBeDeclaredAsDefender, type), + cannotBeDefeatedByDamage: () => OngoingEffectBuilder.card.static(EffectName.CannotBeDefeatedByDamage), // setBaseDash: (type) => OngoingEffectBuilder.card.static(EffectName.SetBaseDash, type), // setBaseMilitarySkill: (value) => OngoingEffectBuilder.card.static(EffectName.SetBaseMilitarySkill, value), // setBasePoliticalSkill: (value) => OngoingEffectBuilder.card.static(EffectName.SetBasePoliticalSkill, value), diff --git a/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts b/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts new file mode 100644 index 000000000..63fb46ad2 --- /dev/null +++ b/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts @@ -0,0 +1,101 @@ +describe('Chirrut Îmwe, One with the Force', function() { + integration(function(contextRef) { + describe('Chirrut\'s deployed ability', function() { + beforeEach(function () { + contextRef.setupTest({ + phase: 'action', + player1: { + hand: ['repair'], + leader: { card: 'chirrut-imwe#one-with-the-force', deployed: true } + }, + player2: { + groundArena: ['mercenary-company'], + hand: ['daring-raid'] + } + }); + }); + + it('prevents him from being defeated by damage during the action phase', function () { + const { context } = contextRef; + + context.player1.clickCard(context.chirrutImwe); + context.player1.clickCard(context.mercenaryCompany); + expect(context.chirrutImwe).toBeInZone('groundArena'); + expect(context.chirrutImwe.damage).toBe(5); + + // add some non-combat damage + context.player2.clickCard(context.daringRaid); + context.player2.clickCard(context.chirrutImwe); + expect(context.chirrutImwe).toBeInZone('groundArena'); + expect(context.chirrutImwe.damage).toBe(7); + + // heal back down below max HP before the phase ends + context.player1.clickCard(context.repair); + context.player1.clickCard(context.chirrutImwe); + expect(context.chirrutImwe).toBeInZone('groundArena'); + expect(context.chirrutImwe.damage).toBe(4); + + context.moveToNextActionPhase(); + expect(context.chirrutImwe).toBeInZone('groundArena'); + + context.player1.passAction(); + + // attack Mercenary Company into Chirrut, overwhelm should not happen + context.player2.clickCard(context.mercenaryCompany); + context.player2.clickCard(context.chirrutImwe); + expect(context.chirrutImwe).toBeInZone('groundArena'); + expect(context.chirrutImwe.damage).toBe(9); + expect(context.p1Base.damage).toBe(0); + + // Chirrut is defeated at the end of the phase + context.moveToRegroupPhase(); + expect(context.chirrutImwe).toBeInZone('base'); + }); + }); + + describe('Chirrut\'s deployed ability', function() { + beforeEach(function () { + contextRef.setupTest({ + phase: 'action', + player1: { + hand: ['repair'], + leader: { card: 'chirrut-imwe#one-with-the-force', deployed: true } + }, + player2: { + groundArena: ['escort-skiff'], + hand: ['make-an-opening', 'supreme-leader-snoke#shadow-ruler'] + } + }); + }); + + it('prevents him from being defeated by HP reduction effects during the action phase', function () { + const { context } = contextRef; + + // deal 4 damage to Chirrut + context.player1.clickCard(context.chirrutImwe); + context.player1.clickCard(context.escortSkiff); + + // apply -2/-2 for the phase + context.player2.clickCard(context.makeAnOpening); + context.player2.clickCard(context.chirrutImwe); + + // Chirrut should survive because the -2/-2 effect expires in the same window as his prevention effect + context.moveToNextActionPhase(); + expect(context.chirrutImwe).toBeInZone('groundArena'); + + // deal 4 damage to Chirrut + context.player1.clickCard(context.chirrutImwe); + context.player1.clickCard(context.escortSkiff); + + // apply permanent -2/-2 with Snoke + context.player2.clickCard(context.supremeLeaderSnokeShadowRuler); + + // Chirrut is defeated at the end of the phase + context.moveToRegroupPhase(); + expect(context.chirrutImwe).toBeInZone('base'); + + // TODO: once Luke is implemented, try his effect to send Chirrut max HP below 0 and confirm he still survives + }); + }); + }); +}); From 6bb684885149b97e3c0ac1a746d2c2170d2e2b8e Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Tue, 7 Jan 2025 17:38:12 -0500 Subject: [PATCH 02/11] Add Chirrut undeployed ability + tests --- .../leaders/ChirrutImweOneWithTheForce.ts | 13 +++- .../ChirrutImweOneWithTheForce.spec.ts | 60 ++++++++++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts b/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts index f296c9b8c..367388715 100644 --- a/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts +++ b/server/game/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.ts @@ -1,6 +1,6 @@ import AbilityHelper from '../../../AbilityHelper'; import { LeaderUnitCard } from '../../../core/card/LeaderUnitCard'; -import { PhaseName } from '../../../core/Constants'; +import { PhaseName, WildcardCardType } from '../../../core/Constants'; export default class ChirrutImweOneWithTheForce extends LeaderUnitCard { protected override getImplementationId() { @@ -11,7 +11,16 @@ export default class ChirrutImweOneWithTheForce extends LeaderUnitCard { } protected override setupLeaderSideAbilities() { - // test + this.addActionAbility({ + title: 'Give a unit +0/+2 for this phase', + cost: AbilityHelper.costs.exhaustSelf(), + targetResolver: { + cardTypeFilter: WildcardCardType.Unit, + immediateEffect: AbilityHelper.immediateEffects.forThisPhaseCardEffect({ + effect: AbilityHelper.ongoingEffects.modifyStats({ power: 0, hp: 2 }) + }) + } + }); } protected override setupLeaderUnitSideAbilities() { diff --git a/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts b/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts index 63fb46ad2..55aec717c 100644 --- a/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts +++ b/test/server/cards/01_SOR/leaders/ChirrutImweOneWithTheForce.spec.ts @@ -1,7 +1,57 @@ describe('Chirrut Îmwe, One with the Force', function() { integration(function(contextRef) { + it('Chirrut\'s undeployed ability', function() { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'chirrut-imwe#one-with-the-force', + groundArena: ['death-star-stormtrooper'], + resources: 4 + }, + player2: { + spaceArena: ['tieln-fighter'], + hand: ['daring-raid'] + } + }); + + const { context } = contextRef; + + // apply +2/+2 effect to Death Star Stormtrooper + context.player1.clickCard(context.chirrutImwe); + expect(context.player1).toBeAbleToSelectExactly([context.deathStarStormtrooper, context.tielnFighter]); + context.player1.clickCard(context.deathStarStormtrooper); + + expect(context.deathStarStormtrooper.getPower()).toBe(3); + expect(context.deathStarStormtrooper.getHp()).toBe(3); + expect(context.tielnFighter.getPower()).toBe(2); + expect(context.tielnFighter.getHp()).toBe(1); + expect(context.chirrutImwe.exhausted).toBeTrue(); + + // deal 2 damage to stormtrooper so it will be defeated when the effect expires + context.player2.clickCard(context.daringRaid); + context.player2.clickCard(context.deathStarStormtrooper); + + // give the +2 effect to the TIE/LN Fighter as well + context.chirrutImwe.exhausted = false; + context.player1.clickCard(context.chirrutImwe); + expect(context.player1).toBeAbleToSelectExactly([context.deathStarStormtrooper, context.tielnFighter]); + context.player1.clickCard(context.tielnFighter); + + expect(context.deathStarStormtrooper.getPower()).toBe(3); + expect(context.deathStarStormtrooper.getHp()).toBe(3); + expect(context.tielnFighter.getPower()).toBe(2); + expect(context.tielnFighter.getHp()).toBe(3); + expect(context.chirrutImwe.exhausted).toBeTrue(); + + // move to regroup phase, confirm effects have expired + context.moveToRegroupPhase(); + expect(context.deathStarStormtrooper).toBeInZone('discard'); + expect(context.tielnFighter.getPower()).toBe(2); + expect(context.tielnFighter.getHp()).toBe(1); + }); + describe('Chirrut\'s deployed ability', function() { - beforeEach(function () { + it('prevents him from being defeated by damage during the action phase', function () { contextRef.setupTest({ phase: 'action', player1: { @@ -13,9 +63,7 @@ describe('Chirrut Îmwe, One with the Force', function() { hand: ['daring-raid'] } }); - }); - it('prevents him from being defeated by damage during the action phase', function () { const { context } = contextRef; context.player1.clickCard(context.chirrutImwe); @@ -51,10 +99,8 @@ describe('Chirrut Îmwe, One with the Force', function() { context.moveToRegroupPhase(); expect(context.chirrutImwe).toBeInZone('base'); }); - }); - describe('Chirrut\'s deployed ability', function() { - beforeEach(function () { + it('prevents him from being defeated by HP reduction effects during the action phase', function () { contextRef.setupTest({ phase: 'action', player1: { @@ -66,9 +112,7 @@ describe('Chirrut Îmwe, One with the Force', function() { hand: ['make-an-opening', 'supreme-leader-snoke#shadow-ruler'] } }); - }); - it('prevents him from being defeated by HP reduction effects during the action phase', function () { const { context } = contextRef; // deal 4 damage to Chirrut From 8ed34d2f8312e1f782aa3049ab683ee9a41badfa Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Tue, 7 Jan 2025 18:16:43 -0500 Subject: [PATCH 03/11] Basic First Light test passes --- .../FirstLightHeadquartersOfTheCrimsonDawn.ts | 66 +++++++++++++++++++ server/game/core/ability/KeywordInstance.ts | 2 +- .../baseClasses/PlayableOrDeployableCard.ts | 5 +- ...tLightHeadquartersOfTheCrimsonDawn.spec.ts | 30 +++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts create mode 100644 test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts diff --git a/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts new file mode 100644 index 000000000..e61880cdd --- /dev/null +++ b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts @@ -0,0 +1,66 @@ +import AbilityHelper from '../../../AbilityHelper'; +import { PlayUnitAction } from '../../../actions/PlayUnitAction'; +import type { IPlayCardActionProperties } from '../../../core/ability/PlayCardAction'; +import type { IPlayCardActionOverrides } from '../../../core/card/baseClasses/PlayableOrDeployableCard'; +import { NonLeaderUnitCard } from '../../../core/card/NonLeaderUnitCard'; +import { Aspect, KeywordName, PlayType, RelativePlayer, WildcardCardType } from '../../../core/Constants'; + +export default class FirstLightHeadquartersOfTheCrimsonDawn extends NonLeaderUnitCard { + protected override getImplementationId() { + return { + id: '4783554451', + internalName: 'first-light#headquarters-of-the-crimson-dawn', + }; + } + + protected override buildPlayCardActions(playType: PlayType = PlayType.PlayFromHand, propertyOverrides: IPlayCardActionOverrides = null) { + const firstLightSmuggleAction = playType === PlayType.Smuggle + ? [new FirstLightSmuggleAction(this)] + : []; + + return super.buildPlayCardActions(playType, propertyOverrides).concat(firstLightSmuggleAction); + } + + public override setupCardAbilities() { + this.addConstantAbility({ + title: 'Each other friendly non-leader unit gains Grit', + targetController: RelativePlayer.Self, + matchTarget: (card, context) => card !== context.source, + ongoingEffect: AbilityHelper.ongoingEffects.gainKeyword(KeywordName.Grit) + }); + } +} + +class FirstLightSmuggleAction extends PlayUnitAction { + private static generateProperties(properties: IPlayCardActionOverrides = {}): IPlayCardActionProperties { + const damageCost = AbilityHelper.costs.dealDamage(4, { + controller: RelativePlayer.Self, + cardTypeFilter: WildcardCardType.Unit + }); + + return { + title: 'Play First Light with Smuggle by dealing 4 damage to a friendly unit', + ...properties, + playType: PlayType.Smuggle, + smuggleAspects: [Aspect.Vigilance, Aspect.Villainy], + smuggleResourceCost: 7, + additionalCosts: [damageCost] + }; + } + + public constructor(card: FirstLightHeadquartersOfTheCrimsonDawn, propertyOverrides: IPlayCardActionOverrides = {}) { + super(card, FirstLightSmuggleAction.generateProperties(propertyOverrides)); + } + + public override clone(overrideProperties: Partial>) { + return new FirstLightSmuggleAction( + this.card, + FirstLightSmuggleAction.generateProperties({ + ...this.createdWithProperties, + ...overrideProperties + }) + ); + } +} + +FirstLightHeadquartersOfTheCrimsonDawn.implemented = true; diff --git a/server/game/core/ability/KeywordInstance.ts b/server/game/core/ability/KeywordInstance.ts index 1f0b70452..2ffb129d2 100644 --- a/server/game/core/ability/KeywordInstance.ts +++ b/server/game/core/ability/KeywordInstance.ts @@ -68,7 +68,7 @@ export class KeywordWithCostValues extends KeywordInstance { name: KeywordName, public readonly cost: number, public readonly aspects: Aspect[], - public readonly additionalCosts: boolean // TODO: implement additional costs (First Light) + public readonly additionalSmuggleCosts: boolean ) { super(name); } diff --git a/server/game/core/card/baseClasses/PlayableOrDeployableCard.ts b/server/game/core/card/baseClasses/PlayableOrDeployableCard.ts index 56523d1f6..2ff0101af 100644 --- a/server/game/core/card/baseClasses/PlayableOrDeployableCard.ts +++ b/server/game/core/card/baseClasses/PlayableOrDeployableCard.ts @@ -131,7 +131,10 @@ export class PlayableOrDeployableCard extends Card { protected buildCheapestSmuggleAction(propertyOverrides: IPlayCardActionOverrides = null) { Contract.assertTrue(this.hasSomeKeyword(KeywordName.Smuggle)); - const smuggleKeywords = this.getKeywordsWithCostValues(KeywordName.Smuggle); + // find all Smuggle keywords, filtering out any with additional ability costs as those will be implemented manually (e.g. First Light) + const smuggleKeywords = this.getKeywordsWithCostValues(KeywordName.Smuggle) + .filter((keyword) => !keyword.additionalSmuggleCosts); + const smuggleActions = smuggleKeywords.map((smuggleKeyword) => { const smuggleActionProps: ISmuggleCardActionProperties = { ...propertyOverrides, diff --git a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts new file mode 100644 index 000000000..6a041dca6 --- /dev/null +++ b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts @@ -0,0 +1,30 @@ +describe('First Light, Headquarters of the Crimson Dawn', function() { + integration(function(contextRef) { + describe('First Light\'s Smuggle ability', function() { + beforeEach(function () { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'qira#i-alone-survived', + groundArena: ['wampa'], + resources: ['first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst'] + }, + player2: { + spaceArena: ['cartel-spacer'] + } + }); + }); + + it('should require dealing 4 damage to a friendly unit as a cost', function () { + const { context } = contextRef; + + context.player1.clickCard(context.firstLight); + expect(context.player1).toBeAbleToSelectExactly([context.wampa]); + context.player1.clickCard(context.wampa); + expect(context.firstLight).toBeInZone('spaceArena'); + expect(context.wampa.damage).toBe(4); + expect(context.wampa.getPower()).toBe(8); + }); + }); + }); +}); From a0ea107aeee5e9afc7595ba52cf3479a4e75c4dc Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Wed, 8 Jan 2025 14:08:55 -0500 Subject: [PATCH 04/11] Finish First Light tests --- .../FirstLightHeadquartersOfTheCrimsonDawn.ts | 3 +- server/game/core/ability/PlayCardAction.ts | 9 +- test/helpers/PlayerInteractionWrapper.js | 4 +- ...tLightHeadquartersOfTheCrimsonDawn.spec.ts | 112 +++++++++++++++++- 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts index e61880cdd..9f5f9f912 100644 --- a/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts +++ b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts @@ -44,7 +44,8 @@ class FirstLightSmuggleAction extends PlayUnitAction { playType: PlayType.Smuggle, smuggleAspects: [Aspect.Vigilance, Aspect.Villainy], smuggleResourceCost: 7, - additionalCosts: [damageCost] + additionalCosts: [damageCost], + appendSmuggleToTitle: false }; } diff --git a/server/game/core/ability/PlayCardAction.ts b/server/game/core/ability/PlayCardAction.ts index c236ffc07..c6b6d66c3 100644 --- a/server/game/core/ability/PlayCardAction.ts +++ b/server/game/core/ability/PlayCardAction.ts @@ -32,6 +32,7 @@ export interface ISmuggleCardActionProperties extends IPlayCardActionPropertiesB playType: PlayType.Smuggle; smuggleResourceCost: number; smuggleAspects: Aspect[]; + appendSmuggleToTitle?: boolean; } export type IPlayCardActionProperties = IStandardPlayActionProperties | ISmuggleCardActionProperties; @@ -61,9 +62,11 @@ export abstract class PlayCardAction extends PlayerAction { let cost: number; let aspects: Aspect[]; + let appendSmuggleToTitle: boolean = null; if (properties.playType === PlayType.Smuggle) { cost = properties.smuggleResourceCost; aspects = properties.smuggleAspects; + appendSmuggleToTitle = properties.appendSmuggleToTitle; } else { cost = card.cost; aspects = card.aspects; @@ -75,7 +78,7 @@ export abstract class PlayCardAction extends PlayerAction { super( card, - PlayCardAction.getTitle(propertiesWithDefaults.title, propertiesWithDefaults.playType, usesExploit), + PlayCardAction.getTitle(propertiesWithDefaults.title, propertiesWithDefaults.playType, usesExploit, appendSmuggleToTitle), propertiesWithDefaults.additionalCosts.concat(playCost), propertiesWithDefaults.targetResolver, propertiesWithDefaults.triggerHandlingMode @@ -88,12 +91,12 @@ export abstract class PlayCardAction extends PlayerAction { this.createdWithProperties = { ...properties }; } - private static getTitle(title: string, playType: PlayType, withExploit: boolean = false): string { + private static getTitle(title: string, playType: PlayType, withExploit: boolean = false, appendToTitle: boolean = true): string { let updatedTitle = title; switch (playType) { case PlayType.Smuggle: - updatedTitle += ' with Smuggle'; + updatedTitle += appendToTitle ? ' with Smuggle' : ''; break; case PlayType.PlayFromHand: case PlayType.PlayFromOutOfPlay: diff --git a/test/helpers/PlayerInteractionWrapper.js b/test/helpers/PlayerInteractionWrapper.js index f8723f7f9..9cf7e24d2 100644 --- a/test/helpers/PlayerInteractionWrapper.js +++ b/test/helpers/PlayerInteractionWrapper.js @@ -220,7 +220,9 @@ class PlayerInteractionWrapper { card = this.findCardByName(options.card, prevZones, opponentControlled ? 'opponent' : null); } - if (card.isUnit() && card.defaultArena !== arenaName) { + if (!card.isUnit()) { + throw new TestSetupError(`Attempting to add non-unit card ${card.internalName} to ${arenaName}`); + } else if (card.defaultArena !== arenaName) { throw new TestSetupError(`Attempting to place ${card.internalName} in invalid arena '${arenaName}'`); } diff --git a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts index 6a041dca6..3730e772d 100644 --- a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts +++ b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts @@ -1,30 +1,132 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { integration(function(contextRef) { - describe('First Light\'s Smuggle ability', function() { + describe('First Light\'s', function() { beforeEach(function () { contextRef.setupTest({ phase: 'action', player1: { leader: 'qira#i-alone-survived', groundArena: ['wampa'], - resources: ['first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst'] + spaceArena: [{ card: 'tie-advanced', damage: 1, upgrades: ['shield'] }], + resources: ['first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst'], }, player2: { - spaceArena: ['cartel-spacer'] + spaceArena: ['cartel-spacer'], + hand: ['waylay'] } }); }); - it('should require dealing 4 damage to a friendly unit as a cost', function () { + it('Smuggle ability should require dealing 4 damage to a friendly unit as a cost, and its constant ability give grit to all friendly units', function () { const { context } = contextRef; context.player1.clickCard(context.firstLight); - expect(context.player1).toBeAbleToSelectExactly([context.wampa]); + expect(context.player1).toBeAbleToSelectExactly([context.wampa, context.tieAdvanced]); context.player1.clickCard(context.wampa); + expect(context.player1.exhaustedResourceCount).toBe(7); expect(context.firstLight).toBeInZone('spaceArena'); + + // check damage and grit expect(context.wampa.damage).toBe(4); expect(context.wampa.getPower()).toBe(8); + expect(context.tieAdvanced.damage).toBe(1); + expect(context.tieAdvanced.getPower()).toBe(4); + + // waylay First Light back to hand so we can play from hand and confirm the ability doesn't trigger + context.player2.clickCard(context.waylay); + context.player2.clickCard(context.firstLight); + + context.player1.readyResources(7); + context.player1.clickCard(context.firstLight); + expect(context.firstLight).toBeInZone('spaceArena'); + expect(context.player1.exhaustedResourceCount).toBe(7); + }); + + it('Smuggle ability should still work if a shield prevents the friendly unit damage', function () { + const { context } = contextRef; + + context.player1.clickCard(context.firstLight); + expect(context.player1).toBeAbleToSelectExactly([context.wampa, context.tieAdvanced]); + context.player1.clickCard(context.tieAdvanced); + + expect(context.firstLight).toBeInZone('spaceArena'); + expect(context.tieAdvanced.damage).toBe(1); + expect(context.tieAdvanced.isUpgraded()).toBeFalse(); + }); + }); + + it('First Light\'s Smuggle ability cannot trigger if there are no friendly units', function() { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'qira#i-alone-survived', + resources: ['first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst'], + }, + player2: { + spaceArena: ['cartel-spacer'] + } }); + + const { context } = contextRef; + + context.player1.clickCardNonChecking(context.firstLight); + expect(context.firstLight).toBeInZone('resource'); + expect(context.player1).toBeActivePlayer(); + }); + + it('First Light\'s Smuggle ability uses the correct cost aspects', function() { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'jyn-erso#resisting-oppression', + base: 'echo-base', + groundArena: ['wampa'], + // 11 total resources + resources: [ + 'first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', + 'atst', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst' + ], + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.firstLight); + expect(context.player1).toBeAbleToSelectExactly(context.wampa); + context.player1.clickCard(context.wampa); + + expect(context.firstLight).toBeInZone('spaceArena'); + expect(context.wampa.damage).toBe(4); + expect(context.player1.exhaustedResourceCount).toBe(11); + }); + + it('First Light\'s Smuggle ability will appear as an alternative to a gained Smuggle ability', function() { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'qira#i-alone-survived', + groundArena: ['tech#source-of-insight'], + resources: ['first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst', 'atst'], + }, + player2: { + spaceArena: ['cartel-spacer'] + } + }); + + const { context } = contextRef; + + context.player1.clickCard(context.firstLight); + expect(context.player1).toHaveExactPromptButtons([ + 'Play First Light with Smuggle by dealing 4 damage to a friendly unit', + 'Play First Light with Smuggle', + 'Cancel' + ]); + + context.player1.clickPrompt('Play First Light with Smuggle'); + expect(context.firstLight).toBeInZone('spaceArena'); + expect(context.player1.exhaustedResourceCount).toBe(9); }); }); + + // TODO: test with tie phantom and confirm it can't be used to pay the damage cost }); From 81231ebdbebd474c73f03c00b4a1740a29e544bc Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Wed, 8 Jan 2025 14:20:11 -0500 Subject: [PATCH 05/11] Improve Tech tests, add TODO --- ...tLightHeadquartersOfTheCrimsonDawn.spec.ts | 1 + .../02_SHD/units/TechSourceOfInsight.spec.ts | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts index 3730e772d..9bc610dbd 100644 --- a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts +++ b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts @@ -129,4 +129,5 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { }); // TODO: test with tie phantom and confirm it can't be used to pay the damage cost + // TODO: test with General's Blade or Lando leader to confirm that cost adjusters apply correctly }); diff --git a/test/server/cards/02_SHD/units/TechSourceOfInsight.spec.ts b/test/server/cards/02_SHD/units/TechSourceOfInsight.spec.ts index 0e1ff7a02..e63ce62cc 100644 --- a/test/server/cards/02_SHD/units/TechSourceOfInsight.spec.ts +++ b/test/server/cards/02_SHD/units/TechSourceOfInsight.spec.ts @@ -4,8 +4,9 @@ describe('Tech, Source of Insight', function () { contextRef.setupTest({ phase: 'action', player1: { - leader: 'hera-syndulla#spectre-two', + leader: { card: 'boba-fett#daimyo', deployed: true }, base: 'tarkintown', + groundArena: ['bendu#the-one-in-the-middle', 'death-trooper'], resources: [ 'tech#source-of-insight', 'wampa', @@ -34,6 +35,9 @@ describe('Tech, Source of Insight', function () { // smuggle Tech into play context.player1.clickCard(context.tech); + // check that arena units haven't gained the Smuggle keyword by checking for the Boba buff + expect(context.deathTrooper.getPower()).toBe(3); + reset(); // test smuggle with unit @@ -73,10 +77,23 @@ describe('Tech, Source of Insight', function () { // test smuggle with upgrade context.player1.clickCard(context.resilient); - expect(context.player1).toBeAbleToSelectExactly([context.tech, context.wampa, context.collectionsStarhopper]); + expect(context.player1).toBeAbleToSelectExactly( + [context.tech, context.wampa, context.collectionsStarhopper, context.bendu, context.bobaFett, context.deathTrooper] + ); context.player1.clickCard(context.collectionsStarhopper); expect(context.collectionsStarhopper).toHaveExactUpgradeNames(['resilient']); expect(context.player1.resources.length).toBe(7); + + reset(); + + // confirm that cost adjusters still work + context.player1.clickCard(context.bendu); + context.player1.clickCard(context.p2Base); + context.player2.passAction(); + + context.player1.clickCard(context.mercenaryCompany); + expect(context.mercenaryCompany).toBeInZone('groundArena'); + expect(context.player1.exhaustedResourceCount).toBe(6); }); From e996bd2145a8bda1820e96e2ca5523fdfb1a0e06 Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Wed, 8 Jan 2025 15:41:51 -0500 Subject: [PATCH 06/11] Improve Bamboozle tests --- test/server/cards/01_SOR/events/Bamboozle.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/server/cards/01_SOR/events/Bamboozle.spec.ts b/test/server/cards/01_SOR/events/Bamboozle.spec.ts index 5ff90782c..8d7ff3c31 100644 --- a/test/server/cards/01_SOR/events/Bamboozle.spec.ts +++ b/test/server/cards/01_SOR/events/Bamboozle.spec.ts @@ -86,6 +86,18 @@ describe('Bamboozle', function () { // no resource exhausted since the last action expect(context.player1.exhaustedResourceCount).toBe(2); + + reset(); + + // alternate play mode should no longer be available since no cunning card in hand + context.player1.clickCard(context.bamboozle); + expect(context.player1).toBeAbleToSelectExactly([context.battlefieldMarine, context.greenSquadronAwing, context.sawGerrera]); + context.player1.clickCard(context.greenSquadronAwing); + + expect(context.bamboozle).toBeInZone('discard'); + expect(context.player2).toBeActivePlayer(); + expect(context.p1Base.damage).toBe(2); + expect(context.player1.exhaustedResourceCount).toBe(4); }); it('Bamboozle\'s play modes should be available even if it is played by another card\'s effect', function () { From 73c85e839e3f9a5c8452bd71c46a1d3bb79af02c Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Thu, 9 Jan 2025 12:59:31 -0500 Subject: [PATCH 07/11] Add TODO --- server/game/cards/01_SOR/units/RelentlessKonstantinesFolly.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/game/cards/01_SOR/units/RelentlessKonstantinesFolly.ts b/server/game/cards/01_SOR/units/RelentlessKonstantinesFolly.ts index 7810f9441..cb3af4f3d 100644 --- a/server/game/cards/01_SOR/units/RelentlessKonstantinesFolly.ts +++ b/server/game/cards/01_SOR/units/RelentlessKonstantinesFolly.ts @@ -29,6 +29,7 @@ export default class RelentlessKonstantinesFolly extends NonLeaderUnitCard { }); } + // TODO: fix this to use "context.source" instead of "this" private isFirstEventPlayedByThisOpponentThisPhase(card) { return card.controller !== this.controller && card.type === CardType.Event && !this.cardsPlayedThisPhaseWatcher.someCardPlayed((playedCardEntry) => playedCardEntry.playedBy === card.controller && From f3f2c57e40f3decb6d888b49e81d83f629d20720 Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Thu, 9 Jan 2025 22:38:03 -0500 Subject: [PATCH 08/11] Initial falcon2 test working --- .../units/MillenniumFalconLandosPride.ts | 30 +++++++++++++++++++ server/game/core/card/LeaderCard.ts | 4 +-- server/game/core/card/LeaderUnitCard.ts | 4 +-- .../propertyMixins/StandardAbilitySetup.ts | 4 +-- .../units/MillenniumFalconLandosPride.spec.ts | 30 +++++++++++++++++++ 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 server/game/cards/02_SHD/units/MillenniumFalconLandosPride.ts create mode 100644 test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts diff --git a/server/game/cards/02_SHD/units/MillenniumFalconLandosPride.ts b/server/game/cards/02_SHD/units/MillenniumFalconLandosPride.ts new file mode 100644 index 000000000..96fcc8535 --- /dev/null +++ b/server/game/cards/02_SHD/units/MillenniumFalconLandosPride.ts @@ -0,0 +1,30 @@ +import AbilityHelper from '../../../AbilityHelper'; +import { NonLeaderUnitCard } from '../../../core/card/NonLeaderUnitCard'; +import { EventName, KeywordName, PlayType } from '../../../core/Constants'; + +export default class MillenniumFalconLandosPride extends NonLeaderUnitCard { + protected override getImplementationId() { + return { + id: '5752414373', + internalName: 'millennium-falcon#landos-pride', + }; + } + + public override setupCardAbilities(sourceCard: this) { + let lastPlayedFromHandId: number | null = null; + + this.game.on(EventName.OnCardPlayed, (event) => { + if (event.card === sourceCard && event.playType === PlayType.PlayFromHand) { + lastPlayedFromHandId = event.card.inPlayId; + } + }); + + this.addConstantAbility({ + title: 'This unit gains Ambush if it was played from hand', + condition: (context) => context.source.isInPlay() && lastPlayedFromHandId === context.source.inPlayId, + ongoingEffect: AbilityHelper.ongoingEffects.gainKeyword(KeywordName.Ambush) + }); + } +} + +MillenniumFalconLandosPride.implemented = true; diff --git a/server/game/core/card/LeaderCard.ts b/server/game/core/card/LeaderCard.ts index a2ebb591a..d96d1052e 100644 --- a/server/game/core/card/LeaderCard.ts +++ b/server/game/core/card/LeaderCard.ts @@ -19,7 +19,7 @@ export class LeaderCard extends InPlayCard { this.hasImplementationFile = true; this.setupLeaderUnitSide = false; - this.setupLeaderSideAbilities(); + this.setupLeaderSideAbilities(this); } public override isLeader(): this is LeaderCard { @@ -33,7 +33,7 @@ export class LeaderCard extends InPlayCard { /** * Create card abilities for the leader (non-unit) side by calling subsequent methods with appropriate properties */ - protected setupLeaderSideAbilities() { + protected setupLeaderSideAbilities(sourceCard: this) { this.hasImplementationFile = false; } diff --git a/server/game/core/card/LeaderUnitCard.ts b/server/game/core/card/LeaderUnitCard.ts index f00e5e578..d4906230a 100644 --- a/server/game/core/card/LeaderUnitCard.ts +++ b/server/game/core/card/LeaderUnitCard.ts @@ -23,7 +23,7 @@ export class LeaderUnitCard extends LeaderUnitCardParent { super(owner, cardData); this.setupLeaderUnitSide = true; - this.setupLeaderUnitSideAbilities(); + this.setupLeaderUnitSideAbilities(this); // add deploy leader action this.addActionAbility({ @@ -71,7 +71,7 @@ export class LeaderUnitCard extends LeaderUnitCardParent { * Create card abilities for the leader unit side by calling subsequent methods with appropriate properties */ // eslint-disable-next-line @typescript-eslint/no-empty-function - protected setupLeaderUnitSideAbilities() { + protected setupLeaderUnitSideAbilities(sourceCard: this) { } protected override addActionAbility(properties: IActionAbilityProps) { diff --git a/server/game/core/card/propertyMixins/StandardAbilitySetup.ts b/server/game/core/card/propertyMixins/StandardAbilitySetup.ts index b9a4b64dd..d5380c8a9 100644 --- a/server/game/core/card/propertyMixins/StandardAbilitySetup.ts +++ b/server/game/core/card/propertyMixins/StandardAbilitySetup.ts @@ -9,7 +9,7 @@ export function WithStandardAbilitySetup(Bas super(...args); this.hasImplementationFile = true; - this.setupCardAbilities(); + this.setupCardAbilities(this); // if an implementation file is provided, enforce that all keywords requiring explicit setup have been set up if (this.hasImplementationFile) { @@ -25,7 +25,7 @@ export function WithStandardAbilitySetup(Bas /** * Create card abilities by calling subsequent methods with appropriate properties */ - protected setupCardAbilities() { + protected setupCardAbilities(sourceCard: this) { this.hasImplementationFile = false; } }; diff --git a/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts b/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts new file mode 100644 index 000000000..0e849bb7f --- /dev/null +++ b/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts @@ -0,0 +1,30 @@ +describe('Millennium Falcon, Landos Pride', function() { + integration(function(contextRef) { + describe('Millennium Falcon\'s constant ability', function() { + beforeEach(function () { + contextRef.setupTest({ + phase: 'action', + player1: { + hand: ['millennium-falcon#landos-pride'], + groundArena: ['wampa'], + }, + player2: { + spaceArena: ['survivors-gauntlet'] + } + }); + }); + + it('should give it Ambush if it is played from hand', function () { + const { context } = contextRef; + + context.player1.clickCard(context.millenniumFalcon); + expect(context.player1).toHavePassAbilityPrompt('Ambush'); + context.player1.clickPrompt('Ambush'); + + context.player1.clickCard(context.survivorsGauntlet); + expect(context.survivorsGauntlet.damage).toBe(5); + expect(context.millenniumFalcon.damage).toBe(4); + }); + }); + }); +}); From a5e281fb267cde17dbb9b93a3d6865f94973a932 Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Thu, 9 Jan 2025 22:54:03 -0500 Subject: [PATCH 09/11] Finish Millennium Falcon tests --- .../units/MillenniumFalconLandosPride.spec.ts | 65 +++++++++++++------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts b/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts index 0e849bb7f..701741806 100644 --- a/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts +++ b/test/server/cards/02_SHD/units/MillenniumFalconLandosPride.spec.ts @@ -1,30 +1,55 @@ describe('Millennium Falcon, Landos Pride', function() { integration(function(contextRef) { - describe('Millennium Falcon\'s constant ability', function() { - beforeEach(function () { - contextRef.setupTest({ - phase: 'action', - player1: { - hand: ['millennium-falcon#landos-pride'], - groundArena: ['wampa'], - }, - player2: { - spaceArena: ['survivors-gauntlet'] - } - }); + it('Millennium Falcon\'s constant ability should give it Ambush if it is played from hand', function () { + contextRef.setupTest({ + phase: 'action', + player1: { + hand: ['millennium-falcon#landos-pride', 'palpatines-return'], + resources: 30 + }, + player2: { + spaceArena: ['survivors-gauntlet', 'ruthless-raider'] + } }); - it('should give it Ambush if it is played from hand', function () { - const { context } = contextRef; + const { context } = contextRef; - context.player1.clickCard(context.millenniumFalcon); - expect(context.player1).toHavePassAbilityPrompt('Ambush'); - context.player1.clickPrompt('Ambush'); + // CASE 1: Falcon gets Ambush when played from hand + context.player1.clickCard(context.millenniumFalcon); + expect(context.player1).toHavePassAbilityPrompt('Ambush'); + context.player1.clickPrompt('Ambush'); - context.player1.clickCard(context.survivorsGauntlet); - expect(context.survivorsGauntlet.damage).toBe(5); - expect(context.millenniumFalcon.damage).toBe(4); + context.player1.clickCard(context.survivorsGauntlet); + expect(context.survivorsGauntlet.damage).toBe(5); + expect(context.millenniumFalcon.damage).toBe(4); + + // CASE 2: same copy of Falcon has lost its Ambush if played from discard + context.player2.clickCard(context.survivorsGauntlet); + context.player2.clickCard(context.millenniumFalcon); + + context.player1.clickCard(context.palpatinesReturn); + context.player1.clickCard(context.millenniumFalcon); + expect(context.millenniumFalcon).toBeInZone('spaceArena'); + expect(context.player2).toBeActivePlayer(); + }); + + it('Millennium Falcon\'s constant ability should not give it Ambush if it is Smuggled', function () { + contextRef.setupTest({ + phase: 'action', + player1: { + leader: 'jyn-erso#resisting-oppression', + resources: ['millennium-falcon#landos-pride', 'atst', 'atst', 'atst', 'atst', 'atst'] + }, + player2: { + spaceArena: ['survivors-gauntlet', 'ruthless-raider'] + } }); + + const { context } = contextRef; + + context.player1.clickCard(context.millenniumFalcon); + expect(context.millenniumFalcon).toBeInZone('spaceArena'); + expect(context.player2).toBeActivePlayer(); }); }); }); From 9bd367f0707164afa672faf5c6ebfc71576c0b9c Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Fri, 10 Jan 2025 10:12:54 -0500 Subject: [PATCH 10/11] PR fixes --- .../02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts | 1 + .../units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts index 9f5f9f912..d73f2e01b 100644 --- a/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts +++ b/server/game/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.ts @@ -25,6 +25,7 @@ export default class FirstLightHeadquartersOfTheCrimsonDawn extends NonLeaderUni this.addConstantAbility({ title: 'Each other friendly non-leader unit gains Grit', targetController: RelativePlayer.Self, + targetCardTypeFilter: WildcardCardType.NonLeaderUnit, matchTarget: (card, context) => card !== context.source, ongoingEffect: AbilityHelper.ongoingEffects.gainKeyword(KeywordName.Grit) }); diff --git a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts index 9bc610dbd..5116fb644 100644 --- a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts +++ b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts @@ -78,7 +78,7 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { contextRef.setupTest({ phase: 'action', player1: { - leader: 'jyn-erso#resisting-oppression', + leader: { card: 'jyn-erso#resisting-oppression', deployed: true, damage: 2 }, base: 'echo-base', groundArena: ['wampa'], // 11 total resources @@ -98,6 +98,9 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { expect(context.firstLight).toBeInZone('spaceArena'); expect(context.wampa.damage).toBe(4); expect(context.player1.exhaustedResourceCount).toBe(11); + + // confirm that leader unit doesn't get grit + expect(context.jynErso.damage).toBe(4); }); it('First Light\'s Smuggle ability will appear as an alternative to a gained Smuggle ability', function() { From b87f283e938140f5518fe6a0c200a582d965a185 Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Fri, 10 Jan 2025 10:21:59 -0500 Subject: [PATCH 11/11] Bug fix --- .../FirstLightHeadquartersOfTheCrimsonDawn.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts index 5116fb644..950dcd0f9 100644 --- a/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts +++ b/test/server/cards/02_SHD/units/FirstLightHeadquartersOfTheCrimsonDawn.spec.ts @@ -78,9 +78,8 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { contextRef.setupTest({ phase: 'action', player1: { - leader: { card: 'jyn-erso#resisting-oppression', deployed: true, damage: 2 }, + leader: { card: 'jyn-erso#resisting-oppression', deployed: true }, base: 'echo-base', - groundArena: ['wampa'], // 11 total resources resources: [ 'first-light#headquarters-of-the-crimson-dawn', 'atst', 'atst', 'atst', @@ -92,15 +91,15 @@ describe('First Light, Headquarters of the Crimson Dawn', function() { const { context } = contextRef; context.player1.clickCard(context.firstLight); - expect(context.player1).toBeAbleToSelectExactly(context.wampa); - context.player1.clickCard(context.wampa); + expect(context.player1).toBeAbleToSelectExactly(context.jynErso); + context.player1.clickCard(context.jynErso); expect(context.firstLight).toBeInZone('spaceArena'); - expect(context.wampa.damage).toBe(4); + expect(context.jynErso.damage).toBe(4); expect(context.player1.exhaustedResourceCount).toBe(11); // confirm that leader unit doesn't get grit - expect(context.jynErso.damage).toBe(4); + expect(context.jynErso.getPower()).toBe(4); }); it('First Light\'s Smuggle ability will appear as an alternative to a gained Smuggle ability', function() {