To implement a card, follow these steps:
Cards are organized under the /server/game/cards
directory by grouping them by cycle/set number, pack number if applicable, and name.
/server/game/cards/01-Core/CloudTheMind.js //Core Set
/server/game/cards/02.1-ToA/GoblinSneak.js //Imperial Cycle, Pack 1, Tears of Amaterasu
Character, holding, event and attachment cards should be derived from the DrawCard
class.
Province cards should be derived from the ProvinceCard
class.
Stronghold cards should be derived from the StrongholdCard
class.
The card class should have its id
property set to the unique card identifier for that card. You can find these at https://api.fiveringsdb.com/cards
const DrawCard = require('../../drawcard.js');
class CloudTheMind extends DrawCard {
// Card definition
}
CloudTheMind.id = 'cloud-the-mind';
module.exports = CloudTheMind;
Persistent effects, actions, and triggered abilities should be defined in the setupCardAbilities
method. This method passes in an ability
parameter that gives you access to effect implementations and ability limits. See below for more documentation.
class CloudTheMind extends DrawCard {
setupCardAbilities(ability) {
// Declare persistent effects, reactions and interrupts here.
}
}
Keywords are automatically parsed from the card text. It isn't necessary to explicitly implement them unless they are provided by a conditional persistent effect.
Static attachment bonuses are automatically included in skill calculation. They don't need to be implemented unless they are dynamic (e.g. Born in War)
Many cards provide continuous bonuses to other cards you control or detrimental effects to opponents cards in certain situations. These can be defined using the persistentEffect
method. Cards that enter play while the persistent effect is in play will automatically have the effect applied, and cards that leave play will have the effect removed. If the card providing the effect becomes blank, the effect is automatically removed from all previously applied cards.
For a full list of properties that can be set when declaring an effect, look at /server/game/Effects/effect.js
. To see all the types of effect which you can use (and whether they apply to cards, rings or players), look at /server/game/effects.js
. Here are some common scenarios:
The effect declaration (for card and ring effects) takes a match
property. In most cases this will be a function that takes a Card
(or Ring
) object and should return true
if the effect should be applied to that card.
// Each honored Crane character you control gains Sincerity.
this.persistentEffect({
match: card => card.getType() === 'character' && card.isHonored && card.isFaction('crane'),
effect: ability.effects.addKeyword('sincerity')
});
In some cases, an effect should be applied to a specific card. While you could write a match
function to match only that card, you can provide the Card
(or Ring
) object as a shorthand.
// This character gets +3P while defending.
this.persistentEffect({
condition: () => this.isDefending(),
match: this,
effect: ability.effects.modifyPoliticalSkill(3)
});
Some effects have a 'when', 'while' or 'if' clause within their text. These cards can be implemented by passing a condition
function into the persistent effect declaration. The effect will only be applied when the function returns true
. If the function returns false
later on, the effect will be automatically unapplied from the cards it matched.
// During a conflict in which this character is participating, each other participating Lion
// character you control gets +1M.
this.persistentEffect({
condition: () => this.isParticipating(),
match: card => card.getType() === 'character' && card.isParticipating() &&
card.isFaction('lion') && card !== this,
effect: ability.effects.modifyMilitarySkill(1)
});
By default, an effect will only be applied to cards controlled by the current player. The targetController
property can be modified to specify which players' cards should be targeted.
To target only opponent cards, set targetController
to 'opponent'
:
// While attacking, each defending character gets -1M.
this.persistentEffect({
condition: () => this.isAttacking(),
match: card => card.isDefending(),
targetController: 'opponent',
effect: ability.effects.modifyMilitarySkill(-1)
});
To target all cards regardless of who controls them, set targetController
to 'any'
:
// While this character is participating in a conflict, characters cannot become dishonored.
this.persistentEffect({
condition: () => this.isParticipating(),
targetController: 'any',
match: card => card.getType() === 'character' && card.location === 'play area',
effect: ability.effects.cardCannot('becomeDishonored')
});
A few cards provide skill bonuses based on game state. For example, Beastmaster Matriarch gets a bonus to military skill depending on how many rings have been claimed. Where the bonus should be continously updated, pass a function as the effect paramater. In /server/game/effects.js
, you can see whether an effect is coded as static (expects to be passed an integer), dynamic (expects to be passed a function) or flexible (can take either).
// This character has +2[military] for each ring in each opponent's claimed ring pool.
this.persistentEffect({
match: this,
effect: ability.effects.modifyMilitarySkill(() => this.getTwiceOpponentsClaimedRings())
}
A whileAttached
method is provided to define persistent effects that are applied to the card an attachment is attached. These effects remain as long as the card is attached to its parent and the attachment has not been blanked.
// Attached character gains Pride.
this.whileAttached({
effect: ability.effects.addKeyword('pride')
});
If the effect has an additional requirement, an optional match
function can be passed in.
// If attached character is unicorn, they gain +1M.
this.whileAttached({
match: card => card.isFaction('unicorn'),
effect: ability.effects.modifyMilitarySkill(1)
});
As a shorthand, it is possible to pass an array into the effect
property to apply multiple effects that have the same conditions / matching functions.
// This character gets +1M and +1P while you are less honorable than an opponent..
this.persistentEffect({
condition: () => this.isLessHonorableThanOpponent(),
match: this,
effect: [
ability.effects.modifyMilitarySkill(1),
ability.effects.modifyPoliticalSkill(1)
]
});
By default, effects will only be applied to cards in the play area. Certain cards effects refer to cards in your hand, such as reducing their cost or providing ambush to matching cards. In these cases, set the targetLocation
property to 'hand'
.
// Each Direwolf card in your hand gains ambush (X). X is that card's printed cost.
this.persistentEffect({
// Explicitly target the effect to cards in hand.
targetLocation: 'hand',
match: card => card.hasTrait('Direwolf'),
effect: ability.effects.gainAmbush()
});
This also applies to provinces, holdings and strongholds, which the game considers to be 'in play' even though they aren't in the play area. Where an effect needs to be applied to these cards (or to characters who are in a province), set targetLocation
to 'province'
.
// This province gets +5 strength during [political] conflicts.
this.persistentEffect({
match: this,
targetLocation: 'province',
condition: () => this.game.isDuringConflict('political'),
effect: ability.effects.modifyProvinceStrength(5)
});
Certain cards provide bonuses or restrictions on the player itself instead of on any specific cards. These effects are marked as player
effects in /server/game/effects.js
. For player effects, targetController
indicates which players the effect should be applied to (with 'current'
acting as the default). Player effects should not have a match
property.
// While this character is participating in a conflict, opponents cannot play events.
this.persistentEffect({
condition: () => this.isParticipating(),
targetController: 'opponent',
effect: ability.effects.playerCannot(context => context.source.type === 'event')
});
Actions are abilities provided by the card text that players may trigger during action windows. They are declared using the action
method. See /server/game/cardaction.js
for full documentation. Here are some common scenarios:
When declaring an action, use the action
method and provide it with a title
property. The title is what will be displayed in the menu players see when clicking on the card.
class BorderRider extends DrawCard {
setupCardAbilities(ability) {
this.action({
title: 'Ready this character',
gameAction: ability.actions.ready()
});
}
}
When the game starts to resolve an ability, it creates a context object for that ability. Generally, the context ability has the following structure:
class AbilityContext {
constructor(properties) {
this.game = properties.game;
this.source = properties.source;
this.player = properties.player;
this.ability = properties.ability;
this.costs = {};
this.targets = {};
this.rings = {};
this.selects = {};
this.stage = Stages.Effect;
}
}
context.source
is the card with the ability being used, and context.player
is the player who is using the ability (almost always the controller of the context.source
). When implementing actions and other triggered abilities, context
should almost always be used (instead of this
) to reference cards or players. The only exception is that this.game
can be used as an alternative to context.game
.
Card abilities can only be triggered if they have the potential to modify game state (outside of paying costs). To ensure that the action's play restrictions are met, pass a condition
function that returns true
when the restrictions are met, and false
otherwise. If the condition returns false
, the action will not be executed and costs will not be paid.
// During a conflict, give this character +2/+2
this.action({
title: 'Give this character +2/+2',
condition: () => this.game.isDuringConflict(),
// ...
});
// While this character is participating in a conflict....
this.action({
title: 'Switch a character\'s M and P skill',
condition: context => context.source.isParticipating(),
// ...
});
Some actions have an additional cost, such as bowing the card. In these cases, specify the cost
parameter. The action will check if the cost can be paid. If it can't, the action will not execute. If it can, costs will be paid automatically and then the action will execute.
For a full list of costs, look at /server/game/costs.js
.
// During a conflict, bow this character. Choose another [crane] character - that character gets +0/+3 until the end of the conflict
this.action({
title: 'Give a character +0/+3',
// This card must be bowed as a cost for the action.
cost: ability.costs.bowSelf(),
// ...
});
If a card has multiple costs, an array of cost objects may be sent using the cost
property.
this.action({
title: 'Give all non-unique participating characters -2/-0',
// This card must be bowed AND sacrificed as a cost for the action.
cost: [
ability.costs.bowSelf(),
ability.costs.sacrificeSelf()
],
// ...
});
Cards that specify to 'choose' or otherwise target a specific card should be implemented by passing a target
property. The target property should include any limitations set by the ability, using the cardType
, location
, controller
and/or cardCondition
property. A game action can also be included by using the gameAction
property, which will restrict the card chosen to those for which that game action is legal (e.g. only cards in the play area can be dishonored, only cards with fate can have fate removed from them, etc.). If an array of game actions is specified, then the target only needs to meet the requirements of one of them.
Generally, it's a good idea to pass at least a cardType
property, as that will automatically change the prompt to make it easier for the player to understand what is going on. Most other properties that apply to Game.promptForSelect
are also valid here.
this.action({
title: 'Grant Covert to a character',
target: {
cardType: 'character',
location: 'play area'
},
// ...
});
this.action({
title: 'Sacrifice to discard an attachment'
target: {
cardType: 'attachment',
gameAction: ability.actions.discardFromPlay()
},
// ...
});
Some card abilities require multiple targets. These may be specified using the targets
property. Each sub key under targets
is the name that will be given to the chosen card, and the value is the prompt properties.
// Action: While this character is participating in a conflict, choose a ready non-participating character with printed
// cost 2 or lower controller by each player – move each chosen character to the conflict
this.action({
title: 'Move characters into conflict',
condition: context => context.source.isParticipating(),
targets: {
myChar: {
cardType: 'character',
controller: 'self',
cardCondition: card => !card.bowed && card.getCost() <= 2,
gameAction: ability.actions.moveToConflict()
},
oppChar: {
cardType: 'character',
controller: 'opponent',
cardCondition: card => !card.bowed && card.getCost() <= 2,
gameAction: ability.actions.moveToConflict()
}
}
});
Once all targets are chosen, they will be set using their specified name under the targets
property on the handler context object.
Rings are targeted in almost the same way as cards. For abilities which target rings, set the mode
property to 'ring'
, and use ringCondition
instead of cardCondition
. Most of the ring selection prompt properties are valid here also, see /server/game/gamesteps/selectringprompt.js
for more details. the chosen ring is stored in context.ring
(or context.rings[targetName]
where an ability has multiple targets).
// Action: Choose a ring and an opponent – that player cannot declare conflicts
// of that ring's element this phase. (Max 1 per phase.)
this.action({
title: 'Prevent an opponent contesting a ring',
condition: context => context.player.opponent,
target: {
mode: 'ring',
ringCondition: () => true
},
// ...
});
Some abilities require the player (or their opponent) to choose between multiple options. This is done in the same way as targets above, but by using the mode
property set to 'select'
. In addition, a choices
object should be included, which contains key:value pairs where the key is the option to display to the player, and the value is either a function which takes the context
object and returns a boolean indicating whether this option is legal, or a game action which will be evaluated on the basis of the specified target (or default as detailed below) to determine whether the choice is legal. The selected option is stored in context.select.choice
(or context.selects[targetName].choice
for an ability with multiple targets).
// Action: During a conflict at this province, select one – switch the contested ring with an unclaimed
// ring, or switch the conflict type.
this.action({
title: 'Switch the conflict type or ring',
condition: context => context.source.isConflictProvince(),
target: {
player: 'self',
mode: 'select',
choices: {
'Switch the contested ring': () => _.any(this.game.rings, ring => ring.isUnclaimed()),
'Switch the conflict type': () => true
}
},
// ...
});
// Action: If an opponent has declared 2 or more conflicts against you this phase, select one –
// take 1 fate or 1 honor from that opponent.
this.action({
title: 'Take 1 fate or 1 honor',
phase: 'conflict',
condition: context => this.game.getConflicts(context.player.opponent).filter(conflict => !conflict.passed).length > 1,
target: {
player: 'self',
mode: 'select',
choices: {
'Take 1 fate': ability.actions.takeFate(),
'Take 1 honor': ability.actions.takeHonor()
}
}
});
In general, the effects of an ability should be implemented using Game Actions.
Actions (and other triggered abilities) often use game actions. Available game actions can be found in /server/game/GameActions/GameActions.js
, along with any parameters and their defaults. Game actions as properties in the main ability section default to targetomg the card generating the ability (for cards), the opponent (for players) and the contested ring (for rings). Game actions included in target
(or in one of targets
) will default to the that target. You can change the target of a game action or the parameters by passing either an object with the properties you want, or a function which takes context
and returns those properties.
// Action: During a conflict, bow this attachment – move attached character to the conflict.
this.action({
title: 'Move this character into the conflict',
cost: ability.costs.bowSelf(),
gameAction: ability.actions.moveToConflict(context => ({ target: context.source.parent }))
});
// Reaction: After this character enters play – place 1 fate from an opponent's fate pool on it.
this.reaction({
title: 'Steal a fate',
// reaction condition code
gameAction: ability.actions.placeFate(context => ({ origin: context.player.opponent }))
});
Once costs have been paid and targets chosen (but before the ability resolves), the game automatically displays a message in the chat box which tells both players the ability, costs and targets of the effect. Game actions will automatically generate their own effect message, although this will only work for a single game action. If the effects of the ability involve two or more game actions, or the effect is a lasting effect or uses a handler, then an effect
property is required. The effect property will be passed the target (card(s) or ring) of the effect (or the source if there are no targets) as its first parameter (and so can be referenced using '{0}'
in the effect property string). If other references are required, this can be done using curly bracket references in the effect string('{1}', '{2', etc
) and supplying an effectArgs
property (which generally will be a function taking the context
object):
this.action({
// Action: Return this attachment to your hand and dishonor attached character.
title: 'Return court mask to hand',
effect: 'return {0} to hand, dishonoring {1}',
effectArgs: context => context.source.parent,
gameAction: [ability.actions.returnToHand(), ability.actions.dishonor(context => ({ target: context.source.parent }))]
});
this.action({
// Action: While this character is participating in a conflict, choose another participating character – until the end of the conflict, that character gets +2/+2 for each holding you control.
title: 'Give a character a bonus for each holding',
condition: context => context.source.isParticipating(),
target: {
cardType: 'character',
cardCondition: (card, context) => card.isParticipating() && card !== context.source,
gameAction: ability.actions.cardLastingEffect(context => ({
effect: ability.effects.modifyBothSkills(2 * context.player.getNumberOfHoldingsInPlay())
}))
},
effect: 'give {0} +{1}{2}/+{1}{3}',
effectArgs: context => [2 * context.player.getNumberOfHoldingsInPlay(), 'military', 'political']
});
Unlike persistent effects, lasting effects are typically applied during an action, reaction or interrupt and expire after a specified period of time. Lasting effect use the same properties as persistent effects, above. Lasting effects are applied using the cardLastingEffect
, ringLastingEffect
or playerLastingEffect
, depending on what they affect. They take a duration:
property which is one of untilEndOfConflict
(default), untilEndOfPhase
or untilEndOfRound
.
// Action: During a conflict, bow this character. Choose another [crane] character – that character
// gets +3 [political] until the end of the conflict.
this.action({
title: 'Give a character +0/+3',
condition: () => this.game.isDuringConflict(),
cost: ability.costs.bowSelf(),
target: {
cardType: 'character',
cardCondition: (card, context) => card !== context.source && card.isFaction('crane'),
gameAction: ability.actions.cardLastingEffect(() => ({
duration: 'untilEndOfConflict',
effect: ability.effects.modifyPoliticalSkill(3)
}))
},
effect: 'give {0} +3{1} skill',
effectArgs: () => 'political'
});
To apply an effect to last until the end of the current phase, use untilEndOfPhase
:
// Action: Reduce the cost of the next event you play this phase by 1.
this.action({
title: 'Reduce cost of next event by 1',
effect: 'reduce the cost of their next event by 1',
gameAction: ability.actions.playerLastingEffect({
duration: 'untilEndOfPhase',
effect: ability.effects.reduceNextPlayedCardCost(1, card => card.type === 'event')
})
});
To apply an effect to last until the end of the round, use untilEndOfRound
:
/// Action: Choose a holding you control – you may trigger each of that holding's triggered abilities an additional time this round (or specified period).
this.action({
title: 'Add an additional ability use to a holding',
target: {
cardType: 'holding',
location: 'province',
controller: 'self',
gameAction: ability.actions.cardLastingEffect({
duration: 'untilEndOfPhase',
targetLocation: 'province',
effect: ability.effects.increaseLimitOnAbilities()
})
},
effect: 'add an additional use to each of {0}\'s abilities'
});
Some actions are limited to a specific phase by their card text. You can pass an optional phase
property to the action to limit it to just that phase. Valid phases include 'dynasty'
, 'draw'
, 'conflict'
, 'fate'
. The default is 'any'
which allows the action to be triggered in any phase.
this.action({
title: 'Sacrifice to discard an attachment',
cost: ability.costs.sacrificeSelf(),
phase: 'conflict',
target: {
cardType: 'attachment',
gameAction: ability.actions.discardFromPlay()
}
});
Some actions have text limiting the number of times they may be used in a given period. You can pass an optional limit
property using one of the duration-specific ability limiters. See /server/game/abilitylimit.js
for more details.
this.action({
title: 'Remove 1 fate',
limit: ability.limit.perConflict(2),
// ...
});
Certain actions, such as that of Ancestral Guidance, can only be activated while the character is in the discard pile. Such actions should be defined by specifying the location
property with the location from which the ability may be activated. The player can then activate the ability by simply clicking the card. If there is a conflict (e.g. both the ability and playing the card normally can occur), then the player will be prompted.
this.action({
title: 'Play from discard pile',
location: 'conflict discard pile',
// ...
})
Triggered abilities include all card abilities that have Interrupt, Forced Interrupt, Reaction, Forced Reaction. Implementing a triggered ability is similar to actions above, but instead of calling this.action
, this.reaction
or this.interrupt
are used instead. Costs and targets are declared in the same way. For full documentation of properties, see /server/game/triggeredability.js
. Here are some common scenarios:
Each triggered ability has an associated triggering condition. This is done using the when
property. This should be an object whose sub-property is the name of the event, and whose value is a function which takes the event and the context object. When the function returns true
, the ability will be executed.
this.reaction({
// When this card enters play, honor it
when: {
onCharacterEntersPlay: (event, context) => event.card === context.source
},
gameAction: ability.actions.honor()
});
In rare cases, there may be multiple triggering conditions for the same ability. For example, Ikoma Prodigy gains an honor when fate is placed on her while playing her, or while she is in play. In these cases, just define an additional event on the when
object.
this.reaction({
title: 'Gain 1 honor',
when: {
onCharacterEntersPlay: (event, context) => event.card === context.source && context.source.fate > 0,
onMoveFate: (event, context) => event.recipient === context.source && event.fate > 0
},
gameAction: ability.actions.gainHonor()
});
Forced reactions and interrupts do not provide the player with a choice - unless cancelled, the effect will always resolve.
To declare a forced reaction, use the forcedReaction
method:
this.forcedReaction({
title: 'Can\'t be discarded or remove fate',
when: {
onPhaseStarted: (event, context) => event.phase === 'fate' && context.player.opponent &&
context.player.honor >= context.player.opponent.honor + 5
},
effect: 'stop him being discarded or losing fate in this phase',
gameAction: ability.actions.cardLastingEffect({
duration: 'untilEndOfPhase',
effect: [
ability.effects.cardCannot('removeFate'),
ability.effects.cardCannot('discardFromPlay')
]
})
});
To declare a forced interrupt, use the forcedInterrupt
method.
this.forcedInterrupt({
when: {
onCardLeavesPlay: (event, context) => event.card === context.source && context.source.hasSincerity()
},
/// ...
effect: '{1} draws a card due to {0}\'s Sincerity',
effectArgs: context => context.player,
gameAction: ability.actions.draw()
});
Some abilities allow the player to cancel an effect. These effects are always interrupts, and are usually templated as 'Interrupt: When [trigger] would....'. These are implemented
using the wouldInterrupt
method. The context object for triggered ability has a useful cancel
method which can be called in these cases
this.wouldInterrupt({
title: 'Cancel an event',
when: {
onInitiateAbilityEffects: event => event.card.type === 'event'
},
cost: ability.costs.dishonor(card => card.hasTrait('courtier')),
effect: 'cancel {1}',
effectArgs: context => context.event.card,
handler: context => context.cancel()
});
Certain abilities, such as that of Vengeful Oathkeeper can only be activated in non-play locations. Such reactions should be defined by specifying the location
property with the location from which the ability may be activated. The player can then activate the ability when prompted.
this.reaction({
when: {
afterConflict: (event, context) => context.conflict.loser === context.player && context.conflict.conflictType === 'military'
},
location: 'hand',
gameAction: ability.actions.putIntoPlay()
})
Actions, reactions, and interrupts can have limits on how many times they may be used within a certain period. These limits can be set by setting the limit
property on the ability. The ability
object has a limit helper with methods for the different periods.
To limit an ability per conflict, use ability.limit.perConflict(x)
.
To limit an ability per phase, use ability.limit.perPhase(x)
.
To limit an ability per round, use ability.limit.perRound(x)
.
In each case, x
should be the number of times the ability is allowed to be used.
Game messages should begin with the name of the player to ensure a uniform format and make it easy to see who triggered an ability.
- Bad: Kaiu Shuichi triggers to gain 1 fate for Player1
- Good: Player1 uses Kaiu Shuichi to gain 1 fate
No game messages should end in a period, exclaimation point or question mark.
- Bad: Player1 draws 2 cards.
- Good: Player1 draws 2 cards
All game messages should use present tense.
- Bad: Player1 has used Isawa Masahiro to discard Miya Mystic
- Bad: Player1 chose to discard Miya Mystic
- Good: Player1 uses Isawa Masahiro to discard Miya Mystic
- Good: Player1 chooses to discard Miya Mystic
Targeting prompts should ask the player to choose a card or a card of particular type to keep prompt titles relatively short, without specifying the final goal of card selection.
- Bad: Choose a character to return to hand
- Good: Choose a character
Exception: If a card requires the player to choose multiple cards (e.g. Rebuild), or if a card requires the player's opponent to choose a card (e.g. Endless Plains) you can add context about which one they should be selecting. Just keep it as short as reasonably possible.
As valid selections are already presented to the user via visual clues, targeting prompts should not repeat selection rules in excessive details. Specifying nothing more and nothing less than the eligible card type (if any) is the good middle ground (this is what most prompts will default to).
-
Bad: Choose a Bushi
-
Good: Choose a character
-
Bad: Choose a defending Crab character
-
Good: Choose a character
-
Bad: Choose a card from your discard pile
-
Good: Choose a card
-
Good: Choose an attachment or location