forked from SWU-Karabast/forceteki
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTriggeredAbility.ts
170 lines (151 loc) · 6.6 KB
/
TriggeredAbility.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import CardAbility from './CardAbility';
import { TriggeredAbilityContext } from './TriggeredAbilityContext';
import { Stage, CardType, EffectName, AbilityType } from '../Constants';
import { ITriggeredAbilityProps, WhenType } from '../../Interfaces';
import { GameEvent } from '../event/GameEvent';
import { Card } from '../card/Card';
import Game from '../Game';
import { TriggeredAbilityWindow } from '../gameSteps/abilityWindow/TriggeredAbilityWindow';
import Contract from '../utils/Contract';
import type CardAbilityStep from './CardAbilityStep';
import { CardWithTriggeredAbilities } from '../card/CardTypes';
interface IEventRegistration {
name: string;
handler: (event: GameEvent, window: TriggeredAbilityWindow) => void;
}
/**
* Represents a reaction ability provided by card text.
*
* Properties:
* when - object whose keys are event names to listen to for the reaction and
* whose values are functions that return a boolean about whether to
* trigger the reaction when that event is fired. For example, to
* trigger only at the end of the challenge phase, you would do:
* when: {
* onPhaseEnded: event => event.phase === 'conflict'
* }
* Multiple events may be specified for cards that have multiple
* possible triggers for the same reaction.
* title - string which is displayed to the player to reference this ability
* cost - object or array of objects representing the cost required to be
* paid before the action will activate. See Costs.
* target - object giving properties for the target API
* handler - function that will be executed if the player chooses 'Yes' when
* asked to trigger the reaction. If the reaction has more than one
* choice, use the choices sub object instead. Defaults to
* {@link CardAbilityStep.executeGameActions}
* limit - optional AbilityLimit object that represents the max number of uses
* for the reaction as well as when it resets.
* location - string or array of strings indicating the location the card should
* be in in order to activate the reaction. Defaults to 'play area'.
*/
export default class TriggeredAbility extends CardAbility {
public when?: WhenType;
public aggregateWhen?: (events: GameEvent[], context: TriggeredAbilityContext) => boolean;
public anyPlayer: boolean;
public collectiveTrigger: boolean;
public eventRegistrations?: IEventRegistration[];
public eventsTriggeredFor: GameEvent[] = [];
public constructor(
game: Game,
card: Card,
properties: ITriggeredAbilityProps,
abilityType: AbilityType = AbilityType.Triggered
) {
super(game, card, properties, abilityType);
if (!card.canRegisterTriggeredAbilities()) {
throw Error(`Card '${card.internalName}' cannot have triggered abilities`);
}
if ('when' in properties) {
this.when = properties.when;
} else if ('aggregateWhen' in properties) {
this.aggregateWhen = properties.aggregateWhen;
}
this.collectiveTrigger = !!properties.collectiveTrigger;
}
public eventHandler(event, window) {
if (!Contract.assertNotNullLike(window)) {
return;
}
for (const player of this.game.getPlayers()) {
const context = this.createContext(player, event);
//console.log(event.name, this.card.name, this.isTriggeredByEvent(event, context), this.meetsRequirements(context));
if (
(this.card as CardWithTriggeredAbilities).getTriggeredAbilities().includes(this) &&
this.isTriggeredByEvent(event, context) &&
this.meetsRequirements(context) === '' &&
!this.eventsTriggeredFor.includes(event)
) {
this.eventsTriggeredFor.push(event);
window.addToWindow(context);
}
}
}
public override meetsRequirements(context, ignoredRequirements = []) {
const canPlayerTrigger = this.anyPlayer || context.player === this.card.controller;
if (!ignoredRequirements.includes('player') && !canPlayerTrigger) {
if (!context.player.isCardInPlayableLocation(this.card, context.playType)) {
return 'player';
}
}
return super.meetsRequirements(context, ignoredRequirements);
}
public override createContext(player = this.card.controller, event: GameEvent) {
return new TriggeredAbilityContext({
event: event,
game: this.game,
source: this.card,
player: player,
ability: this,
stage: Stage.PreTarget
});
}
public registerEvents() {
if (this.eventRegistrations) {
return;
} else if (this.aggregateWhen) {
const event = {
name: 'aggregateEvent:' + this.type,
handler: (events, window) => this.checkAggregateWhen(events, window)
};
this.eventRegistrations = [event];
this.game.on(event.name, event.handler);
return;
}
const eventNames = Object.keys(this.when);
this.eventRegistrations = [];
eventNames.forEach((eventName) => {
const event = {
name: eventName + ':' + this.type,
handler: (event, window) => this.eventHandler(event, window)
};
this.game.on(event.name, event.handler);
this.eventRegistrations.push(event);
});
}
public unregisterEvents() {
if (this.eventRegistrations) {
this.eventRegistrations.forEach((event) => {
this.game.removeListener(event.name, event.handler);
});
this.eventRegistrations = null;
}
}
private isTriggeredByEvent(event, context) {
const listener = this.when[event.name];
return listener && listener(event, context);
}
private checkAggregateWhen(events, window) {
for (const player of this.game.getPlayers()) {
const context = this.createContext(player, events);
//console.log(events.map(event => event.name), this.card.name, this.aggregateWhen(events, context), this.meetsRequirements(context));
if (
(this.card as CardWithTriggeredAbilities).getTriggeredAbilities().includes(this) &&
this.aggregateWhen(events, context) &&
this.meetsRequirements(context) === ''
) {
window.addToWindow(context);
}
}
}
}