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
4 changes: 4 additions & 0 deletions server/game/IDamageOrDefeatSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface IDamagedOrDefeatedByAttack extends IDamageOrDefeatSourceBase {
export interface IDamagedOrDefeatedByAbility extends IDamageOrDefeatSourceBase {
type: DamageSourceType.Ability | DefeatSourceType.Ability;
card: Card;
// TODO: We should eventually make this non-optional when we can update all the
// existing code and guarantee that it's always set.
/* The player controlling the card that caused the damage */
controller?: Player;
event: any;
}

Expand Down
3 changes: 2 additions & 1 deletion server/game/cards/01_SOR/events/OverwhelmingBarrage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default class OverwhelmingBarrage extends EventCard {
canChooseNoTargets: true,
controller: WildcardRelativePlayer.Any,
cardTypeFilter: WildcardCardType.Unit,
cardCondition: (card) => card !== thenContext.target
cardCondition: (card) => card !== thenContext.target,
source: thenContext.target
})
})
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import AbilityHelper from '../../../AbilityHelper';
import { LeaderUnitCard } from '../../../core/card/LeaderUnitCard';
import { DamageSourceType } from '../../../IDamageOrDefeatSource';

export default class JangoFettConcealingTheConspiracy extends LeaderUnitCard {
protected override getImplementationId() {
return {
id: '9155536481',
internalName: 'jango-fett#concealing-the-conspiracy',
};
}

protected override setupLeaderSideAbilities() {
// When a friendly unit deals damage to an enemy unit:
// You may exhaust this leader.
// If you do, exhaust that enemy unit.
this.addTriggeredAbility({
title: 'Exhaust this leader',
optional: true,
when: {
onDamageDealt: (event, context) => this.isEnemyUnitDamagedByFriendlyUnit(event, context)
},
immediateEffect: AbilityHelper.immediateEffects.exhaust(),
ifYouDo: (ifYouDoContext) => ({
title: 'Exhaust the damaged enemy unit',
immediateEffect: AbilityHelper.immediateEffects.exhaust(
{ target: ifYouDoContext.event.card }
)
})
});
}

protected override setupLeaderUnitSideAbilities() {
// When a friendly unit deals damage to an enemy unit:
// You may exhaust that unit.
this.addTriggeredAbility({
title: 'Exhaust the damaged enemy unit',
optional: true,
when: {
onDamageDealt: (event, context) => this.isEnemyUnitDamagedByFriendlyUnit(event, context)
},
immediateEffect: AbilityHelper.immediateEffects.exhaust((context) => {
return { target: context.event.card };
})
});
}

private isEnemyUnitDamagedByFriendlyUnit(event, context): boolean {
// If an enemy unit received the damage
if (event.card.isUnit() && event.card.controller !== context.player) {
switch (event.damageSource.type) {
case DamageSourceType.Ability:
// TODO: event.damageSource.controller will eventually be non-optional
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;

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;
}
}

return false;
}
}
8 changes: 7 additions & 1 deletion server/game/gameSystems/DamageSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,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 +258,13 @@ export class DamageSystem<TContext extends AbilityContext = AbilityContext, TPro
event
};

if (context.isTriggered() && 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
5 changes: 4 additions & 1 deletion server/game/gameSystems/DistributeAmongTargetsSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export interface IDistributeAmongTargetsSystemProperties<TContext extends Abilit
maxTargets?: number;
}

export abstract class DistributeAmongTargetsSystem<TContext extends AbilityContext = AbilityContext> extends CardTargetSystem<TContext, IDistributeAmongTargetsSystemProperties> {
export abstract class DistributeAmongTargetsSystem<
TContext extends AbilityContext = AbilityContext,
TProperties extends IDistributeAmongTargetsSystemProperties<TContext> = IDistributeAmongTargetsSystemProperties<TContext>
> extends CardTargetSystem<TContext, TProperties> {
protected override readonly targetTypeFilter = [WildcardCardType.Unit, CardType.Base];
protected override defaultProperties: IDistributeAmongTargetsSystemProperties<TContext> = {
amountToDistribute: null,
Expand Down
14 changes: 11 additions & 3 deletions server/game/gameSystems/DistributeDamageSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,28 @@ import type { IDistributeAmongTargetsSystemProperties } from './DistributeAmongT
import { DistributeAmongTargetsSystem } from './DistributeAmongTargetsSystem';
import type { HealSystem } from './HealSystem';

export type IDistributeDamageSystemProperties<TContext extends AbilityContext = AbilityContext> = IDistributeAmongTargetsSystemProperties<TContext>;
export interface IDistributeDamageSystemProperties<TContext extends AbilityContext = AbilityContext> extends IDistributeAmongTargetsSystemProperties<TContext> {

/** The source of the damage, if different from the card that triggered the ability */
source?: Card;
}

/**
* System for distributing damage among target cards.
* Will prompt the user to select where to put the damage (unless auto-selecting a single target is possible).
*/
export class DistributeDamageSystem<TContext extends AbilityContext = AbilityContext> extends DistributeAmongTargetsSystem<TContext> {
export class DistributeDamageSystem<
TContext extends AbilityContext = AbilityContext,
TProperties extends IDistributeDamageSystemProperties<TContext> = IDistributeDamageSystemProperties<TContext>
> extends DistributeAmongTargetsSystem<TContext, TProperties> {
protected override readonly eventName = MetaEventName.DistributeDamage;
public override readonly name = 'distributeDamage';

public override promptType: DistributePromptType = StatefulPromptType.DistributeDamage;

protected override generateEffectSystem(target: Card = null, amount = 1): DamageSystem | HealSystem {
return new DamageSystem({ type: DamageType.Ability, target, amount });
const { source } = this.properties;
return new DamageSystem({ type: DamageType.Ability, target, amount, source });
}

// most "distribute damage" abilities require all damage to be dealt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('Maul, Shadow Collective Visionary', function() {
expect(context.mercenaryCompany.damage).toBe(4);
});

it('redirects combat damage to another friendly Underworld unit, handling shields correctly', function () {
it('redirects combat damage to another friendly Underworld unit, handling shields and damage attribution correctly', function () {
contextRef.setupTest({
phase: 'action',
player1: {
Expand All @@ -171,7 +171,8 @@ describe('Maul, Shadow Collective Visionary', function() {
]
},
player2: {
groundArena: ['luminara-unduli#softspoken-master']
groundArena: ['luminara-unduli#softspoken-master'],
leader: 'jango-fett#concealing-the-conspiracy'
}
});

Expand Down Expand Up @@ -221,9 +222,26 @@ describe('Maul, Shadow Collective Visionary', function() {
expect(context.maul.isUpgraded()).toBeFalse();
expect(context.luminaraUnduli.damage).toBe(7);
expect(context.mercenaryCompany.damage).toBe(0);

// CASE 3: Deflect damage to Mercenary Company, opponent is able to exhaust Mercenary Company with Jango Fett's ability

context.moveToNextActionPhase();

context.player1.clickCard(context.maul);
context.player1.clickCard(context.luminaraUnduli);

expect(context.player1).toBeAbleToSelectExactly([context.mercenaryCompany]);
expect(context.player1).toHavePassAbilityButton();
context.player1.clickCard(context.mercenaryCompany);

// Resolve Jango's ability
expect(context.player2).toHavePassAbilityPrompt('Exhaust this leader');
context.player2.clickPrompt('Exhaust this leader');

expect(context.mercenaryCompany.exhausted).toBeTrue();
expect(context.mercenaryCompany.damage).toBe(4);
expect(context.maul.damage).toBe(0);
});
});
});

// TODO: test with Jango leader for attribution
});
Loading