Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jango Fett, Concealing the Conspiracy #537

Merged
merged 12 commits into from
Feb 13, 2025
2 changes: 2 additions & 0 deletions server/game/IDamageOrDefeatSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface IDamagedOrDefeatedByAttack extends IDamageOrDefeatSourceBase {
export interface IDamagedOrDefeatedByAbility extends IDamageOrDefeatSourceBase {
type: DamageSourceType.Ability | DefeatSourceType.Ability;
card: Card;
/* The player controlling the card that caused the damage */
controller?: Player;
event: any;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,19 @@ export default class JangoFettConcealingTheConspiracy extends LeaderUnitCard {
}

private isEnemyUnitDamagedByFriendlyUnit(event, context): boolean {
if (event.card.isUnit() && event.card.controller !== context.source.controller) {
console.log('----------------------');
console.log(`[Damage Recieved] ${event.card.title}`);
// If an enemy unit received the damage
if (event.card.isUnit() && event.card.controller !== context.player) {
switch (event.damageSource.type) {
case DamageSourceType.Ability:
const controller = event.damageSource.controller ?? event.damageSource.card.controller;
// If the damage was dealt by a friendly unit via an ability
return event.damageSource.card.isUnit() &&
controller === context.player;

if (
event.damageSource.type === DamageSourceType.Ability &&
event.damageSource.card.isUnit() &&
event.damageSource.player === context.source.controller
) {
console.log(`[Ability Damage Source] ${event.damageSource.card.title}`);
return true;
}

if (
event.damageSource.type === DamageSourceType.Attack &&
event.damageSource.damageDealtBy.isUnit() &&
event.damageSource.damageDealtBy.controller === context.source.controller
) {
console.log(`[Attack Damage Source] ${event.damageSource.damageDealtBy.title}`);
return true;
case DamageSourceType.Attack:
// If the damage was dealt by a friendly unit via combat
return event.damageSource.damageDealtBy.isUnit() &&
event.damageSource.damageDealtBy.controller === context.player;
}
}

Expand Down
9 changes: 8 additions & 1 deletion server/game/gameSystems/DamageSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Attack } from '../core/attack/Attack';
import type { IDamagedOrDefeatedByAbility, IDamagedOrDefeatedByAttack } from '../IDamageOrDefeatSource';
import { DamageSourceType } from '../IDamageOrDefeatSource';
import type { UnitCard } from '../core/card/CardTypes';
import { TriggeredAbilityContext } from '../core/ability/TriggeredAbilityContext';

export interface IDamagePropertiesBase extends ICardTargetSystemProperties {
type: DamageType;
Expand Down Expand Up @@ -250,7 +251,6 @@ export class DamageSystem<TContext extends AbilityContext = AbilityContext, TPro
event.sourceEventForExcessDamage = properties.sourceEventForExcessDamage;
}

// TODO: confirm that this works when the player controlling the ability is different than the player controlling the card (e.g., bounty)
private addAbilityDamagePropertiesToEvent(event: any, card: Card, context: TContext, properties: IAbilityDamageProperties): void {
const abilityDamageSource: IDamagedOrDefeatedByAbility = {
type: DamageSourceType.Ability,
Expand All @@ -259,6 +259,13 @@ export class DamageSystem<TContext extends AbilityContext = AbilityContext, TPro
event
};

if (context instanceof TriggeredAbilityContext && context.event.name === EventName.OnCardDefeated) {
// For the case where a stolen card is defeated, the card.controller has already reverted back
// to the card's owner. We need to use the last known information to get the correct controller
// for damage attribution (e.g. for Jango's ability)
abilityDamageSource.controller = context.event.lastKnownInformation.controller;
}

event.damageSource = abilityDamageSource;
event.amount = typeof properties.amount === 'function' ? (properties.amount as (Event) => number)(card) : properties.amount;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
'overwhelming-barrage',
'sneak-attack',
'ruthless-raider',
'change-of-heart',
],
groundArena: [
'crafty-smuggler',
Expand All @@ -24,7 +25,8 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
'battlefield-marine',
'mandalorian-warrior',
'fleet-lieutenant',
'volunteer-soldier'
'volunteer-soldier',
'consular-security-force',
],
},
});
Expand Down Expand Up @@ -101,13 +103,18 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
'Exhaust this leader'
]);

// HELP WANTED: The prompts in this test case are not what I expected.
// It prompts to resolve another instance after `Exhaust this leader`
// is clicked.
// Pass for Volunteer Soldier
context.player1.clickPrompt('Exhaust this leader');
expect(context.player1).toHavePassAbilityPrompt('Exhaust this leader');
context.player1.clickPrompt('Pass');
expect(context.jangoFett.exhausted).toBeFalse();

// These are just here to resolve the prompts and make the test pass
// Resolve for Fleet Lieutenant
context.player1.clickPrompt('Exhaust this leader');
expect(context.player1).toHavePassAbilityPrompt('Exhaust this leader');
context.player1.clickPrompt('Exhaust this leader');
expect(context.fleetLieutenant.exhausted).toBeTrue();
expect(context.jangoFett.exhausted).toBeTrue();

// CASE 5: Trigger Jango's ability from an upgrade's granted ability

Expand All @@ -124,7 +131,8 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
[context.fleetLieutenant, 1],
]));

// Choose resolution order (is there a way to know which instant corresponds to which damaged unit?)
// Choose resolution order
// TODO: Determine which trigger corresponds to which damaged unit: https://github.com/SWU-Karabast/forceteki/issues/540
context.player1.clickPrompt('Exhaust this leader');

// Pass for Fleet Lieutenant
Expand Down Expand Up @@ -161,6 +169,26 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
// Use Jango's ability to exhaust Mandalorian Warrior
context.player1.clickPrompt('Exhaust this leader');
expect(context.mandalorianWarrior.exhausted).toBeTrue();

// CASE 7: Trigger Jango's ability when a stolen unit attacks

context.nextPhase();
reset();

// Play Change of Heart to steal Mandalorian Warrior
context.player1.clickCard(context.changeOfHeart);
context.player1.clickCard(context.mandalorianWarrior);

context.player2.passAction();

// Attack with Mandalorian Warrior
context.player1.clickCard(context.mandalorianWarrior);
context.player1.clickCard(context.consularSecurityForce);

// Use Jango's ability to exhaust Consular Security Force
expect(context.player1).toHavePassAbilityPrompt('Exhaust this leader');
context.player1.clickPrompt('Exhaust this leader');
expect(context.consularSecurityForce.exhausted).toBeTrue();
});

it('should not trigger for specific scenarios', function() {
Expand All @@ -175,7 +203,8 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
],
groundArena: [
'grogu#irresistible',
'darth-vader#commanding-the-first-legion'
'darth-vader#commanding-the-first-legion',
'doctor-pershing#experimenting-with-life',
],
leader: 'jango-fett#concealing-the-conspiracy',
},
Expand All @@ -191,15 +220,15 @@ describe('Jango Fett, Concealing the Conspiracy', function () {

const { context } = contextRef;

// CASE 7: Damage dealt by an event does not trigger Jango's ability
// CASE 8: Damage dealt by an event does not trigger Jango's ability

context.player1.clickCard(context.openFire);
context.player1.clickCard(context.atst);

expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');
expect(context.atst.damage).toBe(4);

// CASE 8: Damage dealt by an upgrade does not trigger Jango's ability
// CASE 9: Damage dealt by an upgrade does not trigger Jango's ability

context.moveToNextActionPhase();

Expand All @@ -210,7 +239,7 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');
expect(context.liberatedSlaves.damage).toBe(4);

// CASE 9: Attacks that deal 0 damage do not trigger Jango's ability
// CASE 10: Attacks that deal 0 damage do not trigger Jango's ability

context.moveToNextActionPhase();

Expand All @@ -221,7 +250,7 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');
expect(context.consularSecurityForce.damage).toBe(0);

// CASE 10: Jango should still be exhausted and unable to trigger his ability when the phase ends
// CASE 11: Jango should still be exhausted and unable to trigger his ability when the phase ends

context.moveToNextActionPhase();

Expand All @@ -246,6 +275,23 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
// Jango is still exhausted and his ability is unavailable
expect(context.jangoFett.exhausted).toBeTrue();
expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');

// CASE 12: A friendly unit dealing damage to another friendly unit does not trigger Jango's ability

context.nextPhase();
context.grogu.damage = 0;

// Use Pershing's ability
context.player1.clickCard(context.doctorPershing);
context.player1.clickPrompt('Draw a card');

// Deal damage to grogu
context.player1.clickCard(context.grogu);

expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');
expect(context.jangoFett.exhausted).toBeFalse();
expect(context.grogu.exhausted).toBeFalse();
expect(context.grogu.damage).toBe(1);
});
});

Expand All @@ -258,6 +304,7 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
'elite-p38-starfighter',
'overwhelming-barrage',
'strike-true',
'change-of-heart',
],
groundArena: [
'crafty-smuggler',
Expand All @@ -271,7 +318,8 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
'battlefield-marine',
'mandalorian-warrior',
'fleet-lieutenant',
'volunteer-soldier'
'volunteer-soldier',
'consular-security-force',
],
},
});
Expand Down Expand Up @@ -406,6 +454,26 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
expect(context.fleetLieutenant.exhausted).toBeTrue();
expect(context.battlefieldMarine.exhausted).toBeTrue();
expect(context.volunteerSoldier.exhausted).toBeTrue();

// CASE 6: Trigger Jango's ability when a stolen unit attacks

context.moveToNextActionPhase();
reset();

// Play Change of Heart to steal Mandalorian Warrior
context.player1.clickCard(context.changeOfHeart);
context.player1.clickCard(context.mandalorianWarrior);

context.player2.passAction();

// Attack with Mandalorian Warrior
context.player1.clickCard(context.mandalorianWarrior);
context.player1.clickCard(context.consularSecurityForce);

// Use Jango's ability to exhaust Consular Security Force
expect(context.player1).toHavePassAbilityPrompt('Exhaust the damaged enemy unit');
context.player1.clickPrompt('Exhaust the damaged enemy unit');
expect(context.consularSecurityForce.exhausted).toBeTrue();
});
});

Expand All @@ -420,9 +488,10 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
],
groundArena: [
'grogu#irresistible',
'darth-vader#commanding-the-first-legion'
'darth-vader#commanding-the-first-legion',
'doctor-pershing#experimenting-with-life',
],
leader: 'jango-fett#concealing-the-conspiracy',
leader: { card: 'jango-fett#concealing-the-conspiracy', deployed: true },
},
player2: {
groundArena: [
Expand All @@ -435,15 +504,15 @@ describe('Jango Fett, Concealing the Conspiracy', function () {

const { context } = contextRef;

// CASE 6: Damage dealt by an event does not trigger Jango's ability
// CASE 7: Damage dealt by an event does not trigger Jango's ability

context.player1.clickCard(context.openFire);
context.player1.clickCard(context.atst);

expect(context.player1).not.toHavePassAbilityPrompt('Exhaust the damaged enemy unit');
expect(context.atst.damage).toBe(4);

// CASE 7: Damage dealt by an upgrade does not trigger Jango's ability
// CASE 8: Damage dealt by an upgrade does not trigger Jango's ability

context.moveToNextActionPhase();

Expand All @@ -454,16 +523,32 @@ describe('Jango Fett, Concealing the Conspiracy', function () {
expect(context.player1).not.toHavePassAbilityPrompt('Exhaust the damaged enemy unit');
expect(context.liberatedSlaves.damage).toBe(4);

// CASE 8: Attacks that deal 0 damage do not trigger Jango's ability
// CASE 9: Attacks that deal 0 damage do not trigger Jango's ability

context.moveToNextActionPhase();

context.player1.clickCard(context.grogu);
context.player1.clickPrompt('Attack');
context.player1.clickCard(context.consularSecurityForce);

expect(context.player1).not.toHavePassAbilityPrompt('Exhaust this leader');
expect(context.player1).not.toHavePassAbilityPrompt('Exhaust the damaged enemy unit');
expect(context.consularSecurityForce.damage).toBe(0);

// CASE 10: A friendly unit dealing damage to another friendly unit does not trigger Jango's ability

context.moveToNextActionPhase();
context.grogu.damage = 0;

// Use Pershing's ability
context.player1.clickCard(context.doctorPershing);
context.player1.clickPrompt('Draw a card');

// Deal damage to grogu
context.player1.clickCard(context.grogu);

expect(context.player1).not.toHavePassAbilityPrompt('Exhaust the damaged enemy unit');
expect(context.grogu.exhausted).toBeFalse();
expect(context.grogu.damage).toBe(1);
});
});
});
Expand Down
Loading