Skip to content

Commit

Permalink
Add Sabine unit (partial) and Salacious Crumb (#14)
Browse files Browse the repository at this point in the history
Some more units to validate various engine mechanics. Added Sabine unit (partially) which has a passive ongoing effect, and added the Crumb unit which has a when-played triggered event. Both systems now working, with some outstanding caveats or cleanup tasks that will be addressed soon.
  • Loading branch information
AMMayberry1 authored Aug 10, 2024
1 parent 44f0462 commit a856930
Show file tree
Hide file tree
Showing 71 changed files with 1,610 additions and 1,334 deletions.
104 changes: 104 additions & 0 deletions .vscode/bookmarks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
{
"files": [
{
"path": "server/game/core/Game.js",
"bookmarks": [
{
"line": 686,
"column": 8,
"label": "Game setup and start"
}
]
},
{
"path": "server/game/core/event/EventWindow.js",
"bookmarks": [
{
"line": 25,
"column": 26,
"label": "Event resolution steps"
}
]
},
{
"path": "server/game/core/gameSteps/abilityWindow/ForcedTriggeredAbilityWindow.js",
"bookmarks": [
{
"line": 40,
"column": 7,
"label": "Simultaneous reaction resolver"
}
]
},
{
"path": "server/game/core/gameSteps/ActionWindow.js",
"bookmarks": [
{
"line": 23,
"column": 14,
"label": "Card click entrypoint"
}
]
},
{
"path": "server/game/core/card/Card.ts",
"bookmarks": [
{
"line": 298,
"column": 64,
"label": "EVENT CARDS: play action filter #1"
},
{
"line": 610,
"column": 151,
"label": "EVENT CARDS: bluff window reaction re-registration"
},
{
"line": 636,
"column": 43,
"label": "UPGRADES: effect management"
},
{
"line": 1013,
"column": 43,
"label": "EVENT CARDS: play action filter #2"
}
]
},
{
"path": "server/game/core/ability/CardAbility.js",
"bookmarks": [
{
"line": 33,
"column": 37,
"label": "EVENT CARDS: manual cost handling for play"
}
]
},
{
"path": "server/game/core/Player.js",
"bookmarks": [
{
"line": 636,
"column": 105,
"label": "EVENT CARDS: register for bluff window"
},
{
"line": 1180,
"column": 42,
"label": "EVENT CARDS: being played zone rules"
}
]
},
{
"path": "legacy_jigoku/server/game/cards/08-MotC/ChukanNobue.js",
"bookmarks": [
{
"line": 3,
"column": 15,
"label": "L5R persistent effect example"
}
]
}
]
}
43 changes: 22 additions & 21 deletions legacy_jigoku/server/game/EffectEngine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Durations, EffectNames, EventNames } from './Constants';
import { Event } from './Events/Event';
import type Effect from './Effects/Effect';
import type EffectSource from './EffectSource';
import { EventRegistrar } from './EventRegistrar';
Expand All @@ -7,8 +8,8 @@ import type Game from './game';
export class EffectEngine {
events: EventRegistrar;
effects: Array<Effect> = [];
customDurationEvents = [];
newEffect = false;
customDurationEvents: any[] = [];
effectsChangedSinceLastCheck = false;

constructor(private game: Game) {
this.events = new EventRegistrar(game, this);
Expand All @@ -26,13 +27,13 @@ export class EffectEngine {
if (effect.duration === Durations.Custom) {
this.registerCustomDurationEvents(effect);
}
this.newEffect = true;
this.effectsChangedSinceLastCheck = true;
return effect;
}

checkDelayedEffects(events: any[]) {
let effectsToTrigger = [];
const effectsToRemove = [];
checkDelayedEffects(events: Event[]) {
let effectsToTrigger: Effect[] = [];
const effectsToRemove: Effect[] = [];
for (const effect of this.effects.filter(
(effect) => effect.isEffectActive() && effect.effect.type === EffectNames.DelayedEffect
)) {
Expand All @@ -53,7 +54,7 @@ export class EffectEngine {
}
}
}
effectsToTrigger = effectsToTrigger.map((effect) => {
const effectTriggers = effectsToTrigger.map((effect) => {
const properties = effect.effect.getValue();
const context = effect.context;
const targets = effect.targets;
Expand All @@ -78,8 +79,8 @@ export class EffectEngine {
if (effectsToRemove.length > 0) {
this.unapplyAndRemove((effect) => effectsToRemove.includes(effect));
}
if (effectsToTrigger.length > 0) {
this.game.openSimultaneousEffectWindow(effectsToTrigger);
if (effectTriggers.length > 0) {
this.game.openSimultaneousEffectWindow(effectTriggers);
}
}

Expand All @@ -102,11 +103,11 @@ export class EffectEngine {
}

checkEffects(prevStateChanged = false, loops = 0) {
if (!prevStateChanged && !this.newEffect) {
if (!prevStateChanged && !this.effectsChangedSinceLastCheck) {
return false;
}
let stateChanged = false;
this.newEffect = false;
this.effectsChangedSinceLastCheck = false;
// Check each effect's condition and find new targets
stateChanged = this.effects.reduce((stateChanged, effect) => effect.checkCondition(stateChanged), stateChanged);
if (loops === 10) {
Expand All @@ -118,19 +119,19 @@ export class EffectEngine {
}

onConflictFinished() {
this.newEffect = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfConflict);
this.effectsChangedSinceLastCheck = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfConflict);
}

onDuelFinished() {
this.newEffect = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfDuel);
this.effectsChangedSinceLastCheck = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfDuel);
}

onPhaseEnded() {
this.newEffect = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfPhase);
this.effectsChangedSinceLastCheck = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfPhase);
}

onRoundEnded() {
this.newEffect = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfRound);
this.effectsChangedSinceLastCheck = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilEndOfRound);
}

onPassActionPhasePriority(event) {
Expand All @@ -143,7 +144,7 @@ export class EffectEngine {
}
}

this.newEffect = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilPassPriority);
this.effectsChangedSinceLastCheck = this.unapplyAndRemove((effect) => effect.duration === Durations.UntilPassPriority);
for (const effect of this.effects) {
if (
effect.duration === Durations.UntilOpponentPassPriority ||
Expand Down Expand Up @@ -173,7 +174,7 @@ export class EffectEngine {
}

unregisterCustomDurationEvents(effect: Effect) {
const remainingEvents = [];
const remainingEvents: Event[] = [];
for (const event of this.customDurationEvents) {
if (event.effect === effect) {
this.game.removeListener(event.name, event.handler);
Expand All @@ -197,11 +198,11 @@ export class EffectEngine {
}

unapplyAndRemove(match: (effect: Effect) => boolean) {
let removedEffect = false;
const remainingEffects = [];
let anyEffectRemoved = false;
const remainingEffects: Effect[] = [];
for (const effect of this.effects) {
if (match(effect)) {
removedEffect = true;
anyEffectRemoved = true;
effect.cancel();
if (effect.duration === Durations.Custom) {
this.unregisterCustomDurationEvents(effect);
Expand All @@ -211,7 +212,7 @@ export class EffectEngine {
}
}
this.effects = remainingEffects;
return removedEffect;
return anyEffectRemoved;
}

getDebugInfo() {
Expand Down
6 changes: 3 additions & 3 deletions legacy_jigoku/server/game/Effects/DynamicEffect.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ class DynamicEffect extends StaticEffect {
recalculate(target) {
let oldValue = this.getValue(target);
let newValue = this.setValue(target, this.calculate(target, this.context));
if(typeof oldValue === 'function' && typeof newValue === 'function') {
if (typeof oldValue === 'function' && typeof newValue === 'function') {
return oldValue.toString() !== newValue.toString();
}
if(Array.isArray(oldValue) && Array.isArray(newValue)) {
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
}
return oldValue !== newValue;
}

getValue(target) {
if(target) {
if (target) {
return this.values[target.uuid];
}
}
Expand Down
7 changes: 2 additions & 5 deletions legacy_jigoku/server/game/gamesteps/actionwindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ class ActionWindow extends UiPrompt {

this.title = title;
this.windowName = windowName;
if(this.game.currentConflict && !this.game.currentConflict.isSinglePlayer) {
this.currentPlayer = this.game.currentConflict.defendingPlayer;
} else {
this.currentPlayer = game.getFirstPlayer();
}
this.currentPlayer = game.actionPhaseActivePlayer;

this.currentPlayerConsecutiveActions = 0;
this.opportunityCounter = 0;
this.prevPlayerPassed = false;
Expand Down
7 changes: 4 additions & 3 deletions server/game/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { GameSystem } from './core/gameSystem/GameSystem';
import type Card from './core/card/Card';
import type CardAbility from './core/ability/CardAbility';
import type { IAttackProperties } from './gameSystems/AttackSystem';
import type { RelativePlayer, TargetMode, CardType, Location, EventName, PhaseName } from './core/Constants';
import type { RelativePlayer, TargetMode, CardType, Location, EventName, PhaseName, LocationFilter } from './core/Constants';
// import type { StatusToken } from './StatusToken';

interface IBaseTarget {
Expand Down Expand Up @@ -169,8 +169,9 @@ export interface ITriggeredAbilityAggregateWhenProps extends IAbilityProps<Trigg

export type ITriggeredAbilityProps = ITriggeredAbilityWhenProps | ITriggeredAbilityAggregateWhenProps;

export interface IPersistentEffectProps<Source = any> {
location?: Location | Location[];
export interface IConstantAbilityProps<Source = any> {
// TODO: is this the right name or should it be 'location' like it was before?
locationFilter?: LocationFilter | LocationFilter[];
// TODO: what's the difference between condition and match?
condition?: (context: AbilityContext<Source>) => boolean;
match?: (card: Card, context?: AbilityContext<Source>) => boolean;
Expand Down
7 changes: 3 additions & 4 deletions server/game/actions/PlayUnitAction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AbilityContext } from '../core/ability/AbilityContext.js';
import BaseAction from '../core/ability/PlayerAction.js';
import PlayerAction from '../core/ability/PlayerAction.js';
import { EffectName, EventName, Location, PhaseName, PlayType, RelativePlayer } from '../core/Constants.js';
import { payAdjustableResourceCost } from '../costs/CostLibrary.js';
import { putIntoPlay } from '../gameSystems/GameSystemLibrary.js';
Expand All @@ -8,7 +8,7 @@ import type Player from '../core/Player.js';

type ExecutionContext = AbilityContext & { onPlayCardSource: any };

export class PlayUnitAction extends BaseAction {
export class PlayUnitAction extends PlayerAction {
public title = 'Play this unit';

public constructor(card: Card) {
Expand Down Expand Up @@ -70,8 +70,7 @@ export class PlayUnitAction extends BaseAction {
const player = effect.length > 0 ? RelativePlayer.Opponent : RelativePlayer.Self;
context.game.openEventWindow([
putIntoPlay({
controller: player,
overrideLocation: Location.Hand // TODO: should we be doing this?
controller: player
}).getEvent(context.source, context),
cardPlayedEvent
]);
Expand Down
11 changes: 7 additions & 4 deletions server/game/actions/TriggerAttackAction.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import type { AbilityContext } from '../core/ability/AbilityContext.js';
import BaseAction from '../core/ability/PlayerAction.js';
import PlayerAction from '../core/ability/PlayerAction.js';
import { EffectName, EventName, Location, PhaseName, PlayType, TargetMode, WildcardLocation } from '../core/Constants.js';
import { isArena } from '../core/utils/EnumHelpers.js';
import { exhaustSelf } from '../costs/CostLibrary.js';
import { attack } from '../gameSystems/GameSystemLibrary.js';
import type Player from '../core/Player.js';
import Card from '../core/card/Card.js';
import { unlimited } from '../core/ability/AbilityLimit.js';

export class TriggerAttackAction extends BaseAction {
public title = 'Attack';
export class TriggerAttackAction extends PlayerAction {
title = 'Attack';

// UP NEXT: this is a hack to get this to behave like a regular card ability for testing
limit = unlimited();

// TODO: rename to 'gameSystem' or 'triggeredSystem' or something and centralize where it is created, since it's also emitted from executeHandler
public constructor(card: Card) {
super(card, [exhaustSelf()], {
gameSystem: attack({ attacker: card }),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import AbilityDsl from '../../AbilityDsl';
import Card from '../../core/card/Card';
import { countUniqueAspects } from '../../core/utils/Helpers';

export default class SabineWrenExplosivesArtist extends Card {
protected override getImplementationId() {
return {
id: '3646264648',
internalName: 'sabine-wren#explosives-artist',
};
}

override setupCardAbilities() {
this.constantAbility({
// UP NEXT: helper fn on Card to get all friendly units in play
condition: () => countUniqueAspects(this.controller.getUnitsInPlay((card) => card !== this)) >= 3,

// UP NEXT: convert this to a named effect
effect: AbilityDsl.ongoingEffects.cardCannot('beAttacked')
});
}
}

// sabine is only partially implemented, still need to handle:
// - the effect override if she gains sentinel
// - her active ability
SabineWrenExplosivesArtist.implemented = false;
Loading

0 comments on commit a856930

Please sign in to comment.