From 8a100881c671c544ea824b58bdefc9dc96607c2b Mon Sep 17 00:00:00 2001 From: AMMayberry1 Date: Tue, 30 Jul 2024 14:42:01 -0400 Subject: [PATCH] Fold DeckCard into BaseCard for now --- server/swu/game/Deck.ts | 13 +- server/swu/game/Interfaces.ts | 7 +- server/swu/game/PlayableLocation.ts | 5 +- server/swu/game/attack/Attack.ts | 15 +- server/swu/game/card/basecard.ts | 882 ++++++++++++++++- server/swu/game/card/deckcard.js | 889 ------------------ server/swu/game/costs/Costs.ts | 7 +- server/swu/game/gameActions/AttackAction.ts | 14 +- server/swu/game/gameActions/CardGameAction.ts | 3 +- server/swu/game/gameActions/DamageAction.ts | 3 +- server/swu/game/gameActions/HandlerAction.ts | 3 +- server/swu/game/gameActions/MoveCardAction.ts | 3 +- .../swu/game/gameActions/PutIntoPlayAction.ts | 5 +- .../game/gameActions/ReturnToDeckAction.ts | 5 +- .../game/gameActions/TriggerAttackAction.ts | 3 +- server/swu/game/player.js | 4 +- server/swu/game/utils/helpers.ts | 15 + test/swu/helpers/playerinteractionwrapper.js | 6 +- 18 files changed, 913 insertions(+), 969 deletions(-) delete mode 100644 server/swu/game/card/deckcard.js diff --git a/server/swu/game/Deck.ts b/server/swu/game/Deck.ts index 68115b005..10ebc1477 100644 --- a/server/swu/game/Deck.ts +++ b/server/swu/game/Deck.ts @@ -4,7 +4,6 @@ import { BaseLocationCard } from './card/baseLocationCard'; import { LeaderCard } from './card/leaderCard'; import BaseCard from './card/basecard'; import { cards } from './cards'; -import DeckCard from './card/deckcard'; import Player from './player'; export class Deck { @@ -12,9 +11,9 @@ export class Deck { prepare(player: Player) { const result = { - deckCards: [] as DeckCard[], + deckCards: [] as BaseCard[], outOfPlayCards: [], - outsideTheGameCards: [] as DeckCard[], + outsideTheGameCards: [] as BaseCard[], base: undefined as BaseLocationCard | undefined, leader: undefined as LeaderCard | undefined, allCards: [] as BaseCard[] @@ -23,9 +22,9 @@ export class Deck { //deck for (const { count, card } of this.data.deckCards ?? []) { for (let i = 0; i < count; i++) { - const CardConstructor = cards.get(card.id) ?? DeckCard; + const CardConstructor = cards.get(card.id) ?? BaseCard; // @ts-ignore - const deckCard: DeckCard = new CardConstructor(player, card); + const deckCard: BaseCard = new CardConstructor(player, card); deckCard.location = Locations.Deck; result.deckCards.push(deckCard); } @@ -55,9 +54,9 @@ export class Deck { } for (const cardData of this.data.outsideTheGameCards ?? []) { - const CardConstructor = cards.get(cardData.id) ?? DeckCard; + const CardConstructor = cards.get(cardData.id) ?? BaseCard; // @ts-ignore - const card: DeckCard = new CardConstructor(player, cardData); + const card: BaseCard = new CardConstructor(player, cardData); card.location = Locations.OutsideTheGame; result.outsideTheGameCards.push(card); } diff --git a/server/swu/game/Interfaces.ts b/server/swu/game/Interfaces.ts index 7d78213c5..d8c737b91 100644 --- a/server/swu/game/Interfaces.ts +++ b/server/swu/game/Interfaces.ts @@ -2,7 +2,6 @@ import type { AbilityContext } from './AbilityContext'; import type { TriggeredAbilityContext } from './TriggeredAbilityContext'; import type { GameAction } from './gameActions/GameAction'; import type BaseCard = require('./card/basecard'); -import type DeckCard = require('./card/deckcard'); import type CardAbility = require('./CardTextAbility'); import type { AttackProperties } from './gameActions/AttackAction'; import type { Players, TargetModes, CardTypes, Locations, EventNames, Phases } from './Constants'; @@ -39,8 +38,8 @@ interface TargetAbility extends BaseTarget { export interface InitiateAttack extends AttackProperties { opponentChoosesAttackTarget?: boolean; opponentChoosesAttacker?: boolean; - attackerCondition?: (card: DeckCard, context: TriggeredAbilityContext) => boolean; - targetCondition?: (card: DeckCard, context: TriggeredAbilityContext) => boolean; + attackerCondition?: (card: BaseCard, context: TriggeredAbilityContext) => boolean; + targetCondition?: (card: BaseCard, context: TriggeredAbilityContext) => boolean; } // interface TargetToken extends BaseTarget { @@ -107,7 +106,7 @@ type EffectArg = | number | string | Player - | DeckCard + | BaseCard | { id: string; label: string; name: string; facedown: boolean; type: CardTypes } | EffectArg[]; diff --git a/server/swu/game/PlayableLocation.ts b/server/swu/game/PlayableLocation.ts index de64ad593..6e95a3524 100644 --- a/server/swu/game/PlayableLocation.ts +++ b/server/swu/game/PlayableLocation.ts @@ -1,5 +1,4 @@ import type { Locations, PlayTypes } from './Constants'; -import type DeckCard from './card/deckcard'; import type Player from './player'; export class PlayableLocation { @@ -7,10 +6,10 @@ export class PlayableLocation { public playingType: PlayTypes, private player: Player, private location: Locations, - public cards = new Set() + public cards = new Set() ) {} - public contains(card: DeckCard) { + public contains(card: BaseCard) { if (this.cards.size > 0 && !this.cards.has(card)) { return false; } diff --git a/server/swu/game/attack/Attack.ts b/server/swu/game/attack/Attack.ts index dbebc4760..3b7be827b 100644 --- a/server/swu/game/attack/Attack.ts +++ b/server/swu/game/attack/Attack.ts @@ -1,7 +1,6 @@ import { GameObject } from '../GameObject'; import { EffectNames, EventNames, Locations, isArena } from '../Constants'; import { EventRegistrar } from '../EventRegistrar'; -import type DeckCard from '../card/deckcard'; import type Game from '../game'; import type Player from '../player'; import { AbilityContext } from '../AbilityContext'; @@ -23,20 +22,20 @@ type StatisticTotal = typeof InvalidStats | number; export class Attack extends GameObject { #bidFinished = false; #modifiers = new WeakMap(); - loser?: DeckCard[]; + loser?: BaseCard[]; losingPlayer?: Player; previousAttack?: Attack; - winner?: DeckCard[]; + winner?: BaseCard[]; winningPlayer?: Player; finalDifference?: number; private eventRegistrar?: EventRegistrar; constructor( public game: Game, - public attacker: DeckCard, - public target: DeckCard, + public attacker: BaseCard, + public target: BaseCard, public properties: { - targetCondition?: (card: DeckCard, context: AbilityContext) => boolean; + targetCondition?: (card: BaseCard, context: AbilityContext) => boolean; }, public attackingPlayer = attacker.controller ) { @@ -55,7 +54,7 @@ export class Attack extends GameObject { return this.loser?.[0].controller; } - get participants(): undefined | DeckCard[] { + get participants(): undefined | BaseCard[] { return [...[this.attacker], this.target]; } @@ -138,7 +137,7 @@ export class Attack extends GameObject { } } - #getTotalPower(involvedUnit: DeckCard, player: Player): StatisticTotal { + #getTotalPower(involvedUnit: BaseCard, player: Player): StatisticTotal { if (!isArena(involvedUnit.location)) { return InvalidStats; } diff --git a/server/swu/game/card/basecard.ts b/server/swu/game/card/basecard.ts index 276eca817..d72a66bee 100644 --- a/server/swu/game/card/basecard.ts +++ b/server/swu/game/card/basecard.ts @@ -19,7 +19,8 @@ import { isArena, Aspects, cardLocationMatches, - WildcardLocations + WildcardLocations, + StatType } from '../Constants.js'; import { ActionProps, @@ -31,10 +32,11 @@ import { // import { PlayAttachmentAction } from './PlayAttachmentAction.js'; // import { StatusToken } from './StatusToken'; import Player from '../player.js'; -import type DeckCard = require('./deckcard'); +import StatModifier = require('../StatModifier'); import type { CardEffect } from '../effects/types'; // import type { GainAllAbilities } from './Effects/Library/gainAllAbilities'; import { PlayUnitAction } from '../gameActions/PlayUnitAction.js'; +import { checkConvertToEnum } from '../utils/helpers'; // TODO: convert enums to unions type PrintedKeyword = @@ -69,6 +71,7 @@ const ValidKeywords = new Set([ ]); // TODO: maybe rename this class for clarity +// TODO: switch to using mixins for the different card types class BaseCard extends EffectSource { controller: Player; game: Game; @@ -81,8 +84,13 @@ class BaseCard extends EffectSource { facedown: boolean; resourced: boolean; + menu = [ + { command: 'exhaust', text: 'Exhaust/Ready' }, + { command: 'control', text: 'Give control' } + ]; + tokens: object = {}; - menu: { command: string; text: string }[] = []; + // menu: { command: string; text: string }[] = []; showPopup: boolean = false; popupMenuText: string = ''; @@ -94,8 +102,8 @@ class BaseCard extends EffectSource { isBase: boolean = false; isLeader: boolean = false; - upgrades = [] as DeckCard[]; - childCards = [] as DeckCard[]; + upgrades = [] as BaseCard[]; + childCards = [] as BaseCard[]; // statusTokens = [] as StatusToken[]; allowedAttachmentTraits = [] as string[]; printedKeywords: Array = []; @@ -114,18 +122,59 @@ class BaseCard extends EffectSource { this.printedTitle = cardData.title; this.printedSubtitle = cardData.subtitle; this.internalName = cardData.internalname; - this.printedType = this.#checkConvertToEnum([cardData.type], CardTypes)[0]; // TODO: does this work for leader consistently, since it has two types? + this.printedType = checkConvertToEnum([cardData.type], CardTypes)[0]; // TODO: does this work for leader consistently, since it has two types? this.traits = cardData.traits; // TODO: enum for these - this.aspects = this.#checkConvertToEnum(cardData.aspects, Aspects); + this.aspects = checkConvertToEnum(cardData.aspects, Aspects); this.printedKeywords = cardData.keywords; // TODO: enum for these this.setupCardAbilities(AbilityDsl); - this.parseKeywords(cardData.text ? cardData.text.replace(/<[^>]*>/g, '').toLowerCase() : ''); + // this.parseKeywords(cardData.text ? cardData.text.replace(/<[^>]*>/g, '').toLowerCase() : ''); // this.applyAttachmentBonus(); if (this.type === CardTypes.Unit) { - actions.push(AbilityDsl.attack()); + this.abilities.actions.push(AbilityDsl.attack()); + } + + + + + + // *************************** DECKCARD.JS CONSTRUCTOR ****************************** + + this.defaultController = owner; + this.parent = null; + this.printedHp = this.getPrintedStat(StatType.Hp); + this.printedPower = this.getPrintedStat(StatType.Power); + this.printedCost = parseInt(this.cardData.cost); + this.exhausted = false; + + switch (cardData.arena) { + case "space": + this.defaultArena = Locations.SpaceArena; + break; + case "ground": + this.defaultArena = Locations.GroundArena; + break; + default: + this.defaultArena = null; } + + // if (cardData.type === CardTypes.Character) { + // this.abilities.reactions.push(new CourtesyAbility(this.game, this)); + // this.abilities.reactions.push(new PrideAbility(this.game, this)); + // this.abilities.reactions.push(new SincerityAbility(this.game, this)); + // } + // if (cardData.type === CardTypes.Attachment) { + // this.abilities.reactions.push(new CourtesyAbility(this.game, this)); + // this.abilities.reactions.push(new SincerityAbility(this.game, this)); + // } + // if (cardData.type === CardTypes.Event && this.hasEphemeral()) { + // this.eventRegistrarForEphemeral = new EventRegistrar(this.game, this); + // this.eventRegistrarForEphemeral.register([{ [EventNames.OnCardPlayed]: 'handleEphemeral' }]); + // } + // if (this.isDynasty) { + // this.abilities.reactions.push(new RallyAbility(this.game, this)); + // } } get name(): string { @@ -137,21 +186,6 @@ class BaseCard extends EffectSource { this.printedTitle = name; } - // convert a set of strings to map to an enum type, throw if any of them is not a legal value - #checkConvertToEnum(values: string[], enumObj: T): Array { - let result: Array = []; - - for (const value of values) { - if (Object.values(enumObj).indexOf(value.toLowerCase()) >= 0) { - result.push(value as T[keyof T]); - } else { - throw new Error(`Invalid value for enum: ${value}`); - } - } - - return result; - } - #mostRecentEffect(predicate: (effect: CardEffect) => boolean): CardEffect | undefined { const effects = this.getRawEffects().filter(predicate); return effects[effects.length - 1]; @@ -757,6 +791,7 @@ class BaseCard extends EffectSource { return {}; } + // TODO: would something like this be helpful for swu? parseKeywords(text: string) { const potentialKeywords = []; for (const line of text.split('\n')) { @@ -1026,7 +1061,7 @@ class BaseCard extends EffectSource { /** * This removes an attachment from this card's attachment Array. It doesn't open any windows for * game effects to respond to. - * @param {DeckCard} attachment + * @param {BaseCard} attachment */ removeAttachment(attachment) { this.upgrades = this.upgrades.filter((card) => card.uuid !== attachment.uuid); @@ -1146,6 +1181,803 @@ class BaseCard extends EffectSource { // return Object.assign(state, selectionState); // } + + // --------------------------- TODO: type annotations for all of the below -------------------------- + + // this will be helpful if we ever get a card where a stat that is "X, where X is ..." + getPrintedStat(type: StatType) { + if (type === StatType.Power) { + return this.cardData.damage === null || this.cardData.damage === undefined + ? NaN + : isNaN(parseInt(this.cardData.power)) + ? 0 + : parseInt(this.cardData.power); + } else if (type === StatType.Hp) { + return this.cardData.hp === null || this.cardData.hp === undefined + ? NaN + : isNaN(parseInt(this.cardData.hp)) + ? 0 + : parseInt(this.cardData.hp); + } + } + + addDamage(amount: number) { + if (isNaN(this.hp)) { + + } + } + + // TODO: type annotations for all of the hp stuff + get hp(): number | null { + return this.getHp(); + } + + getHp(floor = true, excludeModifiers = []): number | null { + if (this.printedHp === null) { + return null; + } + + let modifiers = this.getHpModifiers(excludeModifiers); + let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + if (isNaN(skill)) { + return 0; + } + return floor ? Math.max(0, skill) : skill; + } + + get baseHp(): number { + return this.getBaseHp(); + } + + getBaseHp(): number { + let skill = this.getBaseStatModifiers().baseHp; + if (isNaN(skill)) { + return 0; + } + return Math.max(0, skill); + } + + getHpModifiers(exclusions) { + let baseStatModifiers = this.getBaseStatModifiers(); + if (isNaN(baseStatModifiers.baseHp)) { + return baseStatModifiers.baseHpModifiers; + } + + if (!exclusions) { + exclusions = []; + } + + let rawEffects; + if (typeof exclusions === 'function') { + rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); + } else { + rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); + } + + let modifiers = baseStatModifiers.baseHpModifiers; + + // hp modifiers + // TODO: remove status tokens completely, upgrades completely cover that category + let modifierEffects = rawEffects.filter( + (effect) => + effect.type === EffectNames.UpgradeHpModifier || + effect.type === EffectNames.ModifyHp + ); + modifierEffects.forEach((modifierEffect) => { + const value = modifierEffect.getValue(this); + modifiers.push(StatModifier.fromEffect(value, modifierEffect)); + }); + + return modifiers; + } + + get power() { + return this.getPower(); + } + + getPower(floor = true, excludeModifiers = []) { + let modifiers = this.getPowerModifiers(excludeModifiers); + let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + if (isNaN(skill)) { + return 0; + } + return floor ? Math.max(0, skill) : skill; + } + + get basePower() { + return this.getBasePower(); + } + + getBasePower() { + let skill = this.getBaseStatModifiers().basePower; + if (isNaN(skill)) { + return 0; + } + return Math.max(0, skill); + } + + getPowerModifiers(exclusions) { + let baseStatModifiers = this.getBaseStatModifiers(); + if (isNaN(baseStatModifiers.basePower)) { + return baseStatModifiers.basePowerModifiers; + } + + if (!exclusions) { + exclusions = []; + } + + let rawEffects; + if (typeof exclusions === 'function') { + rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); + } else { + rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); + } + + // set effects (i.e., "set power to X") + let setEffects = rawEffects.filter( + (effect) => effect.type === EffectNames.SetPower + ); + if (setEffects.length > 0) { + let latestSetEffect = _.last(setEffects); + let setAmount = latestSetEffect.getValue(this); + return [ + StatModifier.fromEffect( + setAmount, + latestSetEffect, + true, + `Set by ${StatModifier.getEffectName(latestSetEffect)}` + ) + ]; + } + + let modifiers = baseStatModifiers.basePowerModifiers; + + // power modifiers + // TODO: remove status tokens completely, upgrades completely cover that category + // TODO: does this work for resolving effects like Raid that depend on whether we're the attacker or not? + let modifierEffects = rawEffects.filter( + (effect) => + effect.type === EffectNames.UpgradePowerModifier || + effect.type === EffectNames.ModifyPower || + effect.type === EffectNames.ModifyStats + ); + modifierEffects.forEach((modifierEffect) => { + const value = modifierEffect.getValue(this); + modifiers.push(StatModifier.fromEffect(value, modifierEffect)); + }); + + return modifiers; + } + + /** + * Direct the stat query to the correct sub function. + * @param {string} type - The type of the stat; power or hp + * @return {number} The chosen stat value + */ + getStat(type) { + switch (type) { + case StatType.Power: + return this.getPower(); + case StatType.Hp: + return this.getHp(); + } + } + + // TODO: rename this to something clearer + /** + * Apply any modifiers that explicitly say they change the base skill value + */ + getBaseStatModifiers() { + const baseModifierEffects = [ + EffectNames.CopyCharacter, + EffectNames.CalculatePrintedPower, + EffectNames.SetBasePower, + ]; + + let baseEffects = this.getRawEffects().filter((effect) => baseModifierEffects.includes(effect.type)); + let basePowerModifiers = [StatModifier.fromCard(this.printedPower, this, 'Printed power', false)]; + let baseHpModifiers = [StatModifier.fromCard(this.printedHp, this, 'Printed hp', false)]; + let basePower = this.printedPower; + let baseHp = this.printedHp; + + baseEffects.forEach((effect) => { + switch (effect.type) { + // this case is for cards that don't have a default printed power but it is instead calculated + case EffectNames.CalculatePrintedPower: { + let damageFunction = effect.getValue(this); + let calculatedDamageValue = damageFunction(this); + basePower = calculatedDamageValue; + basePowerModifiers = basePowerModifiers.filter( + (mod) => !mod.name.startsWith('Printed power') + ); + basePowerModifiers.push( + StatModifier.fromEffect( + basePower, + effect, + false, + `Printed power due to ${StatModifier.getEffectName(effect)}` + ) + ); + break; + } + case EffectNames.CopyCharacter: { + let copiedCard = effect.getValue(this); + basePower = copiedCard.getPrintedStat(StatType.Power); + baseHp = copiedCard.getPrintedStat(StatType.Hp); + // replace existing base or copied modifier + basePowerModifiers = basePowerModifiers.filter( + (mod) => !mod.name.startsWith('Printed stat') + ); + baseHpModifiers = baseHpModifiers.filter( + (mod) => !mod.name.startsWith('Printed stat') + ); + basePowerModifiers.push( + StatModifier.fromEffect( + basePower, + effect, + false, + `Printed skill from ${copiedCard.name} due to ${StatModifier.getEffectName(effect)}` + ) + ); + baseHpModifiers.push( + StatModifier.fromEffect( + baseHp, + effect, + false, + `Printed skill from ${copiedCard.name} due to ${StatModifier.getEffectName(effect)}` + ) + ); + break; + } + case EffectNames.SetBasePower: + basePower = effect.getValue(this); + basePowerModifiers.push( + StatModifier.fromEffect( + basePower, + effect, + true, + `Base power set by ${StatModifier.getEffectName(effect)}` + ) + ); + break; + } + }); + + let overridingPowerModifiers = basePowerModifiers.filter((mod) => mod.overrides); + if (overridingPowerModifiers.length > 0) { + let lastModifier = _.last(overridingPowerModifiers); + basePowerModifiers = [lastModifier]; + basePower = lastModifier.amount; + } + let overridingHpModifiers = baseHpModifiers.filter((mod) => mod.overrides); + if (overridingHpModifiers.length > 0) { + let lastModifier = _.last(overridingHpModifiers); + baseHpModifiers = [lastModifier]; + baseHp = lastModifier.amount; + } + + return { + basePowerModifiers: basePowerModifiers, + basePower: basePower, + baseHpModifiers: baseHpModifiers, + baseHp: baseHp + }; + } + + + + + + + + + + + + + + + + + + // ******************************************************************************************************* + // ************************************** DECKCARD.JS **************************************************** + // ******************************************************************************************************* + + getCost() { + let copyEffect = this.mostRecentEffect(EffectNames.CopyCharacter); + return copyEffect ? copyEffect.printedCost : this.printedCost; + } + + costLessThan(num) { + let cost = this.printedCost; + return num && (cost || cost === 0) && cost < num; + } + + anotherUniqueInPlay(player) { + return ( + this.isUnique() && + this.game.allCards.any( + (card) => + card.isInPlay() && + card.printedName === this.printedTitle && // TODO: also check subtitle + card !== this && + (card.owner === player || card.controller === player || card.owner === this.owner) + ) + ); + } + + anotherUniqueInPlayControlledBy(player) { + return ( + this.isUnique() && + this.game.allCards.any( + (card) => + card.isInPlay() && + card.printedName === this.printedTitle && + card !== this && + card.controller === player + ) + ); + } + + createSnapshot() { + let clone = new BaseCard(this.owner, this.cardData); + + // clone.upgrades = _(this.upgrades.map((attachment) => attachment.createSnapshot())); + clone.childCards = this.childCards.map((card) => card.createSnapshot()); + clone.effects = _.clone(this.effects); + clone.controller = this.controller; + clone.exhausted = this.exhausted; + // clone.statusTokens = [...this.statusTokens]; + clone.location = this.location; + clone.parent = this.parent; + clone.aspects = _.clone(this.aspects); + // clone.fate = this.fate; + // clone.inConflict = this.inConflict; + clone.traits = Array.from(this.getTraits()); + clone.uuid = this.uuid; + return clone; + } + + // hasDash(type = '') { + // if (type === 'glory' || this.printedType !== CardTypes.Character) { + // return false; + // } + + // let baseSkillModifiers = this.getBaseSkillModifiers(); + + // if (type === 'military') { + // return isNaN(baseSkillModifiers.baseMilitarySkill); + // } else if (type === 'political') { + // return isNaN(baseSkillModifiers.basePoliticalSkill); + // } + + // return isNaN(baseSkillModifiers.baseMilitarySkill) || isNaN(baseSkillModifiers.basePoliticalSkill); + // } + + // getContributionToConflict(type) { + // let skillFunction = this.mostRecentEffect(EffectNames.ChangeContributionFunction); + // if (skillFunction) { + // return skillFunction(this); + // } + // return this.getSkill(type); + // } + + getStatusTokenSkill() { + let modifiers = this.getStatusTokenModifiers(); + let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + if (isNaN(skill)) { + return 0; + } + return skill; + } + + // getStatusTokenModifiers() { + // let modifiers = []; + // let modifierEffects = this.getRawEffects().filter((effect) => effect.type === EffectNames.ModifyBothSkills); + + // // skill modifiers + // modifierEffects.forEach((modifierEffect) => { + // const value = modifierEffect.getValue(this); + // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); + // }); + // modifiers = modifiers.filter((modifier) => modifier.type === 'token'); + + // // adjust honor status effects + // this.adjustHonorStatusModifiers(modifiers); + // return modifiers; + // } + + // getMilitaryModifiers(exclusions) { + // let baseSkillModifiers = this.getBaseSkillModifiers(); + // if (isNaN(baseSkillModifiers.baseMilitarySkill)) { + // return baseSkillModifiers.baseMilitaryModifiers; + // } + + // if (!exclusions) { + // exclusions = []; + // } + + // let rawEffects; + // if (typeof exclusions === 'function') { + // rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); + // } else { + // rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); + // } + + // // set effects + // let setEffects = rawEffects.filter( + // (effect) => effect.type === EffectNames.SetMilitarySkill || effect.type === EffectNames.SetDash + // ); + // if (setEffects.length > 0) { + // let latestSetEffect = _.last(setEffects); + // let setAmount = latestSetEffect.type === EffectNames.SetDash ? undefined : latestSetEffect.getValue(this); + // return [ + // StatModifier.fromEffect( + // setAmount, + // latestSetEffect, + // true, + // `Set by ${StatModifier.getEffectName(latestSetEffect)}` + // ) + // ]; + // } + + // let modifiers = baseSkillModifiers.baseMilitaryModifiers; + + // // skill modifiers + // let modifierEffects = rawEffects.filter( + // (effect) => + // effect.type === EffectNames.AttachmentMilitarySkillModifier || + // effect.type === EffectNames.ModifyMilitarySkill || + // effect.type === EffectNames.ModifyBothSkills + // ); + // modifierEffects.forEach((modifierEffect) => { + // const value = modifierEffect.getValue(this); + // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); + // }); + + // // adjust honor status effects + // this.adjustHonorStatusModifiers(modifiers); + + // // multipliers + // let multiplierEffects = rawEffects.filter( + // (effect) => effect.type === EffectNames.ModifyMilitarySkillMultiplier + // ); + // multiplierEffects.forEach((multiplierEffect) => { + // let multiplier = multiplierEffect.getValue(this); + // let currentTotal = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + // let amount = (multiplier - 1) * currentTotal; + // modifiers.push(StatModifier.fromEffect(amount, multiplierEffect)); + // }); + + // return modifiers; + // } + + // getPoliticalModifiers(exclusions) { + // let baseSkillModifiers = this.getBaseSkillModifiers(); + // if (isNaN(baseSkillModifiers.basePoliticalSkill)) { + // return baseSkillModifiers.basePoliticalModifiers; + // } + + // if (!exclusions) { + // exclusions = []; + // } + + // let rawEffects; + // if (typeof exclusions === 'function') { + // rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); + // } else { + // rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); + // } + + // // set effects + // let setEffects = rawEffects.filter((effect) => effect.type === EffectNames.SetPoliticalSkill); + // if (setEffects.length > 0) { + // let latestSetEffect = _.last(setEffects); + // let setAmount = latestSetEffect.getValue(this); + // return [ + // StatModifier.fromEffect( + // setAmount, + // latestSetEffect, + // true, + // `Set by ${StatModifier.getEffectName(latestSetEffect)}` + // ) + // ]; + // } + + // let modifiers = baseSkillModifiers.basePoliticalModifiers; + + // // skill modifiers + // let modifierEffects = rawEffects.filter( + // (effect) => + // effect.type === EffectNames.AttachmentPoliticalSkillModifier || + // effect.type === EffectNames.ModifyPoliticalSkill || + // effect.type === EffectNames.ModifyBothSkills + // ); + // modifierEffects.forEach((modifierEffect) => { + // const value = modifierEffect.getValue(this); + // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); + // }); + + // // adjust honor status effects + // this.adjustHonorStatusModifiers(modifiers); + + // // multipliers + // let multiplierEffects = rawEffects.filter( + // (effect) => effect.type === EffectNames.ModifyPoliticalSkillMultiplier + // ); + // multiplierEffects.forEach((multiplierEffect) => { + // let multiplier = multiplierEffect.getValue(this); + // let currentTotal = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + // let amount = (multiplier - 1) * currentTotal; + // modifiers.push(StatModifier.fromEffect(amount, multiplierEffect)); + // }); + + // return modifiers; + // } + + get showStats() { + return isArena(this.location) && this.type === CardTypes.Unit; + } + + get militarySkillSummary() { + if (!this.showStats) { + return {}; + } + let modifiers = this.getPowerModifiers().map((modifier) => Object.assign({}, modifier)); + let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + return { + stat: isNaN(skill) ? '-' : Math.max(skill, 0).toString(), + modifiers: modifiers + }; + } + + get politicalSkillSummary() { + if (!this.showStats) { + return {}; + } + let modifiers = this.getPoliticalModifiers().map((modifier) => Object.assign({}, modifier)); + modifiers.forEach((modifier) => (modifier = Object.assign({}, modifier))); + let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); + return { + stat: isNaN(skill) ? '-' : Math.max(skill, 0).toString(), + modifiers: modifiers + }; + } + + exhaust() { + this.exhausted = true; + } + + ready() { + this.exhausted = false; + } + + canPlay(context, type) { + return ( + this.checkRestrictions(type, context) && + context.player.checkRestrictions(type, context) && + this.checkRestrictions('play', context) && + context.player.checkRestrictions('play', context) && + (!this.hasPrintedKeyword('peaceful') || !this.game.currentConflict) + ); + } + + getActions(location = this.location) { + // if card is already in play or is an event, return the default actions + if (location === Locations.SpaceArena || location === Locations.GroundArena || this.type === CardTypes.Event) { + return super.getActions(); + } + + // TODO: add base / leader actions if this doesn't already cover them + + // otherwise (i.e. card is in hand), return play card action(s) + other available card actions + return this.getPlayActions().concat(super.getActions()); + } + + /** + * Deals with the engine effects of leaving play, making sure all statuses are removed. Anything which changes + * the state of the card should be here. This is also called in some strange corner cases e.g. for attachments + * which aren't actually in play themselves when their parent (which is in play) leaves play. + */ + leavesPlay() { + // If this is an attachment and is attached to another card, we need to remove all links between them + if (this.parent && this.parent.attachments) { + this.parent.removeAttachment(this); + this.parent = null; + } + + // Remove any cards underneath from the game + const cardsUnderneath = this.controller.getSourceListForPile(this.uuid).map((a) => a); + if (cardsUnderneath.length > 0) { + cardsUnderneath.forEach((card) => { + this.controller.moveCard(card, Locations.RemovedFromGame); + }); + this.game.addMessage( + '{0} {1} removed from the game due to {2} leaving play', + cardsUnderneath, + cardsUnderneath.length === 1 ? 'is' : 'are', + this + ); + } + + if (this.isParticipating()) { + this.game.currentConflict.removeFromConflict(this); + } + + this.exhausted = false; + this.new = false; + super.leavesPlay(); + } + + // canDeclareAsAttacker(conflictType, ring, province, incomingAttackers = undefined) { + // // eslint-disable-line no-unused-vars + // if (!province) { + // let provinces = + // this.game.currentConflict && this.game.currentConflict.defendingPlayer + // ? this.game.currentConflict.defendingPlayer.getProvinces() + // : null; + // if (provinces) { + // return provinces.some( + // (a) => + // a.canDeclare(conflictType, ring) && + // this.canDeclareAsAttacker(conflictType, ring, a, incomingAttackers) + // ); + // } + // } + + // let attackers = this.game.isDuringConflict() ? this.game.currentConflict.attackers : []; + // if (incomingAttackers) { + // attackers = incomingAttackers; + // } + // if (!attackers.includes(this)) { + // attackers = attackers.concat(this); + // } + + // // Check if I add an element that I can\'t attack with + // const elementsAdded = this.upgrades.reduce( + // (array, attachment) => array.concat(attachment.getEffects(EffectNames.AddElementAsAttacker)), + // this.getEffects(EffectNames.AddElementAsAttacker) + // ); + + // if ( + // elementsAdded.some((element) => + // this.game.rings[element] + // .getEffects(EffectNames.CannotDeclareRing) + // .some((match) => match(this.controller)) + // ) + // ) { + // return false; + // } + + // if ( + // conflictType === ConflictTypes.Military && + // attackers.reduce((total, card) => total + card.sumEffects(EffectNames.CardCostToAttackMilitary), 0) > + // this.controller.hand.size() + // ) { + // return false; + // } + + // let fateCostToAttackProvince = province ? province.getFateCostToAttack() : 0; + // if ( + // attackers.reduce((total, card) => total + card.sumEffects(EffectNames.FateCostToAttack), 0) + + // fateCostToAttackProvince > + // this.controller.fate + // ) { + // return false; + // } + // if (this.anyEffect(EffectNames.CanOnlyBeDeclaredAsAttackerWithElement)) { + // for (let element of this.getEffects(EffectNames.CanOnlyBeDeclaredAsAttackerWithElement)) { + // if (!ring.hasElement(element) && !elementsAdded.includes(element)) { + // return false; + // } + // } + // } + + // if (this.controller.anyEffect(EffectNames.LimitLegalAttackers)) { + // const checks = this.controller.getEffects(EffectNames.LimitLegalAttackers); + // let valid = true; + // checks.forEach((check) => { + // if (typeof check === 'function') { + // valid = valid && check(this); + // } + // }); + // if (!valid) { + // return false; + // } + // } + + // return ( + // this.checkRestrictions('declareAsAttacker', this.game.getFrameworkContext()) && + // this.canParticipateAsAttacker(conflictType) && + // this.location === Locations.PlayArea && + // !this.exhausted + // ); + // } + + // canDeclareAsDefender(conflictType = this.game.currentConflict.conflictType) { + // return ( + // this.checkRestrictions('declareAsDefender', this.game.getFrameworkContext()) && + // this.canParticipateAsDefender(conflictType) && + // this.location === Locations.PlayArea && + // !this.exhausted && + // !this.covert + // ); + // } + + // canParticipateAsAttacker(conflictType = this.game.currentConflict.conflictType) { + // let effects = this.getEffects(EffectNames.CannotParticipateAsAttacker); + // return !effects.some((value) => value === 'both' || value === conflictType) && !this.hasDash(conflictType); + // } + + // canParticipateAsDefender(conflictType = this.game.currentConflict.conflictType) { + // let effects = this.getEffects(EffectNames.CannotParticipateAsDefender); + // let hasDash = conflictType ? this.hasDash(conflictType) : false; + + // return !effects.some((value) => value === 'both' || value === conflictType) && !hasDash; + // } + + // bowsOnReturnHome() { + // return !this.anyEffect(EffectNames.DoesNotBow); + // } + + setDefaultController(player) { + this.defaultController = player; + } + + // getModifiedController() { + // if ( + // this.location === Locations.PlayArea || + // (this.type === CardTypes.Holding && this.location.includes('province')) + // ) { + // return this.mostRecentEffect(EffectNames.TakeControl) || this.defaultController; + // } + // return this.owner; + // } + + // canDisguise(card, context, intoConflictOnly) { + // return ( + // this.disguisedKeywordTraits.some((trait) => card.hasTrait(trait)) && + // card.allowGameAction('discardFromPlay', context) && + // !card.isUnique() && + // (!intoConflictOnly || card.isParticipating()) + // ); + // } + + // play() { + // //empty function so playcardaction doesn't crash the game + // } + + // getSummary(activePlayer, hideWhenFaceup) { + // let baseSummary = super.getSummary(activePlayer, hideWhenFaceup); + + // return _.extend(baseSummary, { + // attached: !!this.parent, + // attachments: this.upgrades.map((attachment) => { + // return attachment.getSummary(activePlayer, hideWhenFaceup); + // }), + // childCards: this.childCards.map((card) => { + // return card.getSummary(activePlayer, hideWhenFaceup); + // }), + // inConflict: this.inConflict, + // isConflict: this.isConflict, + // isDynasty: this.isDynasty, + // isPlayableByMe: this.isConflict && this.controller.isCardInPlayableLocation(this, PlayTypes.PlayFromHand), + // isPlayableByOpponent: + // this.isConflict && + // this.controller.opponent && + // this.controller.opponent.isCardInPlayableLocation(this, PlayTypes.PlayFromHand), + // bowed: this.exhausted, + // fate: this.fate, + // new: this.new, + // covert: this.covert, + // showStats: this.showStats, + // militarySkillSummary: this.militarySkillSummary, + // politicalSkillSummary: this.politicalSkillSummary, + // glorySummary: this.glorySummary, + // controller: this.controller.getShortSummary() + // }); + // } } export = BaseCard; \ No newline at end of file diff --git a/server/swu/game/card/deckcard.js b/server/swu/game/card/deckcard.js deleted file mode 100644 index 5935cfbd3..000000000 --- a/server/swu/game/card/deckcard.js +++ /dev/null @@ -1,889 +0,0 @@ -const _ = require('underscore'); - -const BaseCard = require('./basecard'); -const StatModifier = require('../StatModifier'); - -const { Locations, EffectNames, CardTypes, PlayTypes, EventNames, isArena, StatType } = require('../Constants.js'); -const { GameModes } = require('../../GameModes.js'); -// const { EventRegistrar } = require('./EventRegistrar'); - -// TODO: rename back to DrawCard -class DeckCard extends BaseCard { - // fromOutOfPlaySource ?: Array < DeckCard >; - menu = [ - { command: 'exhaust', text: 'Exhaust/Ready' }, - { command: 'control', text: 'Give control' } - ]; - - constructor(owner, cardData) { - super(owner, cardData); - - this.defaultController = owner; - this.parent = null; - - this.printedPower = this.getPrintedSkill('power'); - this.printedHp = this.getPrintedSkill('health'); - this.printedCost = parseInt(this.cardData.cost); - - if (!_.isNumber(this.printedCost) || isNaN(this.printedCost)) { - if (this.type === CardTypes.Event) { - this.printedCost = 0; - } else { - this.printedCost = null; - } - } - this.printedGlory = parseInt(cardData.glory); - this.printedStrengthBonus = parseInt(cardData.strength_bonus); - this.fate = 0; - this.exhausted = false; - this.covert = false; - this.isConflict = cardData.side === 'conflict'; - this.isDynasty = cardData.side === 'dynasty'; - this.allowDuplicatesOfAttachment = !!cardData.attachment_allow_duplicates; - this.defaultArena = cardData.arena + " arena"; // TODO: figure out how this is actually stored and parse it to enum - - // if (cardData.type === CardTypes.Character) { - // this.abilities.reactions.push(new CourtesyAbility(this.game, this)); - // this.abilities.reactions.push(new PrideAbility(this.game, this)); - // this.abilities.reactions.push(new SincerityAbility(this.game, this)); - // } - // if (cardData.type === CardTypes.Attachment) { - // this.abilities.reactions.push(new CourtesyAbility(this.game, this)); - // this.abilities.reactions.push(new SincerityAbility(this.game, this)); - // } - // if (cardData.type === CardTypes.Event && this.hasEphemeral()) { - // this.eventRegistrarForEphemeral = new EventRegistrar(this.game, this); - // this.eventRegistrarForEphemeral.register([{ [EventNames.OnCardPlayed]: 'handleEphemeral' }]); - // } - // if (this.isDynasty) { - // this.abilities.reactions.push(new RallyAbility(this.game, this)); - // } - } - - handleEphemeral(event) { - if (event.card === this) { - if (this.location !== Locations.RemovedFromGame) { - this.owner.moveCard(this, Locations.RemovedFromGame); - } - this.fromOutOfPlaySource = undefined; - } - } - - getPrintedStat(type) { - if (type === StatType.Power) { - return this.cardData.damage === null || this.cardData.damage === undefined - ? NaN - : isNaN(parseInt(this.cardData.power)) - ? 0 - : parseInt(this.cardData.power); - } else if (type === StatType.Hp) { - return this.cardData.political === null || this.cardData.political === undefined - ? NaN - : isNaN(parseInt(this.cardData.political)) - ? 0 - : parseInt(this.cardData.political); - } - } - - isLimited() { - return this.hasKeyword('limited') || this.hasPrintedKeyword('limited'); - } - - isRestricted() { - return this.hasKeyword('restricted'); - } - - isAncestral() { - return this.hasKeyword('ancestral'); - } - - isCovert() { - return this.hasKeyword('covert'); - } - - hasSincerity() { - return this.hasKeyword('sincerity'); - } - - hasPride() { - return this.hasKeyword('pride'); - } - - hasCourtesy() { - return this.hasKeyword('courtesy'); - } - - hasEphemeral() { - return this.hasPrintedKeyword('ephemeral'); - } - - hasPeaceful() { - return this.hasPrintedKeyword('peaceful'); - } - - hasNoDuels() { - return this.hasKeyword('no duels'); - } - - isDire() { - return this.getFate() === 0; - } - - hasRally() { - //Facedown cards are out of play and their keywords don't update until after the reveal reaction window is done, so we need to check for the printed keyword - return this.hasKeyword('rally') || (!this.isBlank() && this.hasPrintedKeyword('rally')); - } - - getCost() { - let copyEffect = this.mostRecentEffect(EffectNames.CopyCharacter); - return copyEffect ? copyEffect.printedCost : this.printedCost; - } - - costLessThan(num) { - let cost = this.printedCost; - return num && (cost || cost === 0) && cost < num; - } - - anotherUniqueInPlay(player) { - return ( - this.isUnique() && - this.game.allCards.any( - (card) => - card.isInPlay() && - card.printedName === this.printedTitle && - card !== this && - (card.owner === player || card.controller === player || card.owner === this.owner) - ) - ); - } - - anotherUniqueInPlayControlledBy(player) { - return ( - this.isUnique() && - this.game.allCards.any( - (card) => - card.isInPlay() && - card.printedName === this.printedTitle && - card !== this && - card.controller === player - ) - ); - } - - createSnapshot() { - let clone = new DeckCard(this.owner, this.cardData); - - // clone.upgrades = _(this.upgrades.map((attachment) => attachment.createSnapshot())); - clone.childCards = this.childCards.map((card) => card.createSnapshot()); - clone.effects = _.clone(this.effects); - clone.controller = this.controller; - clone.exhausted = this.exhausted; - // clone.statusTokens = [...this.statusTokens]; - clone.location = this.location; - clone.parent = this.parent; - clone.aspects = _.clone(this.aspects); - // clone.fate = this.fate; - // clone.inConflict = this.inConflict; - clone.traits = Array.from(this.getTraits()); - clone.uuid = this.uuid; - return clone; - } - - // hasDash(type = '') { - // if (type === 'glory' || this.printedType !== CardTypes.Character) { - // return false; - // } - - // let baseSkillModifiers = this.getBaseSkillModifiers(); - - // if (type === 'military') { - // return isNaN(baseSkillModifiers.baseMilitarySkill); - // } else if (type === 'political') { - // return isNaN(baseSkillModifiers.basePoliticalSkill); - // } - - // return isNaN(baseSkillModifiers.baseMilitarySkill) || isNaN(baseSkillModifiers.basePoliticalSkill); - // } - - // getContributionToConflict(type) { - // let skillFunction = this.mostRecentEffect(EffectNames.ChangeContributionFunction); - // if (skillFunction) { - // return skillFunction(this); - // } - // return this.getSkill(type); - // } - - /** - * Direct the stat query to the correct sub function. - * @param {string} type - The type of the stat; power or hp - * @return {number} The chosen stat value - */ - getStat(type) { - switch (type) { - case StatType.Power: - return this.getPower(); - case StatType.Hp: - return this.getHp(); - } - } - - /** - * Apply any modifiers that explicitly say they change the base skill value - */ - getBaseStatModifiers() { - const baseModifierEffects = [ - EffectNames.CopyCharacter, - EffectNames.CalculatePrintedPower, - EffectNames.SetBasePower, - ]; - - let baseEffects = this.getRawEffects().filter((effect) => baseModifierEffects.includes(effect.type)); - let basePowerModifiers = [StatModifier.fromCard(this.printedPower, this, 'Printed power', false)]; - let baseHpModifiers = [StatModifier.fromCard(this.printedHp, this, 'Printed hp', false)]; - let basePower = this.printedPower; - let baseHp = this.printedHp; - - baseEffects.forEach((effect) => { - switch (effect.type) { - // this case is for cards that don't have a default printed power but it is instead calculated - case EffectNames.CalculatePrintedPower: { - let damageFunction = effect.getValue(this); - let calculatedDamageValue = damageFunction(this); - basePower = calculatedDamageValue; - basePowerModifiers = basePowerModifiers.filter( - (mod) => !mod.name.startsWith('Printed power') - ); - basePowerModifiers.push( - StatModifier.fromEffect( - basePower, - effect, - false, - `Printed power due to ${StatModifier.getEffectName(effect)}` - ) - ); - break; - } - case EffectNames.CopyCharacter: { - let copiedCard = effect.getValue(this); - basePower = copiedCard.getPrintedStat(StatType.Power); - baseHp = copiedCard.getPrintedStat(StatType.Hp); - // replace existing base or copied modifier - basePowerModifiers = basePowerModifiers.filter( - (mod) => !mod.name.startsWith('Printed stat') - ); - baseHpModifiers = baseHpModifiers.filter( - (mod) => !mod.name.startsWith('Printed stat') - ); - basePowerModifiers.push( - StatModifier.fromEffect( - basePower, - effect, - false, - `Printed skill from ${copiedCard.name} due to ${StatModifier.getEffectName(effect)}` - ) - ); - baseHpModifiers.push( - StatModifier.fromEffect( - baseHp, - effect, - false, - `Printed skill from ${copiedCard.name} due to ${StatModifier.getEffectName(effect)}` - ) - ); - break; - } - case EffectNames.SetBasePower: - basePower = effect.getValue(this); - basePowerModifiers.push( - StatModifier.fromEffect( - basePower, - effect, - true, - `Base power set by ${StatModifier.getEffectName(effect)}` - ) - ); - break; - } - }); - - let overridingPowerModifiers = basePowerModifiers.filter((mod) => mod.overrides); - if (overridingPowerModifiers.length > 0) { - let lastModifier = _.last(overridingPowerModifiers); - basePowerModifiers = [lastModifier]; - basePower = lastModifier.amount; - } - let overridingHpModifiers = baseHpModifiers.filter((mod) => mod.overrides); - if (overridingHpModifiers.length > 0) { - let lastModifier = _.last(overridingHpModifiers); - baseHpModifiers = [lastModifier]; - baseHp = lastModifier.amount; - } - - return { - basePowerModifiers: basePowerModifiers, - basePower: basePower, - baseHpModifiers: baseHpModifiers, - BaseHp: baseHp - }; - } - - getStatusTokenSkill() { - let modifiers = this.getStatusTokenModifiers(); - let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - if (isNaN(skill)) { - return 0; - } - return skill; - } - - // getStatusTokenModifiers() { - // let modifiers = []; - // let modifierEffects = this.getRawEffects().filter((effect) => effect.type === EffectNames.ModifyBothSkills); - - // // skill modifiers - // modifierEffects.forEach((modifierEffect) => { - // const value = modifierEffect.getValue(this); - // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); - // }); - // modifiers = modifiers.filter((modifier) => modifier.type === 'token'); - - // // adjust honor status effects - // this.adjustHonorStatusModifiers(modifiers); - // return modifiers; - // } - - // getMilitaryModifiers(exclusions) { - // let baseSkillModifiers = this.getBaseSkillModifiers(); - // if (isNaN(baseSkillModifiers.baseMilitarySkill)) { - // return baseSkillModifiers.baseMilitaryModifiers; - // } - - // if (!exclusions) { - // exclusions = []; - // } - - // let rawEffects; - // if (typeof exclusions === 'function') { - // rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); - // } else { - // rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); - // } - - // // set effects - // let setEffects = rawEffects.filter( - // (effect) => effect.type === EffectNames.SetMilitarySkill || effect.type === EffectNames.SetDash - // ); - // if (setEffects.length > 0) { - // let latestSetEffect = _.last(setEffects); - // let setAmount = latestSetEffect.type === EffectNames.SetDash ? undefined : latestSetEffect.getValue(this); - // return [ - // StatModifier.fromEffect( - // setAmount, - // latestSetEffect, - // true, - // `Set by ${StatModifier.getEffectName(latestSetEffect)}` - // ) - // ]; - // } - - // let modifiers = baseSkillModifiers.baseMilitaryModifiers; - - // // skill modifiers - // let modifierEffects = rawEffects.filter( - // (effect) => - // effect.type === EffectNames.AttachmentMilitarySkillModifier || - // effect.type === EffectNames.ModifyMilitarySkill || - // effect.type === EffectNames.ModifyBothSkills - // ); - // modifierEffects.forEach((modifierEffect) => { - // const value = modifierEffect.getValue(this); - // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); - // }); - - // // adjust honor status effects - // this.adjustHonorStatusModifiers(modifiers); - - // // multipliers - // let multiplierEffects = rawEffects.filter( - // (effect) => effect.type === EffectNames.ModifyMilitarySkillMultiplier - // ); - // multiplierEffects.forEach((multiplierEffect) => { - // let multiplier = multiplierEffect.getValue(this); - // let currentTotal = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - // let amount = (multiplier - 1) * currentTotal; - // modifiers.push(StatModifier.fromEffect(amount, multiplierEffect)); - // }); - - // return modifiers; - // } - - // getPoliticalModifiers(exclusions) { - // let baseSkillModifiers = this.getBaseSkillModifiers(); - // if (isNaN(baseSkillModifiers.basePoliticalSkill)) { - // return baseSkillModifiers.basePoliticalModifiers; - // } - - // if (!exclusions) { - // exclusions = []; - // } - - // let rawEffects; - // if (typeof exclusions === 'function') { - // rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); - // } else { - // rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); - // } - - // // set effects - // let setEffects = rawEffects.filter((effect) => effect.type === EffectNames.SetPoliticalSkill); - // if (setEffects.length > 0) { - // let latestSetEffect = _.last(setEffects); - // let setAmount = latestSetEffect.getValue(this); - // return [ - // StatModifier.fromEffect( - // setAmount, - // latestSetEffect, - // true, - // `Set by ${StatModifier.getEffectName(latestSetEffect)}` - // ) - // ]; - // } - - // let modifiers = baseSkillModifiers.basePoliticalModifiers; - - // // skill modifiers - // let modifierEffects = rawEffects.filter( - // (effect) => - // effect.type === EffectNames.AttachmentPoliticalSkillModifier || - // effect.type === EffectNames.ModifyPoliticalSkill || - // effect.type === EffectNames.ModifyBothSkills - // ); - // modifierEffects.forEach((modifierEffect) => { - // const value = modifierEffect.getValue(this); - // modifiers.push(StatModifier.fromEffect(value, modifierEffect)); - // }); - - // // adjust honor status effects - // this.adjustHonorStatusModifiers(modifiers); - - // // multipliers - // let multiplierEffects = rawEffects.filter( - // (effect) => effect.type === EffectNames.ModifyPoliticalSkillMultiplier - // ); - // multiplierEffects.forEach((multiplierEffect) => { - // let multiplier = multiplierEffect.getValue(this); - // let currentTotal = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - // let amount = (multiplier - 1) * currentTotal; - // modifiers.push(StatModifier.fromEffect(amount, multiplierEffect)); - // }); - - // return modifiers; - // } - - get showStats() { - return isArena(this.location) && this.type === CardTypes.Unit; - } - - get militarySkillSummary() { - if (!this.showStats) { - return {}; - } - let modifiers = this.getPowerModifiers().map((modifier) => Object.assign({}, modifier)); - let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - return { - stat: isNaN(skill) ? '-' : Math.max(skill, 0).toString(), - modifiers: modifiers - }; - } - - get politicalSkillSummary() { - if (!this.showStats) { - return {}; - } - let modifiers = this.getPoliticalModifiers().map((modifier) => Object.assign({}, modifier)); - modifiers.forEach((modifier) => (modifier = Object.assign({}, modifier))); - let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - return { - stat: isNaN(skill) ? '-' : Math.max(skill, 0).toString(), - modifiers: modifiers - }; - } - - get power() { - return this.getPower(); - } - - getPower(floor = true, excludeModifiers = []) { - let modifiers = this.getPowerModifiers(excludeModifiers); - let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - if (isNaN(skill)) { - return 0; - } - return floor ? Math.max(0, skill) : skill; - } - - get basePower() { - return this.getBasePower(); - } - - getBasePower() { - let skill = this.getBaseStatModifiers().basePower; - if (isNaN(skill)) { - return 0; - } - return Math.max(0, skill); - } - - getPowerModifiers(exclusions) { - let baseStatModifiers = this.getBaseStatModifiers(); - if (isNaN(baseStatModifiers.basePower)) { - return baseStatModifiers.basePowerModifiers; - } - - if (!exclusions) { - exclusions = []; - } - - let rawEffects; - if (typeof exclusions === 'function') { - rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); - } else { - rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); - } - - // set effects (i.e., "set power to X") - let setEffects = rawEffects.filter( - (effect) => effect.type === EffectNames.SetPower - ); - if (setEffects.length > 0) { - let latestSetEffect = _.last(setEffects); - let setAmount = latestSetEffect.getValue(this); - return [ - StatModifier.fromEffect( - setAmount, - latestSetEffect, - true, - `Set by ${StatModifier.getEffectName(latestSetEffect)}` - ) - ]; - } - - let modifiers = baseStatModifiers.basePowerModifiers; - - // power modifiers - // TODO: remove status tokens completely, upgrades completely cover that category - // TODO: does this work for resolving effects like Raid that depend on whether we're the attacker or not? - let modifierEffects = rawEffects.filter( - (effect) => - effect.type === EffectNames.UpgradePowerModifier || - effect.type === EffectNames.ModifyPower || - effect.type === EffectNames.ModifyStats - ); - modifierEffects.forEach((modifierEffect) => { - const value = modifierEffect.getValue(this); - modifiers.push(StatModifier.fromEffect(value, modifierEffect)); - }); - - return modifiers; - } - - get hp() { - return this.getHp(); - } - - getHp(floor = true, excludeModifiers = []) { - let modifiers = this.getPowerModifiers(excludeModifiers); - let skill = modifiers.reduce((total, modifier) => total + modifier.amount, 0); - if (isNaN(skill)) { - return 0; - } - return floor ? Math.max(0, skill) : skill; - } - - get baseHp() { - return this.getBaseHp(); - } - - getBaseHp() { - let skill = this.getBaseStatModifiers().basePoliticalSkill; - if (isNaN(skill)) { - return 0; - } - return Math.max(0, skill); - } - - getHpModifiers(exclusions) { - let baseStatModifiers = this.getBaseStatModifiers(); - if (isNaN(baseStatModifiers.baseHp)) { - return baseStatModifiers.baseHpModifiers; - } - - if (!exclusions) { - exclusions = []; - } - - let rawEffects; - if (typeof exclusions === 'function') { - rawEffects = this.getRawEffects().filter((effect) => !exclusions(effect)); - } else { - rawEffects = this.getRawEffects().filter((effect) => !exclusions.includes(effect.type)); - } - - let modifiers = baseStatModifiers.baseHpModifiers; - - // hp modifiers - // TODO: remove status tokens completely, upgrades completely cover that category - let modifierEffects = rawEffects.filter( - (effect) => - effect.type === EffectNames.UpgradeHpModifier || - effect.type === EffectNames.ModifyHp - ); - modifierEffects.forEach((modifierEffect) => { - const value = modifierEffect.getValue(this); - modifiers.push(StatModifier.fromEffect(value, modifierEffect)); - }); - - return modifiers; - } - - exhaust() { - this.exhausted = true; - } - - ready() { - this.exhausted = false; - } - - canPlay(context, type) { - return ( - this.checkRestrictions(type, context) && - context.player.checkRestrictions(type, context) && - this.checkRestrictions('play', context) && - context.player.checkRestrictions('play', context) && - (!this.hasPrintedKeyword('peaceful') || !this.game.currentConflict) - ); - } - - getActions(location = this.location) { - // if card is already in play or is an event, return the default actions - if (location === Locations.SpaceArena || location === Locations.GroundArena || this.type === CardTypes.Event) { - return super.getActions(); - } - - // TODO: add base / leader actions if this doesn't already cover them - - // otherwise (i.e. card is in hand), return play card action(s) + other available card actions - return this.getPlayActions().concat(super.getActions()); - } - - /** - * Deals with the engine effects of leaving play, making sure all statuses are removed. Anything which changes - * the state of the card should be here. This is also called in some strange corner cases e.g. for attachments - * which aren't actually in play themselves when their parent (which is in play) leaves play. - */ - leavesPlay() { - // If this is an attachment and is attached to another card, we need to remove all links between them - if (this.parent && this.parent.attachments) { - this.parent.removeAttachment(this); - this.parent = null; - } - - // Remove any cards underneath from the game - const cardsUnderneath = this.controller.getSourceListForPile(this.uuid).map((a) => a); - if (cardsUnderneath.length > 0) { - cardsUnderneath.forEach((card) => { - this.controller.moveCard(card, Locations.RemovedFromGame); - }); - this.game.addMessage( - '{0} {1} removed from the game due to {2} leaving play', - cardsUnderneath, - cardsUnderneath.length === 1 ? 'is' : 'are', - this - ); - } - - if (this.isParticipating()) { - this.game.currentConflict.removeFromConflict(this); - } - - this.exhausted = false; - this.new = false; - super.leavesPlay(); - } - - resetForConflict() { - this.covert = false; - this.inConflict = false; - } - - // canDeclareAsAttacker(conflictType, ring, province, incomingAttackers = undefined) { - // // eslint-disable-line no-unused-vars - // if (!province) { - // let provinces = - // this.game.currentConflict && this.game.currentConflict.defendingPlayer - // ? this.game.currentConflict.defendingPlayer.getProvinces() - // : null; - // if (provinces) { - // return provinces.some( - // (a) => - // a.canDeclare(conflictType, ring) && - // this.canDeclareAsAttacker(conflictType, ring, a, incomingAttackers) - // ); - // } - // } - - // let attackers = this.game.isDuringConflict() ? this.game.currentConflict.attackers : []; - // if (incomingAttackers) { - // attackers = incomingAttackers; - // } - // if (!attackers.includes(this)) { - // attackers = attackers.concat(this); - // } - - // // Check if I add an element that I can\'t attack with - // const elementsAdded = this.upgrades.reduce( - // (array, attachment) => array.concat(attachment.getEffects(EffectNames.AddElementAsAttacker)), - // this.getEffects(EffectNames.AddElementAsAttacker) - // ); - - // if ( - // elementsAdded.some((element) => - // this.game.rings[element] - // .getEffects(EffectNames.CannotDeclareRing) - // .some((match) => match(this.controller)) - // ) - // ) { - // return false; - // } - - // if ( - // conflictType === ConflictTypes.Military && - // attackers.reduce((total, card) => total + card.sumEffects(EffectNames.CardCostToAttackMilitary), 0) > - // this.controller.hand.size() - // ) { - // return false; - // } - - // let fateCostToAttackProvince = province ? province.getFateCostToAttack() : 0; - // if ( - // attackers.reduce((total, card) => total + card.sumEffects(EffectNames.FateCostToAttack), 0) + - // fateCostToAttackProvince > - // this.controller.fate - // ) { - // return false; - // } - // if (this.anyEffect(EffectNames.CanOnlyBeDeclaredAsAttackerWithElement)) { - // for (let element of this.getEffects(EffectNames.CanOnlyBeDeclaredAsAttackerWithElement)) { - // if (!ring.hasElement(element) && !elementsAdded.includes(element)) { - // return false; - // } - // } - // } - - // if (this.controller.anyEffect(EffectNames.LimitLegalAttackers)) { - // const checks = this.controller.getEffects(EffectNames.LimitLegalAttackers); - // let valid = true; - // checks.forEach((check) => { - // if (typeof check === 'function') { - // valid = valid && check(this); - // } - // }); - // if (!valid) { - // return false; - // } - // } - - // return ( - // this.checkRestrictions('declareAsAttacker', this.game.getFrameworkContext()) && - // this.canParticipateAsAttacker(conflictType) && - // this.location === Locations.PlayArea && - // !this.exhausted - // ); - // } - - // canDeclareAsDefender(conflictType = this.game.currentConflict.conflictType) { - // return ( - // this.checkRestrictions('declareAsDefender', this.game.getFrameworkContext()) && - // this.canParticipateAsDefender(conflictType) && - // this.location === Locations.PlayArea && - // !this.exhausted && - // !this.covert - // ); - // } - - // canParticipateAsAttacker(conflictType = this.game.currentConflict.conflictType) { - // let effects = this.getEffects(EffectNames.CannotParticipateAsAttacker); - // return !effects.some((value) => value === 'both' || value === conflictType) && !this.hasDash(conflictType); - // } - - // canParticipateAsDefender(conflictType = this.game.currentConflict.conflictType) { - // let effects = this.getEffects(EffectNames.CannotParticipateAsDefender); - // let hasDash = conflictType ? this.hasDash(conflictType) : false; - - // return !effects.some((value) => value === 'both' || value === conflictType) && !hasDash; - // } - - // bowsOnReturnHome() { - // return !this.anyEffect(EffectNames.DoesNotBow); - // } - - setDefaultController(player) { - this.defaultController = player; - } - - // getModifiedController() { - // if ( - // this.location === Locations.PlayArea || - // (this.type === CardTypes.Holding && this.location.includes('province')) - // ) { - // return this.mostRecentEffect(EffectNames.TakeControl) || this.defaultController; - // } - // return this.owner; - // } - - // canDisguise(card, context, intoConflictOnly) { - // return ( - // this.disguisedKeywordTraits.some((trait) => card.hasTrait(trait)) && - // card.allowGameAction('discardFromPlay', context) && - // !card.isUnique() && - // (!intoConflictOnly || card.isParticipating()) - // ); - // } - - // play() { - // //empty function so playcardaction doesn't crash the game - // } - - // getSummary(activePlayer, hideWhenFaceup) { - // let baseSummary = super.getSummary(activePlayer, hideWhenFaceup); - - // return _.extend(baseSummary, { - // attached: !!this.parent, - // attachments: this.upgrades.map((attachment) => { - // return attachment.getSummary(activePlayer, hideWhenFaceup); - // }), - // childCards: this.childCards.map((card) => { - // return card.getSummary(activePlayer, hideWhenFaceup); - // }), - // inConflict: this.inConflict, - // isConflict: this.isConflict, - // isDynasty: this.isDynasty, - // isPlayableByMe: this.isConflict && this.controller.isCardInPlayableLocation(this, PlayTypes.PlayFromHand), - // isPlayableByOpponent: - // this.isConflict && - // this.controller.opponent && - // this.controller.opponent.isCardInPlayableLocation(this, PlayTypes.PlayFromHand), - // bowed: this.exhausted, - // fate: this.fate, - // new: this.new, - // covert: this.covert, - // showStats: this.showStats, - // militarySkillSummary: this.militarySkillSummary, - // politicalSkillSummary: this.politicalSkillSummary, - // glorySummary: this.glorySummary, - // controller: this.controller.getShortSummary() - // }); - // } -} - -module.exports = DeckCard; \ No newline at end of file diff --git a/server/swu/game/costs/Costs.ts b/server/swu/game/costs/Costs.ts index 9d4b19f04..0a8a5d788 100644 --- a/server/swu/game/costs/Costs.ts +++ b/server/swu/game/costs/Costs.ts @@ -14,7 +14,6 @@ import { GameActionCost } from './GameActionCost'; import { MetaActionCost } from './MetaActionCost'; import { ReduceableResourceCost } from '../costs/ReduceableResourceCost'; // import { TargetDependentFateCost } from './costs/TargetDependentFateCost'; -import DeckCard from '../card/deckcard'; import Player from '../player'; type SelectCostProperties = Omit; @@ -117,7 +116,7 @@ export function shuffleIntoDeck(properties: SelectCostProperties): Cost { // /** // * Cost that requires discarding a specific card. // */ -// export function discardCardSpecific(cardFunc: (context: AbilityContext) => DeckCard): Cost { +// export function discardCardSpecific(cardFunc: (context: AbilityContext) => BaseCard): Cost { // return new GameActionCost(GameActions.discardCard((context) => ({ target: cardFunc(context) }))); // } @@ -148,7 +147,7 @@ export function discardTopCardsFromDeck(properties: { amount: number; }): Cost { context.costs.discardTopCardsFromDeck = context.player.deck.first(4); }, pay: (context) => { - for (const card of context.costs.discardTopCardsFromDeck as DeckCard[]) { + for (const card of context.costs.discardTopCardsFromDeck as BaseCard[]) { card.controller.moveCard(card, Locations.Deck); } } @@ -175,7 +174,7 @@ export function discardTopCardsFromDeck(properties: { amount: number; }): Cost { // Object.assign( // { // gameAction: GameActions.discardStatusToken(), -// subActionProperties: (card: DeckCard) => ({ target: card.getStatusToken(CharacterStatus.Honored) }) +// subActionProperties: (card: BaseCard) => ({ target: card.getStatusToken(CharacterStatus.Honored) }) // }, // properties // ) diff --git a/server/swu/game/gameActions/AttackAction.ts b/server/swu/game/gameActions/AttackAction.ts index ca3654afe..85c2d9e04 100644 --- a/server/swu/game/gameActions/AttackAction.ts +++ b/server/swu/game/gameActions/AttackAction.ts @@ -1,6 +1,5 @@ import type { AbilityContext } from '../AbilityContext'; import { CardTypes, Durations, EventNames, Locations, isArena } from '../Constants'; -import type DeckCard from '../card/deckcard'; import { Attack } from '../attack/Attack'; import { EffectNames } from '../Constants' import { AttackFlow } from '../attack/AttackFlow'; @@ -9,12 +8,12 @@ import { CardGameAction, type CardActionProperties } from './CardGameAction'; import { type GameAction } from './GameAction'; export interface AttackProperties extends CardActionProperties { - attacker?: DeckCard; - attackerCondition?: (card: DeckCard, context: TriggeredAbilityContext) => boolean; + attacker?: BaseCard; + attackerCondition?: (card: BaseCard, context: TriggeredAbilityContext) => boolean; message?: string; messageArgs?: (attack: Attack, context: AbilityContext) => any | any[]; costHandler?: (context: AbilityContext, prompt: any) => void; - statistic?: (card: DeckCard) => number; + statistic?: (card: BaseCard) => number; } export class AttackAction extends CardGameAction { @@ -41,7 +40,7 @@ export class AttackAction extends CardGameAction { ]; } - canAffect(card: DeckCard, context: AbilityContext, additionalProperties = {}): boolean { + canAffect(card: BaseCard, context: AbilityContext, additionalProperties = {}): boolean { if (!context.player.opponent) { return false; } @@ -101,7 +100,7 @@ export class AttackAction extends CardGameAction { additionalProperties ); - const cards = (target as DeckCard[]).filter((card) => this.canAffect(card, context)); + const cards = (target as BaseCard[]).filter((card) => this.canAffect(card, context)); if (cards.length !== 1) { return; } @@ -126,7 +125,7 @@ export class AttackAction extends CardGameAction { event.attacker = properties.attacker; event.target = properties.target; - const duel = new Attack( + event.attack = new Attack( context.game, properties.attacker, cards, @@ -134,7 +133,6 @@ export class AttackAction extends CardGameAction { properties.statistic, context.player ); - event.duel = duel; } eventHandler(event, additionalProperties): void { diff --git a/server/swu/game/gameActions/CardGameAction.ts b/server/swu/game/gameActions/CardGameAction.ts index e50442bec..114fe065f 100644 --- a/server/swu/game/gameActions/CardGameAction.ts +++ b/server/swu/game/gameActions/CardGameAction.ts @@ -1,7 +1,6 @@ import type { AbilityContext } from '../AbilityContext'; import type BaseCard from '../card/basecard'; import { CardTypes, EffectNames, Locations } from '../Constants'; -import type DeckCard from '../card/deckcard'; import { GameAction, GameActionProperties } from './GameAction'; // import { LoseFateAction } from './LoseFateAction'; @@ -151,7 +150,7 @@ export class CardGameAction

, C> = T | export function derive, C>(input: Derivable, context: C): T { return typeof input === 'function' ? input(context) : input; +} + +// convert a set of strings to map to an enum type, throw if any of them is not a legal value +export function checkConvertToEnum(values: string[], enumObj: T): Array { + let result: Array = []; + + for (const value of values) { + if (Object.values(enumObj).indexOf(value.toLowerCase()) >= 0) { + result.push(value as T[keyof T]); + } else { + throw new Error(`Invalid value for enum: ${value}`); + } + } + + return result; } \ No newline at end of file diff --git a/test/swu/helpers/playerinteractionwrapper.js b/test/swu/helpers/playerinteractionwrapper.js index f8d147fc9..14d970782 100644 --- a/test/swu/helpers/playerinteractionwrapper.js +++ b/test/swu/helpers/playerinteractionwrapper.js @@ -98,7 +98,7 @@ class PlayerInteractionWrapper { /** * Gets all cards in play for a player in the space arena - * @return {DeckCard[]} - List of player's cards currently in play in the space arena + * @return {BaseCard[]} - List of player's cards currently in play in the space arena */ get inPlay() { return this.player.filterCardsInPlay(() => true); @@ -106,7 +106,7 @@ class PlayerInteractionWrapper { /** * Gets all cards in play for a player in the space arena - * @return {DeckCard[]} - List of player's cards currently in play in the space arena + * @return {BaseCard[]} - List of player's cards currently in play in the space arena */ get spaceArena() { return this.player.filterCardsInPlay((card) => card.location === 'space arena'); @@ -114,7 +114,7 @@ class PlayerInteractionWrapper { /** * Gets all cards in play for a player in the ground arena - * @return {DeckCard[]} - List of player's cards currently in play in the ground arena + * @return {BaseCard[]} - List of player's cards currently in play in the ground arena */ get groundArena() { return this.player.filterCardsInPlay((card) => card.location === 'ground arena');