Compare commits
7 Commits
a82b6b0685
...
08c6a67d16
| Author | SHA1 | Date |
|---|---|---|
|
|
08c6a67d16 | |
|
|
dda8f4cfe9 | |
|
|
2f2e4e56b5 | |
|
|
3840c3d739 | |
|
|
43bb246ab9 | |
|
|
25b44fd6d1 | |
|
|
601eb0f417 |
|
|
@ -1,11 +1,13 @@
|
||||||
import { MutableSignal, mutableSignal } from "@/utils/mutable-signal";
|
import { MutableSignal, mutableSignal } from "@/utils/mutable-signal";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandRegistry, CommandResult,
|
CommandRegistry,
|
||||||
|
CommandResult,
|
||||||
CommandRunnerContextExport,
|
CommandRunnerContextExport,
|
||||||
CommandSchema,
|
CommandSchema,
|
||||||
createCommandRegistry,
|
createCommandRegistry,
|
||||||
createCommandRunnerContext, parseCommandSchema,
|
createCommandRunnerContext,
|
||||||
|
parseCommandSchema,
|
||||||
} from "@/utils/command";
|
} from "@/utils/command";
|
||||||
import { PromptValidator } from "@/utils/command/command-runner";
|
import { PromptValidator } from "@/utils/command/command-runner";
|
||||||
import { Mulberry32RNG, ReadonlyRNG, RNG } from "@/utils/rng";
|
import { Mulberry32RNG, ReadonlyRNG, RNG } from "@/utils/rng";
|
||||||
|
|
@ -17,19 +19,28 @@ export interface IGameContext<TState extends Record<string, unknown> = {} > {
|
||||||
produceAsync(fn: (draft: TState) => void): Promise<void>;
|
produceAsync(fn: (draft: TState) => void): Promise<void>;
|
||||||
run<T>(input: string): Promise<CommandResult<T>>;
|
run<T>(input: string): Promise<CommandResult<T>>;
|
||||||
runParsed<T>(command: Command): Promise<CommandResult<T>>;
|
runParsed<T>(command: Command): Promise<CommandResult<T>>;
|
||||||
prompt: <TResult,TArgs extends any[]=any[]>(def: PromptDef<TArgs>, validator: PromptValidator<TResult,TArgs>, currentPlayer?: string | null) => Promise<TResult>;
|
prompt: <TResult, TArgs extends any[] = any[]>(
|
||||||
|
def: PromptDef<TArgs>,
|
||||||
|
validator: PromptValidator<TResult, TArgs>,
|
||||||
|
currentPlayer?: string | null,
|
||||||
|
) => Promise<TResult>;
|
||||||
|
|
||||||
// test only
|
// test only
|
||||||
_state: MutableSignal<TState>;
|
_state: MutableSignal<TState>;
|
||||||
_commands: CommandRunnerContextExport<IGameContext<TState>>;
|
_commands: CommandRunnerContextExport<IGameContext<TState>>;
|
||||||
_rng: RNG;
|
_rng: RNG;
|
||||||
}
|
}
|
||||||
|
export type IGameContextExport<TState extends Record<string, unknown> = {}> =
|
||||||
|
Omit<IGameContext<TState>, "_state" | "_commands" | "_rng">;
|
||||||
|
|
||||||
export function createGameContext<TState extends Record<string, unknown> = {}>(
|
export function createGameContext<TState extends Record<string, unknown> = {}>(
|
||||||
commandRegistry: CommandRegistry<IGameContext<TState>>,
|
commandRegistry: CommandRegistry<IGameContext<TState>>,
|
||||||
initialState?: TState | (() => TState)
|
initialState?: TState | (() => TState),
|
||||||
): IGameContext<TState> {
|
): IGameContext<TState> {
|
||||||
const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState;
|
const stateValue =
|
||||||
|
typeof initialState === "function"
|
||||||
|
? initialState()
|
||||||
|
: (initialState ?? ({} as TState));
|
||||||
const state = mutableSignal(stateValue);
|
const state = mutableSignal(stateValue);
|
||||||
let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
|
let commands: CommandRunnerContextExport<IGameContext<TState>> = null as any;
|
||||||
|
|
||||||
|
|
@ -53,7 +64,12 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
return commands.runParsed<T>(command);
|
return commands.runParsed<T>(command);
|
||||||
},
|
},
|
||||||
prompt(def, validator, currentPlayer) {
|
prompt(def, validator, currentPlayer) {
|
||||||
return commands.prompt(def.schema, validator, def.hintText, currentPlayer);
|
return commands.prompt(
|
||||||
|
def.schema,
|
||||||
|
validator,
|
||||||
|
def.hintText,
|
||||||
|
currentPlayer,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
_state: state,
|
_state: state,
|
||||||
|
|
@ -61,20 +77,28 @@ export function createGameContext<TState extends Record<string, unknown> = {} >(
|
||||||
_rng: new Mulberry32RNG(),
|
_rng: new Mulberry32RNG(),
|
||||||
};
|
};
|
||||||
|
|
||||||
context._commands = commands = createCommandRunnerContext(commandRegistry, context);
|
context._commands = commands = createCommandRunnerContext(
|
||||||
|
commandRegistry,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PromptDef<TArgs extends any[] = any[]> = {
|
export type PromptDef<TArgs extends any[] = any[]> = {
|
||||||
schema: CommandSchema,
|
schema: CommandSchema;
|
||||||
|
hintText?: string;
|
||||||
|
};
|
||||||
|
export function createPromptDef<TArgs extends any[] = any[]>(
|
||||||
|
schema: CommandSchema | string,
|
||||||
hintText?: string,
|
hintText?: string,
|
||||||
}
|
): PromptDef<TArgs> {
|
||||||
export function createPromptDef<TArgs extends any[]=any[]>(schema: CommandSchema | string, hintText?: string): PromptDef<TArgs> {
|
schema = typeof schema === "string" ? parseCommandSchema(schema) : schema;
|
||||||
schema = typeof schema === 'string' ? parseCommandSchema(schema) : schema;
|
|
||||||
return { schema, hintText };
|
return { schema, hintText };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGameCommandRegistry<TState extends Record<string, unknown> = {} >() {
|
export function createGameCommandRegistry<
|
||||||
|
TState extends Record<string, unknown> = {},
|
||||||
|
>() {
|
||||||
return createCommandRegistry<IGameContext<TState>>();
|
return createCommandRegistry<IGameContext<TState>>();
|
||||||
}
|
}
|
||||||
|
|
@ -2,39 +2,37 @@
|
||||||
# type: 'item' = inventory item card, 'status' = status effect card
|
# type: 'item' = inventory item card, 'status' = status effect card
|
||||||
# costType: 'energy' = costs energy per turn, 'uses' = limited uses, 'none' = free
|
# costType: 'energy' = costs energy per turn, 'uses' = limited uses, 'none' = free
|
||||||
# targetType: 'single' = target one enemy, 'none' = no target
|
# targetType: 'single' = target one enemy, 'none' = no target
|
||||||
# onPlay: effects triggered when card is played
|
# effects := ~cardEffect(card)
|
||||||
# onDraw: effects triggered when card enters hand
|
|
||||||
# onDiscard: effects triggered when card is discarded
|
|
||||||
|
|
||||||
id,name,desc,type,costType,costCount,targetType,effects
|
id,name,desc,type,costType,costCount,targetType
|
||||||
string,string,string,'item'|'status','energy'|'uses'|'none',int,'single'|'none',['onPlay'|'onDraw'|'onDiscard';'self'|'target'|'all'|'random';@effect;number][]
|
string,string,string,'item'|'status','energy'|'uses'|'none',int,'single'|'none'
|
||||||
sword,剑,【攻击2】【攻击2】,item,energy,1,single,[onPlay;target;attack;2];[onPlay;target;attack;2]
|
sword,剑,【攻击2】【攻击2】,item,energy,1,single
|
||||||
greataxe,长斧,对全体【攻击5】,item,energy,2,none,[onPlay;all;attack;5]
|
greataxe,长斧,对全体【攻击5】,item,energy,2,none
|
||||||
spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,single,[onPlay;target;attack;2];[onPlay;target;attack;2];[onPlay;target;attack;2]
|
spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,single
|
||||||
dagger,短刀,【攻击3】【攻击3】,item,energy,1,single,[onPlay;target;attack;3];[onPlay;target;attack;3]
|
dagger,短刀,【攻击3】【攻击3】,item,energy,1,single
|
||||||
dart,飞镖,【攻击1】抓一张牌,item,energy,0,single,[onPlay;target;attack;1];[onPlay;self;draw;1]
|
dart,飞镖,【攻击1】抓一张牌,item,energy,0,single
|
||||||
crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,single,[onPlay;target;attack;6];[onPlay;self;crossbow;0]
|
crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,single
|
||||||
shield,盾,【防御3】,item,energy,1,none,[onPlay;self;defend;3]
|
shield,盾,【防御3】,item,energy,1,none
|
||||||
hat,斗笠,【防御8】,item,energy,2,none,[onPlay;self;defend;8]
|
hat,斗笠,【防御8】,item,energy,2,none
|
||||||
cape,披风,【防御2】下回合【防御2】,item,energy,1,none,[onPlay;self;defend;2];[onPlay;self;defendNext;2]
|
cape,披风,【防御2】下回合【防御2】,item,energy,1,none
|
||||||
bracer,护腕,【防御1】抓1张牌,item,energy,0,none,[onPlay;self;defend;1];[onPlay;self;draw;1]
|
bracer,护腕,【防御1】抓1张牌,item,energy,0,none
|
||||||
greatshield,大盾,【防御5】,item,energy,1,none,[onPlay;self;defend;5]
|
greatshield,大盾,【防御5】,item,energy,1,none
|
||||||
chainmail,锁子甲,本回合受到伤害-3,item,energy,1,none,[onPlay;self;damageReduce;3]
|
chainmail,锁子甲,本回合受到伤害-3,item,energy,1,none
|
||||||
bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,none,[onPlay;self;removeWound;1]
|
bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,none
|
||||||
poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,none,[onPlay;self;attackBuff;2]
|
poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,none
|
||||||
fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,none,[onPlay;self;defendBuff;2]
|
fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,none
|
||||||
vitalityPotion,活力药剂,获得1点能量,item,uses,3,none,[onPlay;self;gainEnergy;1]
|
vitalityPotion,活力药剂,获得1点能量,item,uses,3,none
|
||||||
focusPotion,集中药剂,抓2张牌,item,uses,3,none,[onPlay;self;draw;2]
|
focusPotion,集中药剂,抓2张牌,item,uses,3,none
|
||||||
healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,none,[onPlay;self;removeWound;3]
|
healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,none
|
||||||
waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,none,[onPlay;self;energyNext;1];[onPlay;self;drawNext;2]
|
waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,none
|
||||||
rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,none,[onPlay;self;defendBuffUntilPlay;2]
|
rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,none
|
||||||
belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,none,[onPlay;self;drawChoice;1]
|
belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,none
|
||||||
torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,none,[onPlay;self;burnForEnergy;1]
|
torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,none
|
||||||
whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,none,[onPlay;self;attackBuffUntilPlay;3]
|
whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,none
|
||||||
blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,none,[onPlay;self;transformRandom;1]
|
blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,none
|
||||||
wound,伤口,无效果占用手牌和牌堆,status,none,0,none,
|
wound,伤口,无效果占用手牌和牌堆,status,none,0,none
|
||||||
venom,蛇毒,弃掉时受到3点伤害,status,none,0,none,[onDiscard;self;attack;3]
|
venom,蛇毒,弃掉时受到3点伤害,status,none,0,none
|
||||||
curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,none,[onDraw;self;curse;1]
|
curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,none
|
||||||
static,静电,在手里时受电击伤害+1,status,none,0,none,[onDraw;self;static;1]
|
static,静电,在手里时受电击伤害+1,status,none,0,none
|
||||||
fatigue,疲劳,占用手牌,status,none,0,none,
|
fatigue,疲劳,占用手牌,status,none,0,none
|
||||||
vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,none,[onDraw;self;expose;3]
|
vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,none
|
||||||
|
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Effect } from './effect.csv';
|
import type { CardEffect } from './cardEffect.csv';
|
||||||
|
|
||||||
type CardTable = readonly {
|
type CardTable = readonly {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
@ -8,7 +8,7 @@ type CardTable = readonly {
|
||||||
readonly costType: "energy" | "uses" | "none";
|
readonly costType: "energy" | "uses" | "none";
|
||||||
readonly costCount: number;
|
readonly costCount: number;
|
||||||
readonly targetType: "single" | "none";
|
readonly targetType: "single" | "none";
|
||||||
readonly effects: ["onPlay" | "onDraw" | "onDiscard", "self" | "target" | "all" | "random", Effect, number][];
|
readonly effects: CardEffect[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
export type Card = CardTable[number];
|
export type Card = CardTable[number];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
id,card,trigger,target,effects
|
||||||
|
string,@card,'onPlay'|'onDraw'|'onDiscard','self'|'target'|'all'|'random',[@effect;number][]
|
||||||
|
sword,sword,onPlay,target,[attack;2];[attack;2]
|
||||||
|
greataxe,greataxe,onPlay,all,[attack;5]
|
||||||
|
spear,spear,onPlay,target,[attack;2];[attack;2];[attack;2]
|
||||||
|
dagger,dagger,onPlay,target,[attack;3];[attack;3]
|
||||||
|
dart,dart,onPlay,target,[attack;1]
|
||||||
|
dart-draw,dart,onPlay,self,[draw;1]
|
||||||
|
crossbow,crossbow,onPlay,target,[attack;6]
|
||||||
|
crossbow-combo,crossbow,onPlay,self,[crossbow;0]
|
||||||
|
shield,shield,onPlay,self,[defend;3]
|
||||||
|
hat,hat,onPlay,self,[defend;8]
|
||||||
|
cape,cape,onPlay,self,[defend;2];[defendNext;2]
|
||||||
|
bracer,bracer,onPlay,self,[defend;1];[draw;1]
|
||||||
|
greatshield,greatshield,onPlay,self,[defend;5]
|
||||||
|
chainmail,chainmail,onPlay,self,[damageReduce;3]
|
||||||
|
bandage,bandage,onPlay,self,[removeWound;1]
|
||||||
|
poisonPotion,poisonPotion,onPlay,self,[attackBuff;2]
|
||||||
|
fortifyPotion,fortifyPotion,onPlay,self,[defendBuff;2]
|
||||||
|
vitalityPotion,vitalityPotion,onPlay,self,[gainEnergy;1]
|
||||||
|
focusPotion,focusPotion,onPlay,self,[draw;2]
|
||||||
|
healingPotion,healingPotion,onPlay,self,[removeWound;3]
|
||||||
|
waterBag,waterBag,onPlay,self,[energyNext;1];[drawNext;2]
|
||||||
|
rope,rope,onPlay,self,[defendBuffUntilPlay;2]
|
||||||
|
belt,belt,onPlay,self,[drawChoice;1]
|
||||||
|
torch,torch,onPlay,self,[burnForEnergy;1]
|
||||||
|
whetstone,whetstone,onPlay,self,[attackBuffUntilPlay;3]
|
||||||
|
blacksmithHammer,blacksmithHammer,onPlay,self,[transformRandom;1]
|
||||||
|
venom,venom,onDiscard,self,[attack;3]
|
||||||
|
curse,curse,onDraw,self,[curse;1]
|
||||||
|
static,static,onDraw,self,[static;1]
|
||||||
|
vultureEye,vultureEye,onDraw,self,[expose;3]
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import type { Card } from './card.csv';
|
||||||
|
import type { Effect } from './effect.csv';
|
||||||
|
|
||||||
|
type CardEffectTable = readonly {
|
||||||
|
readonly id: string;
|
||||||
|
readonly card: Card;
|
||||||
|
readonly trigger: "onPlay" | "onDraw" | "onDiscard";
|
||||||
|
readonly target: "self" | "target" | "all" | "random";
|
||||||
|
readonly effects: [Effect, number][];
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export type CardEffect = CardEffectTable[number];
|
||||||
|
|
||||||
|
declare function getData(): CardEffectTable;
|
||||||
|
export default getData;
|
||||||
|
|
@ -9,9 +9,14 @@ export function addCardEventTriggers(triggers: Triggers) {
|
||||||
const effects = getEffects();
|
const effects = getEffects();
|
||||||
|
|
||||||
function findEffect(id: string): EffectData {
|
function findEffect(id: string): EffectData {
|
||||||
const found = effects.find(e => e.id === id);
|
const found = effects.find((e) => e.id === id);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
return { id, name: id, description: "", lifecycle: "instant" } as EffectData;
|
return {
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
description: "",
|
||||||
|
lifecycle: "instant",
|
||||||
|
} as EffectData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// storm: give static card to player when storm enemy attacks
|
// storm: give static card to player when storm enemy attacks
|
||||||
|
|
@ -62,14 +67,17 @@ export function addCardEventTriggers(triggers: Triggers) {
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const playedItemId = card.itemId;
|
const playedItemId = card.itemId;
|
||||||
|
|
||||||
const adjacent = getAdjacentItems<GameItemMeta>(ctx.game.value.inventory, playedItemId);
|
const adjacent = getAdjacentItems<GameItemMeta>(
|
||||||
|
ctx.game.value.inventory,
|
||||||
|
playedItemId,
|
||||||
|
);
|
||||||
for (const [adjItemId] of adjacent) {
|
for (const [adjItemId] of adjacent) {
|
||||||
const adjEffects = ctx.game.value.player.itemEffects[adjItemId];
|
const adjEffects = ctx.game.value.player.itemEffects[adjItemId];
|
||||||
if (!adjEffects) continue;
|
if (!adjEffects) continue;
|
||||||
const burn = adjEffects.burnForEnergy;
|
const burn = adjEffects.burnForEnergy;
|
||||||
if (!burn || burn.stacks <= 0) continue;
|
if (!burn || burn.stacks <= 0) continue;
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const item = draft.inventory.items.get(adjItemId);
|
const item = draft.inventory.items.get(adjItemId);
|
||||||
if (item) {
|
if (item) {
|
||||||
draft.inventory.items.delete(adjItemId);
|
draft.inventory.items.delete(adjItemId);
|
||||||
|
|
@ -89,12 +97,12 @@ export function addCardEventTriggers(triggers: Triggers) {
|
||||||
if (!card || card.cardData.id !== "fatigue") return;
|
if (!card || card.cardData.id !== "fatigue") return;
|
||||||
|
|
||||||
const sandwormKing = ctx.game.value.enemies.find(
|
const sandwormKing = ctx.game.value.enemies.find(
|
||||||
e => e.enemy.id === "沙虫王" && e.isAlive
|
(e) => e.enemy.id === "沙虫王" && e.isAlive,
|
||||||
);
|
);
|
||||||
if (!sandwormKing) return;
|
if (!sandwormKing) return;
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const king = draft.enemies.find(e => e.id === sandwormKing.id);
|
const king = draft.enemies.find((e) => e.id === sandwormKing.id);
|
||||||
if (king) {
|
if (king) {
|
||||||
king.hp = Math.min(king.hp + 10, king.maxHp);
|
king.hp = Math.min(king.hp + 10, king.maxHp);
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +117,8 @@ export function addCardEventTriggers(triggers: Triggers) {
|
||||||
if (dealt <= 0 || !ctx.sourceEntityKey) return;
|
if (dealt <= 0 || !ctx.sourceEntityKey) return;
|
||||||
|
|
||||||
const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey);
|
const attacker = getCombatEntity(ctx.game.value, ctx.sourceEntityKey);
|
||||||
if (!attacker || !("enemy" in attacker) || attacker.enemy.id !== "秃鹫") return;
|
if (!attacker || !("enemy" in attacker) || attacker.enemy.id !== "秃鹫")
|
||||||
|
return;
|
||||||
|
|
||||||
await triggers.onEffectApplied.execute(ctx.game, {
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
effect: findEffect("vultureEye"),
|
effect: findEffect("vultureEye"),
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,15 @@ import {CombatGameContext} from "./types";
|
||||||
import {
|
import {
|
||||||
addEntityEffect,
|
addEntityEffect,
|
||||||
addItemEffect,
|
addItemEffect,
|
||||||
getAliveEnemies, onEntityPostureDamage,
|
getAliveEnemies,
|
||||||
|
onEntityPostureDamage,
|
||||||
onEntityEffectUpkeep,
|
onEntityEffectUpkeep,
|
||||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost, getCombatEntity, getEffectTargets
|
onPlayerItemEffectUpkeep,
|
||||||
|
onItemDiscard,
|
||||||
|
onItemPlay,
|
||||||
|
payCardCost,
|
||||||
|
getCombatEntity,
|
||||||
|
getEffectTargets,
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
import { promptMainAction } from "@/samples/slay-the-spire-like/system/combat/prompts";
|
import { promptMainAction } from "@/samples/slay-the-spire-like/system/combat/prompts";
|
||||||
import { moveToRegion, shuffle } from "@/core/region";
|
import { moveToRegion, shuffle } from "@/core/region";
|
||||||
|
|
@ -14,68 +20,99 @@ import {getAdjacentItems} from "@/samples/slay-the-spire-like/system/grid-invent
|
||||||
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
||||||
|
|
||||||
type TriggerTypes = {
|
type TriggerTypes = {
|
||||||
onCombatStart: {},
|
onCombatStart: {};
|
||||||
onTurnStart: { entityKey: "player" | string, },
|
onTurnStart: { entityKey: "player" | string };
|
||||||
onTurnEnd: { entityKey: "player" | string, },
|
onTurnEnd: { entityKey: "player" | string };
|
||||||
onShuffle: {},
|
onShuffle: {};
|
||||||
onCardPlayed: { cardId: string, targetId?: string, sourceEntityKey?: "player" | string },
|
onCardPlayed: {
|
||||||
onCardDiscarded: { cardId: string, sourceEntityKey?: "player" | string },
|
cardId: string;
|
||||||
onCardDrawn: { cardId: string, sourceEntityKey?: "player" | string },
|
targetId?: string;
|
||||||
onDraw: {count: number},
|
sourceEntityKey?: "player" | string;
|
||||||
onEffectApplied: { effect: EffectData, entityKey: "player" | string, stacks: number, cardId?: string, sourceEntityKey?: "player" | string, targetId?: string },
|
};
|
||||||
onHpChange: { entityKey: "player" | string, amount: number},
|
onCardDiscarded: { cardId: string; sourceEntityKey?: "player" | string };
|
||||||
onDamage: { entityKey: "player" | string, amount: number, prevented?: number, sourceEntityKey?: "player" | string},
|
onCardDrawn: { cardId: string; sourceEntityKey?: "player" | string };
|
||||||
onEnemyIntent: { enemyId: string, sourceEntityKey?: "player" | string },
|
onDraw: { count: number };
|
||||||
onIntentUpdate: { enemyId: string },
|
onEffectApplied: {
|
||||||
}
|
effect: EffectData;
|
||||||
|
entityKey: "player" | string;
|
||||||
|
stacks: number;
|
||||||
|
cardId?: string;
|
||||||
|
sourceEntityKey?: "player" | string;
|
||||||
|
targetId?: string;
|
||||||
|
};
|
||||||
|
onHpChange: { entityKey: "player" | string; amount: number };
|
||||||
|
onDamage: {
|
||||||
|
entityKey: "player" | string;
|
||||||
|
amount: number;
|
||||||
|
prevented?: number;
|
||||||
|
sourceEntityKey?: "player" | string;
|
||||||
|
};
|
||||||
|
onEnemyIntent: { enemyId: string; sourceEntityKey?: "player" | string };
|
||||||
|
onIntentUpdate: { enemyId: string };
|
||||||
|
};
|
||||||
|
|
||||||
function createTriggers(){
|
export function createTriggers() {
|
||||||
const triggers = {
|
const triggers = {
|
||||||
onCombatStart: createTrigger("onCombatStart"),
|
onCombatStart: createTrigger("onCombatStart"),
|
||||||
onTurnStart: createTrigger("onTurnStart", async ctx => {
|
onTurnStart: createTrigger("onTurnStart", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const entity = getCombatEntity(draft, ctx.entityKey);
|
const entity = getCombatEntity(draft, ctx.entityKey);
|
||||||
if (entity) onEntityEffectUpkeep(entity);
|
if (entity) onEntityEffectUpkeep(entity);
|
||||||
if(entity === draft.player)
|
if (entity === draft.player) onPlayerItemEffectUpkeep(draft.player);
|
||||||
onPlayerItemEffectUpkeep(draft.player);
|
});
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
onTurnEnd: createTrigger("onTurnEnd", async ctx => {
|
onTurnEnd: createTrigger("onTurnEnd", async (ctx) => {
|
||||||
if (ctx.entityKey !== "player") return;
|
if (ctx.entityKey !== "player") return;
|
||||||
const { regions } = ctx.game.value.player.deck;
|
const { regions } = ctx.game.value.player.deck;
|
||||||
for (const cardId of Object.values(regions.hand.childIds)) {
|
for (const cardId of Object.values(regions.hand.childIds)) {
|
||||||
await triggers.onCardDiscarded.execute(ctx.game, { cardId });
|
await triggers.onCardDiscarded.execute(ctx.game, { cardId });
|
||||||
}
|
}
|
||||||
await ctx.game.produceAsync(draft => draft.player.energy = draft.player.maxEnergy);
|
await ctx.game.produceAsync(
|
||||||
|
(draft) => (draft.player.energy = draft.player.maxEnergy),
|
||||||
|
);
|
||||||
await triggers.onDraw.execute(ctx.game, { count: 5 });
|
await triggers.onDraw.execute(ctx.game, { count: 5 });
|
||||||
}),
|
}),
|
||||||
onShuffle: createTrigger("onShuffle", async ctx => {
|
onShuffle: createTrigger("onShuffle", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const { cards, regions } = draft.player.deck;
|
const { cards, regions } = draft.player.deck;
|
||||||
for (const cardId of Object.values(regions.discardPile.childIds))
|
for (const cardId of Object.values(regions.discardPile.childIds))
|
||||||
moveToRegion(cards[cardId], regions.discardPile, regions.drawPile);
|
moveToRegion(cards[cardId], regions.discardPile, regions.drawPile);
|
||||||
shuffle(regions.drawPile, cards, ctx.game.rng);
|
shuffle(regions.drawPile, cards, ctx.game.rng);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
onCardPlayed: createTrigger("onCardPlayed", async ctx => {
|
onCardPlayed: createTrigger("onCardPlayed", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const { cards, regions } = draft.player.deck;
|
const { cards, regions } = draft.player.deck;
|
||||||
const card = cards[ctx.cardId];
|
const card = cards[ctx.cardId];
|
||||||
payCardCost(draft.player, card.cardData.costType, card.cardData.costCount, card.itemId, draft.inventory);
|
payCardCost(
|
||||||
|
draft.player,
|
||||||
|
card.cardData.costType,
|
||||||
|
card.cardData.costCount,
|
||||||
|
card.itemId,
|
||||||
|
draft.inventory,
|
||||||
|
);
|
||||||
moveToRegion(card, regions.hand, regions.discardPile);
|
moveToRegion(card, regions.hand, regions.discardPile);
|
||||||
onItemPlay(draft.player, card.itemId);
|
onItemPlay(draft.player, card.itemId);
|
||||||
});
|
});
|
||||||
const { cards, regions } = ctx.game.value.player.deck;
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
const card = cards[ctx.cardId];
|
const card = cards[ctx.cardId];
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
if(trigger !== 'onPlay') continue;
|
if (trigger !== "onPlay") continue;
|
||||||
|
for (const [effect, stacks] of effects)
|
||||||
for (const entity of getEffectTargets(target, ctx.game, ctx.targetId))
|
for (const entity of getEffectTargets(target, ctx.game, ctx.targetId))
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source, targetId: ctx.targetId});
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
cardId: ctx.cardId,
|
||||||
|
sourceEntityKey: source,
|
||||||
|
targetId: ctx.targetId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
|
onCardDiscarded: createTrigger("onCardDiscarded", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const { cards, regions } = draft.player.deck;
|
const { cards, regions } = draft.player.deck;
|
||||||
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||||
onItemDiscard(draft.player, cards[ctx.cardId].itemId);
|
onItemDiscard(draft.player, cards[ctx.cardId].itemId);
|
||||||
|
|
@ -83,30 +120,45 @@ function createTriggers(){
|
||||||
const { cards, regions } = ctx.game.value.player.deck;
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
const card = cards[ctx.cardId];
|
const card = cards[ctx.cardId];
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
if(trigger !== 'onDiscard') continue;
|
if (trigger !== "onDiscard") continue;
|
||||||
|
for (const [effect, stacks] of effects)
|
||||||
for (const entity of getEffectTargets(target, ctx.game))
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
cardId: ctx.cardId,
|
||||||
|
sourceEntityKey: source,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onCardDrawn: createTrigger("onCardDrawn", async ctx => {
|
onCardDrawn: createTrigger("onCardDrawn", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const { cards, regions } = draft.player.deck;
|
const { cards, regions } = draft.player.deck;
|
||||||
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
||||||
});
|
});
|
||||||
const { cards, regions } = ctx.game.value.player.deck;
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
const card = cards[ctx.cardId];
|
const card = cards[ctx.cardId];
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
if(trigger !== 'onDraw') continue;
|
if (trigger !== "onDraw") continue;
|
||||||
|
for (const [effect, stacks] of effects)
|
||||||
for (const entity of getEffectTargets(target, ctx.game))
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
cardId: ctx.cardId,
|
||||||
|
sourceEntityKey: source,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onDraw: createTrigger("onDraw", async ctx => {
|
onDraw: createTrigger("onDraw", async (ctx) => {
|
||||||
let toDraw = ctx.count;
|
let toDraw = ctx.count;
|
||||||
while (toDraw > 0) {
|
while (toDraw > 0) {
|
||||||
let inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
let inDraw =
|
||||||
|
ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
if (inDraw <= 0) await triggers.onShuffle.execute(ctx.game, {});
|
if (inDraw <= 0) await triggers.onShuffle.execute(ctx.game, {});
|
||||||
|
|
||||||
inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
|
|
@ -118,15 +170,18 @@ function createTriggers(){
|
||||||
toDraw--;
|
toDraw--;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onEffectApplied: createTrigger("onEffectApplied", async ctx => {
|
onEffectApplied: createTrigger("onEffectApplied", async (ctx) => {
|
||||||
if(ctx.effect.lifecycle === 'instant') return;
|
if (ctx.effect.lifecycle === "instant") return;
|
||||||
|
|
||||||
if (ctx.effect.lifecycle.startsWith("item")) {
|
if (ctx.effect.lifecycle.startsWith("item")) {
|
||||||
if (ctx.cardId) {
|
if (ctx.cardId) {
|
||||||
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
||||||
const nearby = getAdjacentItems<GameItemMeta>(ctx.game.value.inventory, card.itemId);
|
const nearby = getAdjacentItems<GameItemMeta>(
|
||||||
|
ctx.game.value.inventory,
|
||||||
|
card.itemId,
|
||||||
|
);
|
||||||
for (const itemId of nearby.keys()) {
|
for (const itemId of nearby.keys()) {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -134,32 +189,51 @@ function createTriggers(){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? draft.player
|
||||||
|
: draft.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
if (entity) addEntityEffect(entity, ctx.effect, ctx.stacks);
|
if (entity) addEntityEffect(entity, ctx.effect, ctx.stacks);
|
||||||
})
|
});
|
||||||
}),
|
}),
|
||||||
onHpChange: createTrigger("onHpChange", async ctx => {
|
onHpChange: createTrigger("onHpChange", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? draft.player
|
||||||
|
: draft.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
if (!entity) return;
|
if (!entity) return;
|
||||||
entity.hp += ctx.amount;
|
entity.hp += ctx.amount;
|
||||||
entity.isAlive = entity.hp > 0;
|
entity.isAlive = entity.hp > 0;
|
||||||
draft.result = !draft.player.isAlive ? "defeat" : draft.enemies.every(e => !e.isAlive) ? "victory" : null;
|
draft.result = !draft.player.isAlive
|
||||||
|
? "defeat"
|
||||||
|
: draft.enemies.every((e) => !e.isAlive)
|
||||||
|
? "victory"
|
||||||
|
: null;
|
||||||
});
|
});
|
||||||
if (ctx.game.value.result) throw ctx.game.value;
|
if (ctx.game.value.result) throw ctx.game.value;
|
||||||
}),
|
}),
|
||||||
onDamage: createTrigger("onDamage", async ctx => {
|
onDamage: createTrigger("onDamage", async (ctx) => {
|
||||||
const entity = ctx.entityKey === "player" ? ctx.game.value.player : ctx.game.value.enemies.find(e => e.id === ctx.entityKey);
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? ctx.game.value.player
|
||||||
|
: ctx.game.value.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
if (!entity || !entity.isAlive) return;
|
if (!entity || !entity.isAlive) return;
|
||||||
const dealt = Math.min(Math.max(0,entity.hp), ctx.amount - (ctx.prevented || 0));
|
const dealt = Math.min(
|
||||||
await ctx.game.produceAsync(draft => {
|
Math.max(0, entity.hp),
|
||||||
|
ctx.amount - (ctx.prevented || 0),
|
||||||
|
);
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
onEntityPostureDamage(entity, dealt);
|
onEntityPostureDamage(entity, dealt);
|
||||||
});
|
});
|
||||||
await triggers.onHpChange.execute(ctx.game,{entityKey: ctx.entityKey, amount: -dealt});
|
await triggers.onHpChange.execute(ctx.game, {
|
||||||
|
entityKey: ctx.entityKey,
|
||||||
|
amount: -dealt,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
onEnemyIntent: createTrigger("onEnemyIntent", async ctx => {
|
onEnemyIntent: createTrigger("onEnemyIntent", async (ctx) => {
|
||||||
const enemy = ctx.game.value.enemies.find(e => e.id === ctx.enemyId);
|
const enemy = ctx.game.value.enemies.find((e) => e.id === ctx.enemyId);
|
||||||
if (!enemy || !enemy.isAlive) return;
|
if (!enemy || !enemy.isAlive) return;
|
||||||
|
|
||||||
const intent = enemy.currentIntent;
|
const intent = enemy.currentIntent;
|
||||||
|
|
@ -168,12 +242,17 @@ function createTriggers(){
|
||||||
const source = ctx.sourceEntityKey ?? enemy.id;
|
const source = ctx.sourceEntityKey ?? enemy.id;
|
||||||
for (const [target, effect, stacks] of intent.effects) {
|
for (const [target, effect, stacks] of intent.effects) {
|
||||||
for (const entity of getEffectTargets(target, ctx.game))
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, sourceEntityKey: source });
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
sourceEntityKey: source,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onIntentUpdate: createTrigger("onIntentUpdate", async ctx => {
|
onIntentUpdate: createTrigger("onIntentUpdate", async (ctx) => {
|
||||||
await ctx.game.produceAsync(draft => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const enemy = draft.enemies.find(e => e.id === ctx.enemyId);
|
const enemy = draft.enemies.find((e) => e.id === ctx.enemyId);
|
||||||
if (!enemy) return;
|
if (!enemy) return;
|
||||||
|
|
||||||
const intent = enemy.currentIntent;
|
const intent = enemy.currentIntent;
|
||||||
|
|
@ -186,10 +265,10 @@ function createTriggers(){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
return triggers;
|
return triggers;
|
||||||
}
|
}
|
||||||
export type Triggers = ReturnType<typeof createTriggers>
|
export type Triggers = ReturnType<typeof createTriggers>;
|
||||||
export function createStartWith(build: (triggers: Triggers) => void) {
|
export function createStartWith(build: (triggers: Triggers) => void) {
|
||||||
const triggers = createTriggers();
|
const triggers = createTriggers();
|
||||||
build(triggers);
|
build(triggers);
|
||||||
|
|
@ -223,12 +302,20 @@ export function createStartWith(build: (triggers: Triggers) => void){
|
||||||
if (e === game.value) return game.value.result;
|
if (e === game.value) return game.value.result;
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type TriggerContext<TKey extends keyof TriggerTypes> = TriggerTypes[TKey] & { event: TKey, game: CombatGameContext };
|
type TriggerContext<TKey extends keyof TriggerTypes> = TriggerTypes[TKey] & {
|
||||||
function createTrigger<TKey extends keyof TriggerTypes>(event: TKey, fallback?: (ctx: TriggerContext<TKey>) => Promise<void>) {
|
event: TKey;
|
||||||
const {use, execute} = createMiddlewareChain<TriggerContext<TKey>,void>(fallback);
|
game: CombatGameContext;
|
||||||
|
};
|
||||||
|
function createTrigger<TKey extends keyof TriggerTypes>(
|
||||||
|
event: TKey,
|
||||||
|
fallback?: (ctx: TriggerContext<TKey>) => Promise<void>,
|
||||||
|
) {
|
||||||
|
const { use, execute } = createMiddlewareChain<TriggerContext<TKey>, void>(
|
||||||
|
fallback,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
use,
|
use,
|
||||||
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
|
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
|
||||||
|
|
@ -236,5 +323,5 @@ function createTrigger<TKey extends keyof TriggerTypes>(event: TKey, fallback?:
|
||||||
await execute(param);
|
await execute(param);
|
||||||
return param;
|
return param;
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import type { PlayerDeck } from "../deck/types";
|
import type { PlayerDeck } from "../deck/types";
|
||||||
import {EnemyData, IntentData} from "@/samples/slay-the-spire-like/system/types";
|
import {
|
||||||
|
EnemyData,
|
||||||
|
IntentData,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
import { GridInventory } from "@/samples/slay-the-spire-like/system/grid-inventory";
|
import { GridInventory } from "@/samples/slay-the-spire-like/system/grid-inventory";
|
||||||
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
||||||
|
|
||||||
export type EffectTable = Record<string, {data: EffectData, stacks: number}>;
|
export type EffectTable = Record<string, { data: EffectData; stacks: number }>;
|
||||||
|
|
||||||
export type CombatEntity = {
|
export type CombatEntity = {
|
||||||
id: string; // player is just "player"
|
id: string; // player is just "player"
|
||||||
|
|
@ -19,7 +22,7 @@ export type PlayerEntity = CombatEntity & {
|
||||||
maxEnergy: number;
|
maxEnergy: number;
|
||||||
deck: PlayerDeck;
|
deck: PlayerDeck;
|
||||||
itemEffects: Record<string, EffectTable>;
|
itemEffects: Record<string, EffectTable>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type EnemyEntity = CombatEntity & {
|
export type EnemyEntity = CombatEntity & {
|
||||||
enemy: EnemyData;
|
enemy: EnemyData;
|
||||||
|
|
@ -30,11 +33,13 @@ export type EnemyEntity = CombatEntity & {
|
||||||
export type CombatPhase = "playerTurn" | "enemyTurn" | "combatEnd";
|
export type CombatPhase = "playerTurn" | "enemyTurn" | "combatEnd";
|
||||||
export type CombatResult = "victory" | "defeat";
|
export type CombatResult = "victory" | "defeat";
|
||||||
|
|
||||||
export type LootEntry = {
|
export type LootEntry =
|
||||||
|
| {
|
||||||
type: "gold";
|
type: "gold";
|
||||||
amount: number;
|
amount: number;
|
||||||
} | {
|
}
|
||||||
type: "item",
|
| {
|
||||||
|
type: "item";
|
||||||
itemId: string;
|
itemId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -50,4 +55,5 @@ export type CombatState = {
|
||||||
loot: LootEntry[];
|
loot: LootEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CombatGameContext = import("@/core/game").IGameContext<CombatState>;
|
export type CombatGameContext =
|
||||||
|
import("@/core/game").IGameContextExport<CombatState>;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,17 @@ export type EffectData = {
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly lifecycle: EffectLifecycle;
|
readonly lifecycle: EffectLifecycle;
|
||||||
};
|
};
|
||||||
export type EffectLifecycle = "instant" | "temporary" | "lingering" | "permanent" | "posture" | "item" | "itemTemporary" | "itemUntilPlay" | "itemUntilDiscard" | "itemPermanent";
|
export type EffectLifecycle =
|
||||||
|
| "instant"
|
||||||
|
| "temporary"
|
||||||
|
| "lingering"
|
||||||
|
| "permanent"
|
||||||
|
| "posture"
|
||||||
|
| "item"
|
||||||
|
| "itemTemporary"
|
||||||
|
| "itemUntilPlay"
|
||||||
|
| "itemUntilDiscard"
|
||||||
|
| "itemPermanent";
|
||||||
|
|
||||||
export type EnemyData = {
|
export type EnemyData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
|
@ -18,6 +28,16 @@ export type CardCostType = "energy" | "uses" | "none";
|
||||||
export type CardTargetType = "single" | "none";
|
export type CardTargetType = "single" | "none";
|
||||||
export type EffectTarget = "self" | "player" | "team";
|
export type EffectTarget = "self" | "player" | "team";
|
||||||
|
|
||||||
|
export type CardEffectTrigger = "onPlay" | "onDraw" | "onDiscard";
|
||||||
|
export type CardEffectTarget = "self" | "target" | "all" | "random";
|
||||||
|
|
||||||
|
export type CardEffect = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly trigger: CardEffectTrigger;
|
||||||
|
readonly target: CardEffectTarget;
|
||||||
|
readonly effects: readonly [EffectData, number][];
|
||||||
|
};
|
||||||
|
|
||||||
export type CardData = {
|
export type CardData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
|
@ -26,18 +46,26 @@ export type CardData = {
|
||||||
readonly costType: CardCostType;
|
readonly costType: CardCostType;
|
||||||
readonly costCount: number;
|
readonly costCount: number;
|
||||||
readonly targetType: CardTargetType;
|
readonly targetType: CardTargetType;
|
||||||
readonly effects: readonly [CardEffectTrigger, CardEffectTarget, EffectData, number][];
|
readonly effects: readonly CardEffect[];
|
||||||
};
|
};
|
||||||
export type CardEffectTrigger = "onPlay" | "onDraw" | "onDiscard";
|
|
||||||
export type CardEffectTarget = "self" | "target" | "all" | "random"
|
|
||||||
|
|
||||||
export type EncounterType = "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
export type EncounterType =
|
||||||
|
| "minion"
|
||||||
|
| "elite"
|
||||||
|
| "event"
|
||||||
|
| "shop"
|
||||||
|
| "camp"
|
||||||
|
| "curio";
|
||||||
export type EncounterData = {
|
export type EncounterData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly type: EncounterType;
|
readonly type: EncounterType;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
readonly enemies: readonly [data: EnemyData, hp: number, effects: [EffectData, stacks: number][]][];
|
readonly enemies: readonly [
|
||||||
|
data: EnemyData,
|
||||||
|
hp: number,
|
||||||
|
effects: [EffectData, stacks: number][],
|
||||||
|
][];
|
||||||
readonly dialogue: string;
|
readonly dialogue: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
addEffect,
|
addEffect,
|
||||||
addEntityEffect,
|
addEntityEffect,
|
||||||
|
|
@ -12,38 +12,86 @@ import {
|
||||||
getCombatEntity,
|
getCombatEntity,
|
||||||
canPlayCard,
|
canPlayCard,
|
||||||
payCardCost,
|
payCardCost,
|
||||||
} from '@/samples/slay-the-spire-like/system/combat/effects';
|
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
import type { CombatEntity, CombatState, EffectTable, PlayerEntity, EnemyEntity } from '@/samples/slay-the-spire-like/system/combat/types';
|
import type {
|
||||||
import type { EffectData } from '@/samples/slay-the-spire-like/system/types';
|
CombatEntity,
|
||||||
import type { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/system/grid-inventory/types';
|
CombatState,
|
||||||
import type { GameItemMeta } from '@/samples/slay-the-spire-like/system/progress/types';
|
EffectTable,
|
||||||
import type { ParsedShape } from '@/samples/slay-the-spire-like/system/utils/parse-shape';
|
PlayerEntity,
|
||||||
import type { Transform2D } from '@/samples/slay-the-spire-like/system/utils/shape-collision';
|
EnemyEntity,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
|
import type { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
import type {
|
||||||
|
CellKey,
|
||||||
|
GridInventory,
|
||||||
|
InventoryItem,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
||||||
|
import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress/types";
|
||||||
|
import type { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
||||||
|
import type { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
||||||
|
|
||||||
function createEffect(id: string, lifecycle: EffectData['lifecycle']): EffectData {
|
function createEffect(
|
||||||
return { id, name: id, description: '', lifecycle };
|
id: string,
|
||||||
|
lifecycle: EffectData["lifecycle"],
|
||||||
|
): EffectData {
|
||||||
|
return { id, name: id, description: "", lifecycle };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCard(id: string, costType: 'energy' | 'uses' | 'none', costCount: number) {
|
function createCard(
|
||||||
return { id, name: id, desc: '', type: 'item' as const, costType, costCount, targetType: 'none' as const, effects: [] as const };
|
id: string,
|
||||||
|
costType: "energy" | "uses" | "none",
|
||||||
|
costCount: number,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
desc: "",
|
||||||
|
type: "item" as const,
|
||||||
|
costType,
|
||||||
|
costCount,
|
||||||
|
targetType: "none" as const,
|
||||||
|
effects: [] as const,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createItem(itemId: string, cardId: string, costType: 'energy' | 'uses' | 'none', costCount: number, depletion = 0): InventoryItem<GameItemMeta> {
|
function createItem(
|
||||||
|
itemId: string,
|
||||||
|
cardId: string,
|
||||||
|
costType: "energy" | "uses" | "none",
|
||||||
|
costCount: number,
|
||||||
|
depletion = 0,
|
||||||
|
): InventoryItem<GameItemMeta> {
|
||||||
return {
|
return {
|
||||||
id: itemId,
|
id: itemId,
|
||||||
shape: { id: '1x1', cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||||
transform: { x: 0, y: 0, rotation: 0, flipX: false, flipY: false } as unknown as Transform2D,
|
transform: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotation: 0,
|
||||||
|
flipX: false,
|
||||||
|
flipY: false,
|
||||||
|
} as unknown as Transform2D,
|
||||||
meta: {
|
meta: {
|
||||||
itemData: { id: itemId, type: 'weapon', name: itemId, shape: '1x1', card: createCard(cardId, costType, costCount), price: 0, description: '' },
|
itemData: {
|
||||||
shape: { id: '1x1', cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
id: itemId,
|
||||||
depletion: costType === 'uses' ? depletion : undefined,
|
type: "weapon",
|
||||||
|
name: itemId,
|
||||||
|
shape: "1x1",
|
||||||
|
card: createCard(cardId, costType, costCount),
|
||||||
|
price: 0,
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||||
|
depletion: costType === "uses" ? depletion : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInventory(items: InventoryItem<GameItemMeta>[]): GridInventory<GameItemMeta> {
|
function createInventory(
|
||||||
|
items: InventoryItem<GameItemMeta>[],
|
||||||
|
): GridInventory<GameItemMeta> {
|
||||||
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
||||||
const occupied = new Set<string>();
|
const occupied = new Set<CellKey>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
map.set(item.id, item);
|
map.set(item.id, item);
|
||||||
occupied.add(`${item.transform.x},${item.transform.y}`);
|
occupied.add(`${item.transform.x},${item.transform.y}`);
|
||||||
|
|
@ -65,7 +113,15 @@ function createPlayerEntity(hp = 30, maxHp = 30): PlayerEntity {
|
||||||
...createCombatEntity(hp, maxHp),
|
...createCombatEntity(hp, maxHp),
|
||||||
energy: 3,
|
energy: 3,
|
||||||
maxEnergy: 3,
|
maxEnergy: 3,
|
||||||
deck: { cards: {}, regions: { drawPile: { id: 'drawPile', axes: [], childIds: [], partMap: {} }, hand: { id: 'hand', axes: [], childIds: [], partMap: {} }, discardPile: { id: 'discardPile', axes: [], childIds: [], partMap: {} }, exhaustPile: { id: 'exhaustPile', axes: [], childIds: [], partMap: {} } } },
|
deck: {
|
||||||
|
cards: {},
|
||||||
|
regions: {
|
||||||
|
drawPile: { id: "drawPile", axes: [], childIds: [], partMap: {} },
|
||||||
|
hand: { id: "hand", axes: [], childIds: [], partMap: {} },
|
||||||
|
discardPile: { id: "discardPile", axes: [], childIds: [], partMap: {} },
|
||||||
|
exhaustPile: { id: "exhaustPile", axes: [], childIds: [], partMap: {} },
|
||||||
|
},
|
||||||
|
},
|
||||||
itemEffects: {},
|
itemEffects: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -74,334 +130,342 @@ function createEnemyEntity(id: string, hp = 10, maxHp = 10): EnemyEntity {
|
||||||
return {
|
return {
|
||||||
...createCombatEntity(hp, maxHp),
|
...createCombatEntity(hp, maxHp),
|
||||||
id,
|
id,
|
||||||
enemy: { id, name: id, description: '' },
|
enemy: { id, name: id, description: "" },
|
||||||
intents: {},
|
intents: {},
|
||||||
currentIntentId: '',
|
currentIntentId: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCombatState(playerHp = 30, enemies: EnemyEntity[] = []): CombatState {
|
function createCombatState(
|
||||||
|
playerHp = 30,
|
||||||
|
enemies: EnemyEntity[] = [],
|
||||||
|
): CombatState {
|
||||||
return {
|
return {
|
||||||
player: createPlayerEntity(playerHp),
|
player: createPlayerEntity(playerHp),
|
||||||
enemies,
|
enemies,
|
||||||
inventory: { width: 6, height: 4, items: new Map(), occupiedCells: new Set() },
|
inventory: {
|
||||||
phase: 'playerTurn',
|
width: 6,
|
||||||
|
height: 4,
|
||||||
|
items: new Map(),
|
||||||
|
occupiedCells: new Set(),
|
||||||
|
},
|
||||||
|
phase: "playerTurn",
|
||||||
turnNumber: 1,
|
turnNumber: 1,
|
||||||
result: null,
|
result: null,
|
||||||
loot: [],
|
loot: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('combat/effects', () => {
|
describe("combat/effects", () => {
|
||||||
describe('addEffect', () => {
|
describe("addEffect", () => {
|
||||||
it('should add a new effect to an empty table', () => {
|
it("should add a new effect to an empty table", () => {
|
||||||
const table: EffectTable = {};
|
const table: EffectTable = {};
|
||||||
const effect = createEffect('strength', 'temporary');
|
const effect = createEffect("strength", "temporary");
|
||||||
|
|
||||||
addEffect(table, effect, 3);
|
addEffect(table, effect, 3);
|
||||||
|
|
||||||
expect(table['strength']).toBeDefined();
|
expect(table["strength"]).toBeDefined();
|
||||||
expect(table['strength'].data).toBe(effect);
|
expect(table["strength"].data).toBe(effect);
|
||||||
expect(table['strength'].stacks).toBe(3);
|
expect(table["strength"].stacks).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stack with existing effect of same id', () => {
|
it("should stack with existing effect of same id", () => {
|
||||||
const table: EffectTable = {};
|
const table: EffectTable = {};
|
||||||
const effect = createEffect('strength', 'lingering');
|
const effect = createEffect("strength", "lingering");
|
||||||
|
|
||||||
addEffect(table, effect, 2);
|
addEffect(table, effect, 2);
|
||||||
addEffect(table, effect, 3);
|
addEffect(table, effect, 3);
|
||||||
|
|
||||||
expect(table['strength'].stacks).toBe(5);
|
expect(table["strength"].stacks).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove effect when stacks reach 0', () => {
|
it("should remove effect when stacks reach 0", () => {
|
||||||
const table: EffectTable = {};
|
const table: EffectTable = {};
|
||||||
const effect = createEffect('strength', 'temporary');
|
const effect = createEffect("strength", "temporary");
|
||||||
|
|
||||||
addEffect(table, effect, 3);
|
addEffect(table, effect, 3);
|
||||||
addEffect(table, effect, -3);
|
addEffect(table, effect, -3);
|
||||||
|
|
||||||
expect(table['strength']).toBeUndefined();
|
expect(table["strength"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not add effect when stacks is 0', () => {
|
it("should not add effect when stacks is 0", () => {
|
||||||
const table: EffectTable = {};
|
const table: EffectTable = {};
|
||||||
const effect = createEffect('strength', 'temporary');
|
const effect = createEffect("strength", "temporary");
|
||||||
|
|
||||||
addEffect(table, effect, 0);
|
addEffect(table, effect, 0);
|
||||||
|
|
||||||
expect(table['strength']).toBeUndefined();
|
expect(table["strength"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle negative stacks', () => {
|
it("should handle negative stacks", () => {
|
||||||
const table: EffectTable = {};
|
const table: EffectTable = {};
|
||||||
const effect = createEffect('weak', 'temporary');
|
const effect = createEffect("weak", "temporary");
|
||||||
|
|
||||||
addEffect(table, effect, -2);
|
addEffect(table, effect, -2);
|
||||||
|
|
||||||
expect(table['weak'].stacks).toBe(-2);
|
expect(table["weak"].stacks).toBe(-2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addEntityEffect', () => {
|
describe("addEntityEffect", () => {
|
||||||
it('should add effect to entity.effects', () => {
|
it("should add effect to entity.effects", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const effect = createEffect('vulnerable', 'lingering');
|
const effect = createEffect("vulnerable", "lingering");
|
||||||
|
|
||||||
addEntityEffect(entity, effect, 2);
|
addEntityEffect(entity, effect, 2);
|
||||||
|
|
||||||
expect(entity.effects['vulnerable'].stacks).toBe(2);
|
expect(entity.effects["vulnerable"].stacks).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('addItemEffect', () => {
|
describe("addItemEffect", () => {
|
||||||
it('should add effect to player.itemEffects[itemKey]', () => {
|
it("should add effect to player.itemEffects[itemKey]", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('adjacent-buff', 'itemTemporary');
|
const effect = createEffect("adjacent-buff", "itemTemporary");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 3);
|
addItemEffect(player, "sword-1", effect, 3);
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['adjacent-buff'].stacks).toBe(3);
|
expect(player.itemEffects["sword-1"]["adjacent-buff"].stacks).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should initialize itemEffects entry if not present', () => {
|
it("should initialize itemEffects entry if not present", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('adjacent-buff', 'itemTemporary');
|
const effect = createEffect("adjacent-buff", "itemTemporary");
|
||||||
|
|
||||||
addItemEffect(player, 'new-item', effect, 1);
|
addItemEffect(player, "new-item", effect, 1);
|
||||||
|
|
||||||
expect(player.itemEffects['new-item']).toBeDefined();
|
expect(player.itemEffects["new-item"]).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stack with existing item effect', () => {
|
it("should stack with existing item effect", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('adjacent-buff', 'itemTemporary');
|
const effect = createEffect("adjacent-buff", "itemTemporary");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 2);
|
addItemEffect(player, "sword-1", effect, 2);
|
||||||
addItemEffect(player, 'sword-1', effect, 3);
|
addItemEffect(player, "sword-1", effect, 3);
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['adjacent-buff'].stacks).toBe(5);
|
expect(player.itemEffects["sword-1"]["adjacent-buff"].stacks).toBe(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onEntityEffectUpkeep', () => {
|
describe("onEntityEffectUpkeep", () => {
|
||||||
it('should remove temporary effects', () => {
|
it("should remove temporary effects", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const tempEffect = createEffect('temp-shield', 'temporary');
|
const tempEffect = createEffect("temp-shield", "temporary");
|
||||||
|
|
||||||
addEntityEffect(entity, tempEffect, 5);
|
addEntityEffect(entity, tempEffect, 5);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['temp-shield']).toBeUndefined();
|
expect(entity.effects["temp-shield"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decrement lingering effects by 1', () => {
|
it("should decrement lingering effects by 1", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const lingeringEffect = createEffect('poison', 'lingering');
|
const lingeringEffect = createEffect("poison", "lingering");
|
||||||
|
|
||||||
addEntityEffect(entity, lingeringEffect, 3);
|
addEntityEffect(entity, lingeringEffect, 3);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['poison'].stacks).toBe(2);
|
expect(entity.effects["poison"].stacks).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove lingering effects when stacks reach 0', () => {
|
it("should remove lingering effects when stacks reach 0", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const lingeringEffect = createEffect('poison', 'lingering');
|
const lingeringEffect = createEffect("poison", "lingering");
|
||||||
|
|
||||||
addEntityEffect(entity, lingeringEffect, 1);
|
addEntityEffect(entity, lingeringEffect, 1);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['poison']).toBeUndefined();
|
expect(entity.effects["poison"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect permanent effects', () => {
|
it("should not affect permanent effects", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const permEffect = createEffect('max-hp-up', 'permanent');
|
const permEffect = createEffect("max-hp-up", "permanent");
|
||||||
|
|
||||||
addEntityEffect(entity, permEffect, 5);
|
addEntityEffect(entity, permEffect, 5);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['max-hp-up'].stacks).toBe(5);
|
expect(entity.effects["max-hp-up"].stacks).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect instant effects', () => {
|
it("should not affect instant effects", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const instantEffect = createEffect('instant-damage', 'instant');
|
const instantEffect = createEffect("instant-damage", "instant");
|
||||||
|
|
||||||
addEntityEffect(entity, instantEffect, 10);
|
addEntityEffect(entity, instantEffect, 10);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['instant-damage'].stacks).toBe(10);
|
expect(entity.effects["instant-damage"].stacks).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should increment lingering effects with negative stacks', () => {
|
it("should increment lingering effects with negative stacks", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const lingeringEffect = createEffect('regen', 'lingering');
|
const lingeringEffect = createEffect("regen", "lingering");
|
||||||
|
|
||||||
addEntityEffect(entity, lingeringEffect, -3);
|
addEntityEffect(entity, lingeringEffect, -3);
|
||||||
onEntityEffectUpkeep(entity);
|
onEntityEffectUpkeep(entity);
|
||||||
|
|
||||||
expect(entity.effects['regen'].stacks).toBe(-2);
|
expect(entity.effects["regen"].stacks).toBe(-2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onEntityPostureDamage', () => {
|
describe("onEntityPostureDamage", () => {
|
||||||
it('should reduce posture effects by damage amount', () => {
|
it("should reduce posture effects by damage amount", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const postureEffect = createEffect('block', 'posture');
|
const postureEffect = createEffect("block", "posture");
|
||||||
|
|
||||||
addEntityEffect(entity, postureEffect, 10);
|
addEntityEffect(entity, postureEffect, 10);
|
||||||
onEntityPostureDamage(entity, 4);
|
onEntityPostureDamage(entity, 4);
|
||||||
|
|
||||||
expect(entity.effects['block'].stacks).toBe(6);
|
expect(entity.effects["block"].stacks).toBe(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not reduce posture effects below 0', () => {
|
it("should not reduce posture effects below 0", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const postureEffect = createEffect('block', 'posture');
|
const postureEffect = createEffect("block", "posture");
|
||||||
|
|
||||||
addEntityEffect(entity, postureEffect, 3);
|
addEntityEffect(entity, postureEffect, 3);
|
||||||
onEntityPostureDamage(entity, 10);
|
onEntityPostureDamage(entity, 10);
|
||||||
|
|
||||||
expect(entity.effects['block']).toBeUndefined();
|
expect(entity.effects["block"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect non-posture effects', () => {
|
it("should not affect non-posture effects", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const postureEffect = createEffect('block', 'posture');
|
const postureEffect = createEffect("block", "posture");
|
||||||
const permEffect = createEffect('strength', 'permanent');
|
const permEffect = createEffect("strength", "permanent");
|
||||||
|
|
||||||
addEntityEffect(entity, postureEffect, 5);
|
addEntityEffect(entity, postureEffect, 5);
|
||||||
addEntityEffect(entity, permEffect, 3);
|
addEntityEffect(entity, permEffect, 3);
|
||||||
onEntityPostureDamage(entity, 2);
|
onEntityPostureDamage(entity, 2);
|
||||||
|
|
||||||
expect(entity.effects['block'].stacks).toBe(3);
|
expect(entity.effects["block"].stacks).toBe(3);
|
||||||
expect(entity.effects['strength'].stacks).toBe(3);
|
expect(entity.effects["strength"].stacks).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle zero damage', () => {
|
it("should handle zero damage", () => {
|
||||||
const entity = createCombatEntity();
|
const entity = createCombatEntity();
|
||||||
const postureEffect = createEffect('block', 'posture');
|
const postureEffect = createEffect("block", "posture");
|
||||||
|
|
||||||
addEntityEffect(entity, postureEffect, 5);
|
addEntityEffect(entity, postureEffect, 5);
|
||||||
onEntityPostureDamage(entity, 0);
|
onEntityPostureDamage(entity, 0);
|
||||||
|
|
||||||
expect(entity.effects['block'].stacks).toBe(5);
|
expect(entity.effects["block"].stacks).toBe(5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onPlayerItemEffectUpkeep', () => {
|
describe("onPlayerItemEffectUpkeep", () => {
|
||||||
it('should remove itemTemporary effects', () => {
|
it("should remove itemTemporary effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('adjacent-buff', 'itemTemporary');
|
const effect = createEffect("adjacent-buff", "itemTemporary");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 5);
|
addItemEffect(player, "sword-1", effect, 5);
|
||||||
onPlayerItemEffectUpkeep(player);
|
onPlayerItemEffectUpkeep(player);
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['adjacent-buff']).toBeUndefined();
|
expect(player.itemEffects["sword-1"]["adjacent-buff"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect itemPermanent effects', () => {
|
it("should not affect itemPermanent effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('adjacent-buff', 'itemPermanent');
|
const effect = createEffect("adjacent-buff", "itemPermanent");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 5);
|
addItemEffect(player, "sword-1", effect, 5);
|
||||||
onPlayerItemEffectUpkeep(player);
|
onPlayerItemEffectUpkeep(player);
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['adjacent-buff'].stacks).toBe(5);
|
expect(player.itemEffects["sword-1"]["adjacent-buff"].stacks).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect itemUntilPlay effects', () => {
|
it("should not affect itemUntilPlay effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('charged', 'itemUntilPlay');
|
const effect = createEffect("charged", "itemUntilPlay");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 3);
|
addItemEffect(player, "sword-1", effect, 3);
|
||||||
onPlayerItemEffectUpkeep(player);
|
onPlayerItemEffectUpkeep(player);
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['charged'].stacks).toBe(3);
|
expect(player.itemEffects["sword-1"]["charged"].stacks).toBe(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onItemPlay', () => {
|
describe("onItemPlay", () => {
|
||||||
it('should remove itemUntilPlay effects', () => {
|
it("should remove itemUntilPlay effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('charged', 'itemUntilPlay');
|
const effect = createEffect("charged", "itemUntilPlay");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 3);
|
addItemEffect(player, "sword-1", effect, 3);
|
||||||
onItemPlay(player, 'sword-1');
|
onItemPlay(player, "sword-1");
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['charged']).toBeUndefined();
|
expect(player.itemEffects["sword-1"]["charged"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect other lifecycle effects', () => {
|
it("should not affect other lifecycle effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const permEffect = createEffect('passive', 'itemPermanent');
|
const permEffect = createEffect("passive", "itemPermanent");
|
||||||
const playEffect = createEffect('charged', 'itemUntilPlay');
|
const playEffect = createEffect("charged", "itemUntilPlay");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', permEffect, 5);
|
addItemEffect(player, "sword-1", permEffect, 5);
|
||||||
addItemEffect(player, 'sword-1', playEffect, 3);
|
addItemEffect(player, "sword-1", playEffect, 3);
|
||||||
onItemPlay(player, 'sword-1');
|
onItemPlay(player, "sword-1");
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['passive'].stacks).toBe(5);
|
expect(player.itemEffects["sword-1"]["passive"].stacks).toBe(5);
|
||||||
expect(player.itemEffects['sword-1']['charged']).toBeUndefined();
|
expect(player.itemEffects["sword-1"]["charged"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing for item with no effects', () => {
|
it("should do nothing for item with no effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
|
|
||||||
expect(() => onItemPlay(player, 'nonexistent')).not.toThrow();
|
expect(() => onItemPlay(player, "nonexistent")).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onItemDiscard', () => {
|
describe("onItemDiscard", () => {
|
||||||
it('should remove itemUntilDiscard effects', () => {
|
it("should remove itemUntilDiscard effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const effect = createEffect('discard-buff', 'itemUntilDiscard');
|
const effect = createEffect("discard-buff", "itemUntilDiscard");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', effect, 3);
|
addItemEffect(player, "sword-1", effect, 3);
|
||||||
onItemDiscard(player, 'sword-1');
|
onItemDiscard(player, "sword-1");
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['discard-buff']).toBeUndefined();
|
expect(player.itemEffects["sword-1"]["discard-buff"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not affect other lifecycle effects', () => {
|
it("should not affect other lifecycle effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const permEffect = createEffect('passive', 'itemPermanent');
|
const permEffect = createEffect("passive", "itemPermanent");
|
||||||
const discardEffect = createEffect('discard-buff', 'itemUntilDiscard');
|
const discardEffect = createEffect("discard-buff", "itemUntilDiscard");
|
||||||
|
|
||||||
addItemEffect(player, 'sword-1', permEffect, 5);
|
addItemEffect(player, "sword-1", permEffect, 5);
|
||||||
addItemEffect(player, 'sword-1', discardEffect, 3);
|
addItemEffect(player, "sword-1", discardEffect, 3);
|
||||||
onItemDiscard(player, 'sword-1');
|
onItemDiscard(player, "sword-1");
|
||||||
|
|
||||||
expect(player.itemEffects['sword-1']['passive'].stacks).toBe(5);
|
expect(player.itemEffects["sword-1"]["passive"].stacks).toBe(5);
|
||||||
expect(player.itemEffects['sword-1']['discard-buff']).toBeUndefined();
|
expect(player.itemEffects["sword-1"]["discard-buff"]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing for item with no effects', () => {
|
it("should do nothing for item with no effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
|
|
||||||
expect(() => onItemDiscard(player, 'nonexistent')).not.toThrow();
|
expect(() => onItemDiscard(player, "nonexistent")).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getAliveEnemies', () => {
|
describe("getAliveEnemies", () => {
|
||||||
it('should yield only alive enemies', () => {
|
it("should yield only alive enemies", () => {
|
||||||
const state = createCombatState(30, [
|
const state = createCombatState(30, [
|
||||||
createEnemyEntity('slime-1', 10, 10),
|
createEnemyEntity("slime-1", 10, 10),
|
||||||
createEnemyEntity('slime-2', 0, 10),
|
createEnemyEntity("slime-2", 0, 10),
|
||||||
createEnemyEntity('slime-3', 5, 10),
|
createEnemyEntity("slime-3", 5, 10),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const alive = [...getAliveEnemies(state)];
|
const alive = [...getAliveEnemies(state)];
|
||||||
|
|
||||||
expect(alive.length).toBe(2);
|
expect(alive.length).toBe(2);
|
||||||
expect(alive[0].id).toBe('slime-1');
|
expect(alive[0].id).toBe("slime-1");
|
||||||
expect(alive[1].id).toBe('slime-3');
|
expect(alive[1].id).toBe("slime-3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty for no enemies', () => {
|
it("should return empty for no enemies", () => {
|
||||||
const state = createCombatState(30, []);
|
const state = createCombatState(30, []);
|
||||||
|
|
||||||
const alive = [...getAliveEnemies(state)];
|
const alive = [...getAliveEnemies(state)];
|
||||||
|
|
@ -409,10 +473,10 @@ describe('combat/effects', () => {
|
||||||
expect(alive.length).toBe(0);
|
expect(alive.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty when all enemies are dead', () => {
|
it("should return empty when all enemies are dead", () => {
|
||||||
const state = createCombatState(30, [
|
const state = createCombatState(30, [
|
||||||
createEnemyEntity('slime-1', 0, 10),
|
createEnemyEntity("slime-1", 0, 10),
|
||||||
createEnemyEntity('slime-2', 0, 10),
|
createEnemyEntity("slime-2", 0, 10),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const alive = [...getAliveEnemies(state)];
|
const alive = [...getAliveEnemies(state)];
|
||||||
|
|
@ -421,130 +485,132 @@ describe('combat/effects', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getCombatEntity', () => {
|
describe("getCombatEntity", () => {
|
||||||
it('should return player for "player" key', () => {
|
it('should return player for "player" key', () => {
|
||||||
const state = createCombatState(30);
|
const state = createCombatState(30);
|
||||||
|
|
||||||
const entity = getCombatEntity(state, 'player');
|
const entity = getCombatEntity(state, "player");
|
||||||
|
|
||||||
expect(entity).toBe(state.player);
|
expect(entity).toBe(state.player);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return enemy by id', () => {
|
it("should return enemy by id", () => {
|
||||||
const enemy = createEnemyEntity('boss-1', 50, 50);
|
const enemy = createEnemyEntity("boss-1", 50, 50);
|
||||||
const state = createCombatState(30, [enemy]);
|
const state = createCombatState(30, [enemy]);
|
||||||
|
|
||||||
const entity = getCombatEntity(state, 'boss-1');
|
const entity = getCombatEntity(state, "boss-1");
|
||||||
|
|
||||||
expect(entity).toBe(enemy);
|
expect(entity).toBe(enemy);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined for non-existent enemy', () => {
|
it("should return undefined for non-existent enemy", () => {
|
||||||
const state = createCombatState(30, [createEnemyEntity('slime-1')]);
|
const state = createCombatState(30, [createEnemyEntity("slime-1")]);
|
||||||
|
|
||||||
const entity = getCombatEntity(state, 'nonexistent');
|
const entity = getCombatEntity(state, "nonexistent");
|
||||||
|
|
||||||
expect(entity).toBeUndefined();
|
expect(entity).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('canPlayCard', () => {
|
describe("canPlayCard", () => {
|
||||||
it('should allow playing energy card when player has enough energy', () => {
|
it("should allow playing energy card when player has enough energy", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'energy', 2, 'any', inventory);
|
const result = canPlayCard(player, "energy", 2, "any", inventory);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject playing energy card when player lacks energy', () => {
|
it("should reject playing energy card when player lacks energy", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 1;
|
player.energy = 1;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'energy', 2, 'any', inventory);
|
const result = canPlayCard(player, "energy", 2, "any", inventory);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow playing uses card when item has remaining uses', () => {
|
it("should allow playing uses card when item has remaining uses", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 1);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'uses', 3, 'potion-1', inventory);
|
const result = canPlayCard(player, "uses", 3, "potion-1", inventory);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject playing uses card when item is depleted', () => {
|
it("should reject playing uses card when item is depleted", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 3);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 3);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'uses', 3, 'potion-1', inventory);
|
const result = canPlayCard(player, "uses", 3, "potion-1", inventory);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject playing uses card when item not in inventory', () => {
|
it("should reject playing uses card when item not in inventory", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'uses', 1, 'missing', inventory);
|
const result = canPlayCard(player, "uses", 1, "missing", inventory);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should always allow playing none cost card', () => {
|
it("should always allow playing none cost card", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 0;
|
player.energy = 0;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
const result = canPlayCard(player, 'none', 0, 'any', inventory);
|
const result = canPlayCard(player, "none", 0, "any", inventory);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('payCardCost', () => {
|
describe("payCardCost", () => {
|
||||||
it('should deduct energy for energy cost card', () => {
|
it("should deduct energy for energy cost card", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
payCardCost(player, 'energy', 2, 'any', inventory);
|
payCardCost(player, "energy", 2, "any", inventory);
|
||||||
|
|
||||||
expect(player.energy).toBe(1);
|
expect(player.energy).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should increment depletion for uses cost card', () => {
|
it("should increment depletion for uses cost card", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 1);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
|
||||||
payCardCost(player, 'uses', 3, 'potion-1', inventory);
|
payCardCost(player, "uses", 3, "potion-1", inventory);
|
||||||
|
|
||||||
expect(item.meta?.depletion).toBe(4);
|
expect(item.meta?.depletion).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing for none cost card', () => {
|
it("should do nothing for none cost card", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
payCardCost(player, 'none', 0, 'any', inventory);
|
payCardCost(player, "none", 0, "any", inventory);
|
||||||
|
|
||||||
expect(player.energy).toBe(3);
|
expect(player.energy).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle missing item gracefully for uses cost', () => {
|
it("should handle missing item gracefully for uses cost", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
|
||||||
expect(() => payCardCost(player, 'uses', 1, 'missing', inventory)).not.toThrow();
|
expect(() =>
|
||||||
|
payCardCost(player, "uses", 1, "missing", inventory),
|
||||||
|
).not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,99 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import { createGameContext, createGameCommandRegistry, IGameContext } from '@/core/game';
|
import {
|
||||||
import { createRegion } from '@/core/region';
|
createGameContext,
|
||||||
import { createStartWith, Triggers } from '@/samples/slay-the-spire-like/system/combat/triggers';
|
createGameCommandRegistry,
|
||||||
import { addTriggers } from '@/samples/slay-the-spire-like/data/desert/triggers';
|
IGameContext,
|
||||||
import { CombatState, EnemyEntity } from '@/samples/slay-the-spire-like/system/combat/types';
|
} from "@/core/game";
|
||||||
import { EffectData } from '@/samples/slay-the-spire-like/system/types';
|
import { createRegion } from "@/core/region";
|
||||||
import { GameCard, DeckRegions } from '@/samples/slay-the-spire-like/system/deck';
|
import {
|
||||||
import { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/system/grid-inventory/types';
|
createStartWith,
|
||||||
import { GameItemMeta } from '@/samples/slay-the-spire-like/system/progress/types';
|
createTriggers,
|
||||||
import { ParsedShape } from '@/samples/slay-the-spire-like/system/utils/parse-shape';
|
Triggers,
|
||||||
import { Transform2D } from '@/samples/slay-the-spire-like/system/utils/shape-collision';
|
} from "@/samples/slay-the-spire-like/system/combat/triggers";
|
||||||
import { cards, effects, enemies, items } from '@/samples/slay-the-spire-like/data/desert';
|
import { addTriggers } from "@/samples/slay-the-spire-like/data/desert/triggers";
|
||||||
|
import {
|
||||||
|
CombatState,
|
||||||
|
EnemyEntity,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
import {
|
||||||
|
GameCard,
|
||||||
|
DeckRegions,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/deck";
|
||||||
|
import {
|
||||||
|
CellKey,
|
||||||
|
GridInventory,
|
||||||
|
InventoryItem,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
||||||
|
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress/types";
|
||||||
|
import { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape";
|
||||||
|
import { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
|
||||||
|
import {
|
||||||
|
getCards,
|
||||||
|
getEffects,
|
||||||
|
getEncounters,
|
||||||
|
getEnemies,
|
||||||
|
getItems,
|
||||||
|
} from "@/samples/slay-the-spire-like/data/desert";
|
||||||
|
const cards = getCards();
|
||||||
|
const effects = getEffects();
|
||||||
|
const encounters = getEncounters();
|
||||||
|
const items = getItems();
|
||||||
|
const enemies = getEnemies();
|
||||||
|
|
||||||
function createEffect(id: string, lifecycle: EffectData['lifecycle'] = 'instant'): EffectData {
|
function createEffect(
|
||||||
const found = effects.find(e => e.id === id);
|
id: string,
|
||||||
|
lifecycle: EffectData["lifecycle"] = "instant",
|
||||||
|
): EffectData {
|
||||||
|
const found = effects.find((e) => e.id === id);
|
||||||
if (found) return found;
|
if (found) return found;
|
||||||
return { id, name: id, description: '', lifecycle };
|
return { id, name: id, description: "", lifecycle };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDeckRegions(): DeckRegions {
|
function createDeckRegions(): DeckRegions {
|
||||||
return {
|
return {
|
||||||
drawPile: createRegion('drawPile', []),
|
drawPile: createRegion("drawPile", []),
|
||||||
hand: createRegion('hand', []),
|
hand: createRegion("hand", []),
|
||||||
discardPile: createRegion('discardPile', []),
|
discardPile: createRegion("discardPile", []),
|
||||||
exhaustPile: createRegion('exhaustPile', []),
|
exhaustPile: createRegion("exhaustPile", []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCard(id: string, itemId: string, costType: 'energy' | 'uses' | 'none' = 'energy', costCount = 0): GameCard {
|
function createCard(
|
||||||
const cardData = cards.find(c => c.id === itemId) ?? {
|
id: string,
|
||||||
id: itemId, name: itemId, desc: '', type: 'item' as const, costType, costCount, targetType: 'none' as const, effects: [],
|
itemId: string,
|
||||||
|
costType: "energy" | "uses" | "none" = "energy",
|
||||||
|
costCount = 0,
|
||||||
|
): GameCard {
|
||||||
|
const cardData = cards.find((c) => c.id === itemId) ?? {
|
||||||
|
id: itemId,
|
||||||
|
name: itemId,
|
||||||
|
desc: "",
|
||||||
|
type: "item" as const,
|
||||||
|
costType,
|
||||||
|
costCount,
|
||||||
|
targetType: "none" as const,
|
||||||
|
effects: [],
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
regionId: '',
|
regionId: "",
|
||||||
position: [0],
|
position: [0],
|
||||||
itemId,
|
itemId,
|
||||||
cardData,
|
cardData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEnemyEntity(enemyId: string, hp: number, maxHp: number, instanceIndex = 0): EnemyEntity {
|
function createEnemyEntity(
|
||||||
const enemyData = enemies.find(e => e.id === enemyId);
|
enemyId: string,
|
||||||
|
hp: number,
|
||||||
|
maxHp: number,
|
||||||
|
instanceIndex = 0,
|
||||||
|
): EnemyEntity {
|
||||||
|
const enemyData = enemies.find((e) => e.id === enemyId);
|
||||||
if (!enemyData) throw new Error(`Enemy "${enemyId}" not found`);
|
if (!enemyData) throw new Error(`Enemy "${enemyId}" not found`);
|
||||||
const intent = enemyData.intents.find(i => i.initialIntent) ?? enemyData.intents[0];
|
const intent =
|
||||||
|
enemyData.intents.find((i) => i.initialIntent) ?? enemyData.intents[0];
|
||||||
const instanceId = `${enemyId}-${instanceIndex}`;
|
const instanceId = `${enemyId}-${instanceIndex}`;
|
||||||
const intentMap: Record<string, typeof intent> = {};
|
const intentMap: Record<string, typeof intent> = {};
|
||||||
for (const i of enemyData.intents) {
|
for (const i of enemyData.intents) {
|
||||||
|
|
@ -61,12 +111,14 @@ function createEnemyEntity(enemyId: string, hp: number, maxHp: number, instanceI
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createInventory(itemsList: InventoryItem<GameItemMeta>[]): GridInventory<GameItemMeta> {
|
function createInventory(
|
||||||
|
itemsList: InventoryItem<GameItemMeta>[],
|
||||||
|
): GridInventory<GameItemMeta> {
|
||||||
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
||||||
const occupied = new Set<string>();
|
const occupied = new Set<CellKey>();
|
||||||
for (const item of itemsList) {
|
for (const item of itemsList) {
|
||||||
map.set(item.id, item);
|
map.set(item.id, item);
|
||||||
occupied.add(`${item.transform.x},${item.transform.y}`);
|
occupied.add(`${item.transform.offset.x},${item.transform.offset.y}`);
|
||||||
}
|
}
|
||||||
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +127,7 @@ function createCombatState(overrides: Partial<CombatState> = {}): CombatState {
|
||||||
const regions = createDeckRegions();
|
const regions = createDeckRegions();
|
||||||
return {
|
return {
|
||||||
player: {
|
player: {
|
||||||
id: 'player',
|
id: "player",
|
||||||
effects: {},
|
effects: {},
|
||||||
hp: 30,
|
hp: 30,
|
||||||
maxHp: 30,
|
maxHp: 30,
|
||||||
|
|
@ -87,7 +139,7 @@ function createCombatState(overrides: Partial<CombatState> = {}): CombatState {
|
||||||
},
|
},
|
||||||
enemies: [],
|
enemies: [],
|
||||||
inventory: createInventory([]),
|
inventory: createInventory([]),
|
||||||
phase: 'playerTurn',
|
phase: "playerTurn",
|
||||||
turnNumber: 1,
|
turnNumber: 1,
|
||||||
result: null,
|
result: null,
|
||||||
loot: [],
|
loot: [],
|
||||||
|
|
@ -103,72 +155,71 @@ function createTestContext(state?: CombatState): IGameContext<CombatState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggers(): Triggers {
|
function getTriggers(): Triggers {
|
||||||
let capturedTriggers: Triggers;
|
const triggers = createTriggers();
|
||||||
createStartWith(triggers => {
|
|
||||||
capturedTriggers = triggers;
|
|
||||||
addTriggers(triggers);
|
addTriggers(triggers);
|
||||||
});
|
return triggers;
|
||||||
return capturedTriggers!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardToHand(ctx: IGameContext<CombatState>, card: GameCard) {
|
function addCardToHand(ctx: IGameContext<CombatState>, card: GameCard) {
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.deck.cards[card.id] = card;
|
draft.player.deck.cards[card.id] = card;
|
||||||
card.regionId = 'hand';
|
card.regionId = "hand";
|
||||||
draft.player.deck.regions.hand.childIds.push(card.id);
|
draft.player.deck.regions.hand.childIds.push(card.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardToDrawPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
function addCardToDrawPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.deck.cards[card.id] = card;
|
draft.player.deck.cards[card.id] = card;
|
||||||
card.regionId = 'drawPile';
|
card.regionId = "drawPile";
|
||||||
draft.player.deck.regions.drawPile.childIds.push(card.id);
|
draft.player.deck.regions.drawPile.childIds.push(card.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCardToDiscardPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
function addCardToDiscardPile(ctx: IGameContext<CombatState>, card: GameCard) {
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.deck.cards[card.id] = card;
|
draft.player.deck.cards[card.id] = card;
|
||||||
card.regionId = 'discardPile';
|
card.regionId = "discardPile";
|
||||||
draft.player.deck.regions.discardPile.childIds.push(card.id);
|
draft.player.deck.regions.discardPile.childIds.push(card.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeDummyEnemy() {
|
function makeDummyEnemy() {
|
||||||
return createEnemyEntity('仙人掌怪', 999, 999);
|
return createEnemyEntity("仙人掌怪", 999, 999);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('desert triggers', () => {
|
describe("desert triggers", () => {
|
||||||
describe('instant effects', () => {
|
describe("instant effects", () => {
|
||||||
it('should apply attack effect as damage', async () => {
|
it("should apply attack effect as damage", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
|
createCombatState({
|
||||||
enemies: [makeDummyEnemy()],
|
enemies: [makeDummyEnemy()],
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const attackEffect = createEffect('attack');
|
const attackEffect = createEffect("attack");
|
||||||
|
|
||||||
await triggers.onEffectApplied.execute(ctx, {
|
await triggers.onEffectApplied.execute(ctx, {
|
||||||
effect: attackEffect,
|
effect: attackEffect,
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
stacks: 5,
|
stacks: 5,
|
||||||
sourceEntityKey: 'enemy-0',
|
sourceEntityKey: "enemy-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(25);
|
expect(ctx.value.player.hp).toBe(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply draw effect', async () => {
|
it("should apply draw effect", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const drawEffect = createEffect('draw');
|
const drawEffect = createEffect("draw");
|
||||||
|
|
||||||
addCardToDrawPile(ctx, createCard('card-1', 'sword'));
|
addCardToDrawPile(ctx, createCard("card-1", "sword"));
|
||||||
addCardToDrawPile(ctx, createCard('card-2', 'sword'));
|
addCardToDrawPile(ctx, createCard("card-2", "sword"));
|
||||||
|
|
||||||
await triggers.onEffectApplied.execute(ctx, {
|
await triggers.onEffectApplied.execute(ctx, {
|
||||||
effect: drawEffect,
|
effect: drawEffect,
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
stacks: 2,
|
stacks: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -176,182 +227,203 @@ describe('desert triggers', () => {
|
||||||
expect(ctx.value.player.deck.regions.drawPile.childIds.length).toBe(0);
|
expect(ctx.value.player.deck.regions.drawPile.childIds.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply gainEnergy effect', async () => {
|
it("should apply gainEnergy effect", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const gainEnergyEffect = createEffect('gainEnergy');
|
const gainEnergyEffect = createEffect("gainEnergy");
|
||||||
|
|
||||||
const initialEnergy = ctx.value.player.energy;
|
const initialEnergy = ctx.value.player.energy;
|
||||||
await triggers.onEffectApplied.execute(ctx, {
|
await triggers.onEffectApplied.execute(ctx, {
|
||||||
effect: gainEnergyEffect,
|
effect: gainEnergyEffect,
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
stacks: 2,
|
stacks: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.energy).toBe(initialEnergy + 2);
|
expect(ctx.value.player.energy).toBe(initialEnergy + 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove wound cards from draw and discard piles', async () => {
|
it("should remove wound cards from draw and discard piles", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
|
createCombatState({
|
||||||
enemies: [makeDummyEnemy()],
|
enemies: [makeDummyEnemy()],
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const removeWoundEffect = createEffect('removeWound');
|
const removeWoundEffect = createEffect("removeWound");
|
||||||
|
|
||||||
addCardToDrawPile(ctx, createCard('wound-1', 'wound', 'none', 0));
|
addCardToDrawPile(ctx, createCard("wound-1", "wound", "none", 0));
|
||||||
addCardToDiscardPile(ctx, createCard('wound-2', 'wound', 'none', 0));
|
addCardToDiscardPile(ctx, createCard("wound-2", "wound", "none", 0));
|
||||||
addCardToDrawPile(ctx, createCard('sword-1', 'sword'));
|
addCardToDrawPile(ctx, createCard("sword-1", "sword"));
|
||||||
|
|
||||||
await triggers.onEffectApplied.execute(ctx, {
|
await triggers.onEffectApplied.execute(ctx, {
|
||||||
effect: removeWoundEffect,
|
effect: removeWoundEffect,
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
stacks: 2,
|
stacks: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.deck.cards['wound-1']).toBeUndefined();
|
expect(ctx.value.player.deck.cards["wound-1"]).toBeUndefined();
|
||||||
expect(ctx.value.player.deck.cards['wound-2']).toBeUndefined();
|
expect(ctx.value.player.deck.cards["wound-2"]).toBeUndefined();
|
||||||
expect(ctx.value.player.deck.cards['sword-1']).toBeDefined();
|
expect(ctx.value.player.deck.cards["sword-1"]).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('damage pipeline', () => {
|
describe("damage pipeline", () => {
|
||||||
it('should prevent damage with block', async () => {
|
it("should prevent damage with block", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
|
createCombatState({
|
||||||
enemies: [makeDummyEnemy()],
|
enemies: [makeDummyEnemy()],
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const defendEffect = createEffect('defend', 'posture');
|
const defendEffect = createEffect("defend", "posture");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.defend = { data: defendEffect, stacks: 5 };
|
draft.player.effects.defend = { data: defendEffect, stacks: 5 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 8,
|
amount: 8,
|
||||||
sourceEntityKey: 'enemy-0',
|
sourceEntityKey: "enemy-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(27);
|
expect(ctx.value.player.hp).toBe(27);
|
||||||
expect(ctx.value.player.effects.defend?.stacks).toBe(2);
|
expect(ctx.value.player.effects.defend?.stacks).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reduce damage with damageReduce', async () => {
|
it("should reduce damage with damageReduce", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
|
createCombatState({
|
||||||
enemies: [makeDummyEnemy()],
|
enemies: [makeDummyEnemy()],
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const damageReduceEffect = createEffect('damageReduce', 'temporary');
|
const damageReduceEffect = createEffect("damageReduce", "temporary");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.damageReduce = { data: damageReduceEffect, stacks: 3 };
|
draft.player.effects.damageReduce = {
|
||||||
|
data: damageReduceEffect,
|
||||||
|
stacks: 3,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 8,
|
amount: 8,
|
||||||
sourceEntityKey: 'enemy-0',
|
sourceEntityKey: "enemy-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(25);
|
expect(ctx.value.player.hp).toBe(25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should increase damage with expose', async () => {
|
it("should increase damage with expose", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
|
createCombatState({
|
||||||
enemies: [makeDummyEnemy()],
|
enemies: [makeDummyEnemy()],
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const exposeEffect = createEffect('expose', 'temporary');
|
const exposeEffect = createEffect("expose", "temporary");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.expose = { data: exposeEffect, stacks: 2 };
|
draft.player.effects.expose = { data: exposeEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: 'enemy-0',
|
sourceEntityKey: "enemy-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(23);
|
expect(ctx.value.player.hp).toBe(23);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('spike reflection', () => {
|
describe("spike reflection", () => {
|
||||||
it('should damage attacker when entity has spike', async () => {
|
it("should damage attacker when entity has spike", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('仙人掌怪', 12, 12)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("仙人掌怪", 12, 12)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const spikeEffect = createEffect('spike', 'permanent');
|
const spikeEffect = createEffect("spike", "permanent");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.spike = { data: spikeEffect, stacks: 3 };
|
enemy.effects.spike = { data: spikeEffect, stacks: 3 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: '仙人掌怪-0',
|
entityKey: "仙人掌怪-0",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(27);
|
expect(ctx.value.player.hp).toBe(27);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('storm static card generation', () => {
|
describe("storm static card generation", () => {
|
||||||
it('should give player static cards when storm enemy executes intent', async () => {
|
it("should give player static cards when storm enemy executes intent", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('风暴之灵', 30, 30)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("风暴之灵", 30, 30)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const stormEffect = createEffect('storm', 'permanent');
|
const stormEffect = createEffect("storm", "permanent");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.storm = { data: stormEffect, stacks: 2 };
|
enemy.effects.storm = { data: stormEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onEnemyIntent.execute(ctx, { enemyId: '风暴之灵-0' });
|
await triggers.onEnemyIntent.execute(ctx, { enemyId: "风暴之灵-0" });
|
||||||
|
|
||||||
const staticCards = Object.values(ctx.value.player.deck.cards).filter((c: GameCard) => c.itemId === 'static');
|
const staticCards = Object.values(ctx.value.player.deck.cards).filter(
|
||||||
|
(c: GameCard) => c.itemId === "static",
|
||||||
|
);
|
||||||
expect(staticCards.length).toBe(2);
|
expect(staticCards.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('energyDrain', () => {
|
describe("energyDrain", () => {
|
||||||
it('should drain player energy when energyDrain enemy takes damage', async () => {
|
it("should drain player energy when energyDrain enemy takes damage", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('幼沙虫', 18, 18)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("幼沙虫", 18, 18)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const energyDrainEffect = createEffect('energyDrain', 'lingering');
|
const energyDrainEffect = createEffect("energyDrain", "lingering");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.energyDrain = { data: energyDrainEffect, stacks: 1 };
|
enemy.effects.energyDrain = { data: energyDrainEffect, stacks: 1 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: '幼沙虫-0',
|
entityKey: "幼沙虫-0",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.energy).toBe(2);
|
expect(ctx.value.player.energy).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('molt flee', () => {
|
describe("molt flee", () => {
|
||||||
it('should make enemy flee when molt >= maxHp after taking damage', async () => {
|
it("should make enemy flee when molt >= maxHp after taking damage", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('蜥蜴', 14, 14)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("蜥蜴", 14, 14)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const moltEffect = createEffect('molt', 'posture');
|
const moltEffect = createEffect("molt", "posture");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.molt = { data: moltEffect, stacks: 14 };
|
enemy.effects.molt = { data: moltEffect, stacks: 14 };
|
||||||
});
|
});
|
||||||
|
|
@ -359,233 +431,249 @@ describe('desert triggers', () => {
|
||||||
let threw = false;
|
let threw = false;
|
||||||
try {
|
try {
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: '蜥蜴-0',
|
entityKey: "蜥蜴-0",
|
||||||
amount: 1,
|
amount: 1,
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
threw = true;
|
threw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(threw).toBe(true);
|
expect(threw).toBe(true);
|
||||||
expect(ctx.value.result).toBe('victory');
|
expect(ctx.value.result).toBe("victory");
|
||||||
expect(ctx.value.enemies[0].isAlive).toBe(false);
|
expect(ctx.value.enemies[0].isAlive).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discard at turn start', () => {
|
describe("discard at turn start", () => {
|
||||||
it('should randomly discard a card when discard effect is active', async () => {
|
it("should randomly discard a card when discard effect is active", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const discardEffect = createEffect('discard', 'lingering');
|
const discardEffect = createEffect("discard", "lingering");
|
||||||
|
|
||||||
addCardToHand(ctx, createCard('card-1', 'sword'));
|
addCardToHand(ctx, createCard("card-1", "sword"));
|
||||||
addCardToHand(ctx, createCard('card-2', 'shield'));
|
addCardToHand(ctx, createCard("card-2", "shield"));
|
||||||
addCardToHand(ctx, createCard('card-3', 'dagger'));
|
addCardToHand(ctx, createCard("card-3", "dagger"));
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.discard = { data: discardEffect, stacks: 1 };
|
draft.player.effects.discard = { data: discardEffect, stacks: 1 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onTurnStart.execute(ctx, { entityKey: 'player' });
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
||||||
|
|
||||||
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
||||||
expect(ctx.value.player.deck.regions.discardPile.childIds.length).toBe(1);
|
expect(ctx.value.player.deck.regions.discardPile.childIds.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('next-turn effects', () => {
|
describe("next-turn effects", () => {
|
||||||
it('should gain block from defendNext at turn start', async () => {
|
it("should gain block from defendNext at turn start", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const defendNextEffect = createEffect('defendNext', 'temporary');
|
const defendNextEffect = createEffect("defendNext", "temporary");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.defendNext = { data: defendNextEffect, stacks: 5 };
|
draft.player.effects.defendNext = { data: defendNextEffect, stacks: 5 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onTurnStart.execute(ctx, { entityKey: 'player' });
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
||||||
|
|
||||||
expect(ctx.value.player.effects.defend?.stacks).toBe(5);
|
expect(ctx.value.player.effects.defend?.stacks).toBe(5);
|
||||||
expect(ctx.value.player.effects.defendNext).toBeUndefined();
|
expect(ctx.value.player.effects.defendNext).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should gain energy from energyNext at turn start', async () => {
|
it("should gain energy from energyNext at turn start", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const energyNextEffect = createEffect('energyNext', 'temporary');
|
const energyNextEffect = createEffect("energyNext", "temporary");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.energyNext = { data: energyNextEffect, stacks: 2 };
|
draft.player.effects.energyNext = { data: energyNextEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onTurnStart.execute(ctx, { entityKey: 'player' });
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
||||||
|
|
||||||
expect(ctx.value.player.energy).toBe(5);
|
expect(ctx.value.player.energy).toBe(5);
|
||||||
expect(ctx.value.player.effects.energyNext).toBeUndefined();
|
expect(ctx.value.player.effects.energyNext).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should draw extra cards from drawNext at turn start', async () => {
|
it("should draw extra cards from drawNext at turn start", async () => {
|
||||||
const ctx = createTestContext();
|
const ctx = createTestContext();
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const drawNextEffect = createEffect('drawNext', 'temporary');
|
const drawNextEffect = createEffect("drawNext", "temporary");
|
||||||
|
|
||||||
addCardToDrawPile(ctx, createCard('card-1', 'sword'));
|
addCardToDrawPile(ctx, createCard("card-1", "sword"));
|
||||||
addCardToDrawPile(ctx, createCard('card-2', 'sword'));
|
addCardToDrawPile(ctx, createCard("card-2", "sword"));
|
||||||
addCardToDrawPile(ctx, createCard('card-3', 'sword'));
|
addCardToDrawPile(ctx, createCard("card-3", "sword"));
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.drawNext = { data: drawNextEffect, stacks: 2 };
|
draft.player.effects.drawNext = { data: drawNextEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onTurnStart.execute(ctx, { entityKey: 'player' });
|
await triggers.onTurnStart.execute(ctx, { entityKey: "player" });
|
||||||
|
|
||||||
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
expect(ctx.value.player.deck.regions.hand.childIds.length).toBe(2);
|
||||||
expect(ctx.value.player.effects.drawNext).toBeUndefined();
|
expect(ctx.value.player.effects.drawNext).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('posture damage effects', () => {
|
describe("posture damage effects", () => {
|
||||||
it('should double damage with aim', async () => {
|
it("should double damage with aim", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('仙人掌怪', 12, 12)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("仙人掌怪", 12, 12)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const aimEffect = createEffect('aim', 'posture');
|
const aimEffect = createEffect("aim", "posture");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.aim = { data: aimEffect, stacks: 2 };
|
draft.player.effects.aim = { data: aimEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: '仙人掌怪-0',
|
entityKey: "仙人掌怪-0",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.enemies[0].hp).toBe(2);
|
expect(ctx.value.enemies[0].hp).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add bonus damage with roll', async () => {
|
it("should add bonus damage with roll", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('仙人掌怪', 99, 99)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("仙人掌怪", 99, 99)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const rollEffect = createEffect('roll', 'posture');
|
const rollEffect = createEffect("roll", "posture");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
draft.player.effects.roll = { data: rollEffect, stacks: 20 };
|
draft.player.effects.roll = { data: rollEffect, stacks: 20 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: '仙人掌怪-0',
|
entityKey: "仙人掌怪-0",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.enemies[0].hp).toBe(74);
|
expect(ctx.value.enemies[0].hp).toBe(74);
|
||||||
expect(ctx.value.player.effects.roll).toBeUndefined();
|
expect(ctx.value.player.effects.roll).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add bonus damage with tailSting', async () => {
|
it("should add bonus damage with tailSting", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('沙蝎', 10, 10)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("沙蝎", 10, 10)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const tailStingEffect = createEffect('tailSting', 'posture');
|
const tailStingEffect = createEffect("tailSting", "posture");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.tailSting = { data: tailStingEffect, stacks: 2 };
|
enemy.effects.tailSting = { data: tailStingEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: '沙蝎-0',
|
sourceEntityKey: "沙蝎-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(23);
|
expect(ctx.value.player.hp).toBe(23);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should double damage with charge on attacker', async () => {
|
it("should double damage with charge on attacker", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('骑马枪手', 25, 25)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("骑马枪手", 25, 25)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const chargeEffect = createEffect('charge', 'lingering');
|
const chargeEffect = createEffect("charge", "lingering");
|
||||||
|
|
||||||
ctx._state.produce(draft => {
|
ctx._state.produce((draft) => {
|
||||||
const enemy = draft.enemies[0];
|
const enemy = draft.enemies[0];
|
||||||
enemy.effects.charge = { data: chargeEffect, stacks: 2 };
|
enemy.effects.charge = { data: chargeEffect, stacks: 2 };
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: '骑马枪手-0',
|
sourceEntityKey: "骑马枪手-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.player.hp).toBe(20);
|
expect(ctx.value.player.hp).toBe(20);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('crossbow chain', () => {
|
describe("crossbow chain", () => {
|
||||||
it('should replay other crossbows on same target', async () => {
|
it("should replay other crossbows on same target", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('仙人掌怪', 20, 20)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("仙人掌怪", 20, 20)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
const crossbowEffect = createEffect('crossbow');
|
const crossbowEffect = createEffect("crossbow");
|
||||||
|
|
||||||
addCardToHand(ctx, createCard('crossbow-1', 'crossbow'));
|
addCardToHand(ctx, createCard("crossbow-1", "crossbow"));
|
||||||
addCardToHand(ctx, createCard('crossbow-2', 'crossbow'));
|
addCardToHand(ctx, createCard("crossbow-2", "crossbow"));
|
||||||
|
|
||||||
await triggers.onEffectApplied.execute(ctx, {
|
await triggers.onEffectApplied.execute(ctx, {
|
||||||
effect: crossbowEffect,
|
effect: crossbowEffect,
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
stacks: 0,
|
stacks: 0,
|
||||||
cardId: 'crossbow-1',
|
cardId: "crossbow-1",
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
targetId: '仙人掌怪-0',
|
targetId: "仙人掌怪-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.enemies[0].hp).toBe(8);
|
expect(ctx.value.enemies[0].hp).toBe(8);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sandwormKing fatigue heal', () => {
|
describe("sandwormKing fatigue heal", () => {
|
||||||
it('should heal sandworm king when player discards fatigue', async () => {
|
it("should heal sandworm king when player discards fatigue", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('沙虫王', 30, 40)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("沙虫王", 30, 40)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
|
|
||||||
addCardToHand(ctx, createCard('fatigue-1', 'fatigue', 'none', 0));
|
addCardToHand(ctx, createCard("fatigue-1", "fatigue", "none", 0));
|
||||||
|
|
||||||
await triggers.onCardDiscarded.execute(ctx, {
|
await triggers.onCardDiscarded.execute(ctx, {
|
||||||
cardId: 'fatigue-1',
|
cardId: "fatigue-1",
|
||||||
sourceEntityKey: 'player',
|
sourceEntityKey: "player",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ctx.value.enemies[0].hp).toBe(40);
|
expect(ctx.value.enemies[0].hp).toBe(40);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('vulture on-damage', () => {
|
describe("vulture on-damage", () => {
|
||||||
it('should give player vultureEye when vulture deals damage', async () => {
|
it("should give player vultureEye when vulture deals damage", async () => {
|
||||||
const ctx = createTestContext(createCombatState({
|
const ctx = createTestContext(
|
||||||
enemies: [createEnemyEntity('秃鹫', 12, 12)],
|
createCombatState({
|
||||||
}));
|
enemies: [createEnemyEntity("秃鹫", 12, 12)],
|
||||||
|
}),
|
||||||
|
);
|
||||||
const triggers = getTriggers();
|
const triggers = getTriggers();
|
||||||
|
|
||||||
await triggers.onDamage.execute(ctx, {
|
await triggers.onDamage.execute(ctx, {
|
||||||
entityKey: 'player',
|
entityKey: "player",
|
||||||
amount: 5,
|
amount: 5,
|
||||||
sourceEntityKey: '秃鹫-0',
|
sourceEntityKey: "秃鹫-0",
|
||||||
});
|
});
|
||||||
|
|
||||||
const vultureEyeCards = Object.values(ctx.value.player.deck.cards).filter((c: GameCard) => c.itemId === 'vultureEye');
|
const vultureEyeCards = Object.values(ctx.value.player.deck.cards).filter(
|
||||||
|
(c: GameCard) => c.itemId === "vultureEye",
|
||||||
|
);
|
||||||
expect(vultureEyeCards.length).toBe(1);
|
expect(vultureEyeCards.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import data from '@/samples/slay-the-spire-like/data';
|
import data from "@/samples/slay-the-spire-like/data";
|
||||||
|
import { CardData } from "@/samples/slay-the-spire-like";
|
||||||
|
|
||||||
describe('data import', () => {
|
describe("data import", () => {
|
||||||
it('should import properly', () => {
|
it("should import properly", () => {
|
||||||
expect(data.desert.effects).toBeDefined();
|
expect(data.desert.getEffects).toBeDefined();
|
||||||
|
expect(
|
||||||
|
data.desert.getCards().find((c: CardData) => c.id === "crossbow")?.effects
|
||||||
|
?.length,
|
||||||
|
).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from "vitest";
|
||||||
import { generatePointCrawlMap, hasPath } from '@/samples/slay-the-spire-like/system/map/generator';
|
import {
|
||||||
import { MapNodeType, MapLayerType } from '@/samples/slay-the-spire-like/system/map/types';
|
generatePointCrawlMap,
|
||||||
import { createRNG } from '@/utils/rng';
|
hasPath,
|
||||||
import { encounters } from '@/samples/slay-the-spire-like/data/desert';
|
} from "@/samples/slay-the-spire-like/system/map/generator";
|
||||||
|
import {
|
||||||
|
MapNodeType,
|
||||||
|
MapLayerType,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/map/types";
|
||||||
|
import { createRNG } from "@/utils/rng";
|
||||||
|
import { getEncounters } from "@/samples/slay-the-spire-like/data/desert";
|
||||||
|
const encounters = getEncounters();
|
||||||
|
|
||||||
describe('generatePointCrawlMap', () => {
|
describe("generatePointCrawlMap", () => {
|
||||||
it('should generate a map with 10 layers', () => {
|
it("should generate a map with 10 layers", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
expect(map.layers.length).toBe(10);
|
expect(map.layers.length).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct layer structure', () => {
|
it("should have correct layer structure", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const expectedStructure = [
|
const expectedStructure = [
|
||||||
'start',
|
"start",
|
||||||
MapLayerType.Wild,
|
MapLayerType.Wild,
|
||||||
MapLayerType.Wild,
|
MapLayerType.Wild,
|
||||||
MapLayerType.Settlement,
|
MapLayerType.Settlement,
|
||||||
|
|
@ -22,7 +29,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
MapLayerType.Settlement,
|
MapLayerType.Settlement,
|
||||||
MapLayerType.Wild,
|
MapLayerType.Wild,
|
||||||
MapLayerType.Wild,
|
MapLayerType.Wild,
|
||||||
'end',
|
"end",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < expectedStructure.length; i++) {
|
for (let i = 0; i < expectedStructure.length; i++) {
|
||||||
|
|
@ -30,7 +37,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct node counts per layer', () => {
|
it("should have correct node counts per layer", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const expectedCounts = [1, 3, 3, 4, 3, 3, 4, 3, 3, 1];
|
const expectedCounts = [1, 3, 3, 4, 3, 3, 4, 3, 3, 1];
|
||||||
|
|
||||||
|
|
@ -39,19 +46,23 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have Start and End nodes with correct types', () => {
|
it("should have Start and End nodes with correct types", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const startNode = map.nodes.get('node-0-0');
|
const startNode = map.nodes.get("node-0-0");
|
||||||
const endNode = map.nodes.get('node-9-0');
|
const endNode = map.nodes.get("node-9-0");
|
||||||
|
|
||||||
expect(startNode?.type).toBe(MapNodeType.Start);
|
expect(startNode?.type).toBe(MapNodeType.Start);
|
||||||
expect(endNode?.type).toBe(MapNodeType.End);
|
expect(endNode?.type).toBe(MapNodeType.End);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have wild layers with minion/elite/event types', () => {
|
it("should have wild layers with minion/elite/event types", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const wildLayerIndices = [1, 2, 4, 5, 7, 8];
|
const wildLayerIndices = [1, 2, 4, 5, 7, 8];
|
||||||
const validWildTypes = new Set([MapNodeType.Minion, MapNodeType.Elite, MapNodeType.Event]);
|
const validWildTypes = new Set([
|
||||||
|
MapNodeType.Minion,
|
||||||
|
MapNodeType.Elite,
|
||||||
|
MapNodeType.Event,
|
||||||
|
]);
|
||||||
|
|
||||||
for (const layerIdx of wildLayerIndices) {
|
for (const layerIdx of wildLayerIndices) {
|
||||||
const layer = map.layers[layerIdx];
|
const layer = map.layers[layerIdx];
|
||||||
|
|
@ -63,13 +74,13 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have settlement layers with at least 1 camp, 1 shop, 1 curio', () => {
|
it("should have settlement layers with at least 1 camp, 1 shop, 1 curio", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const settlementLayerIndices = [3, 6];
|
const settlementLayerIndices = [3, 6];
|
||||||
|
|
||||||
for (const layerIdx of settlementLayerIndices) {
|
for (const layerIdx of settlementLayerIndices) {
|
||||||
const layer = map.layers[layerIdx];
|
const layer = map.layers[layerIdx];
|
||||||
const nodeTypes = layer.nodeIds.map(id => map.nodes.get(id)!.type);
|
const nodeTypes = layer.nodeIds.map((id) => map.nodes.get(id)!.type);
|
||||||
|
|
||||||
expect(nodeTypes).toContain(MapNodeType.Camp);
|
expect(nodeTypes).toContain(MapNodeType.Camp);
|
||||||
expect(nodeTypes).toContain(MapNodeType.Shop);
|
expect(nodeTypes).toContain(MapNodeType.Shop);
|
||||||
|
|
@ -78,16 +89,18 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have Start connected to all 3 wild nodes', () => {
|
it("should have Start connected to all 3 wild nodes", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(42), encounters);
|
const map = generatePointCrawlMap(createRNG(42), encounters);
|
||||||
const startNode = map.nodes.get('node-0-0');
|
const startNode = map.nodes.get("node-0-0");
|
||||||
const wildLayer = map.layers[1];
|
const wildLayer = map.layers[1];
|
||||||
|
|
||||||
expect(startNode?.childIds.length).toBe(3);
|
expect(startNode?.childIds.length).toBe(3);
|
||||||
expect(startNode?.childIds).toEqual(expect.arrayContaining(wildLayer.nodeIds));
|
expect(startNode?.childIds).toEqual(
|
||||||
|
expect.arrayContaining(wildLayer.nodeIds),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have each wild node connect to 1 wild node in wild→wild layers', () => {
|
it("should have each wild node connect to 1 wild node in wild→wild layers", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(42), encounters);
|
const map = generatePointCrawlMap(createRNG(42), encounters);
|
||||||
const wildToWildTransitions = [
|
const wildToWildTransitions = [
|
||||||
{ src: 1, tgt: 2 },
|
{ src: 1, tgt: 2 },
|
||||||
|
|
@ -106,7 +119,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have each wild node connect to 2 settlement nodes in wild→settlement layers', () => {
|
it("should have each wild node connect to 2 settlement nodes in wild→settlement layers", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(42), encounters);
|
const map = generatePointCrawlMap(createRNG(42), encounters);
|
||||||
const wildToSettlementTransitions = [
|
const wildToSettlementTransitions = [
|
||||||
{ src: 2, tgt: 3 },
|
{ src: 2, tgt: 3 },
|
||||||
|
|
@ -126,7 +139,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have settlement nodes connect correctly (1-2-2-1 pattern)', () => {
|
it("should have settlement nodes connect correctly (1-2-2-1 pattern)", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(42), encounters);
|
const map = generatePointCrawlMap(createRNG(42), encounters);
|
||||||
const settlementToWildTransitions = [
|
const settlementToWildTransitions = [
|
||||||
{ src: 3, tgt: 4 },
|
{ src: 3, tgt: 4 },
|
||||||
|
|
@ -154,10 +167,10 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have all 3 wild nodes connect to End', () => {
|
it("should have all 3 wild nodes connect to End", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(42), encounters);
|
const map = generatePointCrawlMap(createRNG(42), encounters);
|
||||||
const lastWildLayer = map.layers[8];
|
const lastWildLayer = map.layers[8];
|
||||||
const endNode = map.nodes.get('node-9-0');
|
const endNode = map.nodes.get("node-9-0");
|
||||||
|
|
||||||
for (const wildId of lastWildLayer.nodeIds) {
|
for (const wildId of lastWildLayer.nodeIds) {
|
||||||
const wildNode = map.nodes.get(wildId);
|
const wildNode = map.nodes.get(wildId);
|
||||||
|
|
@ -165,10 +178,10 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have all nodes reachable from Start and can reach End', () => {
|
it("should have all nodes reachable from Start and can reach End", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(123), encounters);
|
const map = generatePointCrawlMap(createRNG(123), encounters);
|
||||||
const startId = 'node-0-0';
|
const startId = "node-0-0";
|
||||||
const endId = 'node-9-0';
|
const endId = "node-9-0";
|
||||||
|
|
||||||
for (const nodeId of map.nodes.keys()) {
|
for (const nodeId of map.nodes.keys()) {
|
||||||
if (nodeId === startId || nodeId === endId) continue;
|
if (nodeId === startId || nodeId === endId) continue;
|
||||||
|
|
@ -177,7 +190,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have crossing edges in wild→wild transitions', () => {
|
it("should not have crossing edges in wild→wild transitions", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
||||||
const wildToWildTransitions = [
|
const wildToWildTransitions = [
|
||||||
{ src: 1, tgt: 2 },
|
{ src: 1, tgt: 2 },
|
||||||
|
|
@ -215,7 +228,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have crossing edges in wild→settlement transitions', () => {
|
it("should not have crossing edges in wild→settlement transitions", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
||||||
const wildToSettlementTransitions = [
|
const wildToSettlementTransitions = [
|
||||||
{ src: 2, tgt: 3 },
|
{ src: 2, tgt: 3 },
|
||||||
|
|
@ -252,7 +265,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not have crossing edges in settlement→wild transitions', () => {
|
it("should not have crossing edges in settlement→wild transitions", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
||||||
const settlementToWildTransitions = [
|
const settlementToWildTransitions = [
|
||||||
{ src: 3, tgt: 4 },
|
{ src: 3, tgt: 4 },
|
||||||
|
|
@ -289,7 +302,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should assign encounters to all non-Start/End nodes', () => {
|
it("should assign encounters to all non-Start/End nodes", () => {
|
||||||
const map = generatePointCrawlMap(createRNG(456), encounters);
|
const map = generatePointCrawlMap(createRNG(456), encounters);
|
||||||
|
|
||||||
for (const node of map.nodes.values()) {
|
for (const node of map.nodes.values()) {
|
||||||
|
|
@ -298,14 +311,17 @@ describe('generatePointCrawlMap', () => {
|
||||||
expect(node.encounter).toBeUndefined();
|
expect(node.encounter).toBeUndefined();
|
||||||
} else {
|
} else {
|
||||||
// All other nodes (minion/elite/event/camp/shop/curio) must have encounters
|
// All other nodes (minion/elite/event/camp/shop/curio) must have encounters
|
||||||
expect(node.encounter, `Node ${node.id} (${node.type}) should have encounter data`).toBeDefined();
|
expect(
|
||||||
|
node.encounter,
|
||||||
|
`Node ${node.id} (${node.type}) should have encounter data`,
|
||||||
|
).toBeDefined();
|
||||||
expect(node.encounter!.name).toBeTruthy();
|
expect(node.encounter!.name).toBeTruthy();
|
||||||
expect(node.encounter!.description).toBeTruthy();
|
expect(node.encounter!.description).toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should assign encounters to all nodes across multiple seeds', () => {
|
it("should assign encounters to all nodes across multiple seeds", () => {
|
||||||
// Test multiple seeds to ensure no random failure
|
// Test multiple seeds to ensure no random failure
|
||||||
for (let seed = 0; seed < 20; seed++) {
|
for (let seed = 0; seed < 20; seed++) {
|
||||||
const map = generatePointCrawlMap(createRNG(seed), encounters);
|
const map = generatePointCrawlMap(createRNG(seed), encounters);
|
||||||
|
|
@ -314,14 +330,17 @@ describe('generatePointCrawlMap', () => {
|
||||||
if (node.type === MapNodeType.Start || node.type === MapNodeType.End) {
|
if (node.type === MapNodeType.Start || node.type === MapNodeType.End) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
expect(node.encounter, `Seed ${seed}: Node ${node.id} (${node.type}) missing encounter`).toBeDefined();
|
expect(
|
||||||
|
node.encounter,
|
||||||
|
`Seed ${seed}: Node ${node.id} (${node.type}) missing encounter`,
|
||||||
|
).toBeDefined();
|
||||||
expect(node.encounter!.name).toBeTruthy();
|
expect(node.encounter!.name).toBeTruthy();
|
||||||
expect(node.encounter!.description).toBeTruthy();
|
expect(node.encounter!.description).toBeTruthy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should minimize same-layer repetitions in wild layer pairs', () => {
|
it("should minimize same-layer repetitions in wild layer pairs", () => {
|
||||||
// Test that wild layers in pairs (1-2, 4-5, 7-8) have minimal duplicate types within each layer
|
// Test that wild layers in pairs (1-2, 4-5, 7-8) have minimal duplicate types within each layer
|
||||||
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
||||||
const wildPairIndices = [
|
const wildPairIndices = [
|
||||||
|
|
@ -335,12 +354,12 @@ describe('generatePointCrawlMap', () => {
|
||||||
const layer2 = map.layers[layer2Idx];
|
const layer2 = map.layers[layer2Idx];
|
||||||
|
|
||||||
// Count repetitions in layer 1
|
// Count repetitions in layer 1
|
||||||
const layer1Types = layer1.nodeIds.map(id => map.nodes.get(id)!.type);
|
const layer1Types = layer1.nodeIds.map((id) => map.nodes.get(id)!.type);
|
||||||
const layer1Unique = new Set(layer1Types).size;
|
const layer1Unique = new Set(layer1Types).size;
|
||||||
const layer1Repetitions = layer1Types.length - layer1Unique;
|
const layer1Repetitions = layer1Types.length - layer1Unique;
|
||||||
|
|
||||||
// Count repetitions in layer 2
|
// Count repetitions in layer 2
|
||||||
const layer2Types = layer2.nodeIds.map(id => map.nodes.get(id)!.type);
|
const layer2Types = layer2.nodeIds.map((id) => map.nodes.get(id)!.type);
|
||||||
const layer2Unique = new Set(layer2Types).size;
|
const layer2Unique = new Set(layer2Types).size;
|
||||||
const layer2Repetitions = layer2Types.length - layer2Unique;
|
const layer2Repetitions = layer2Types.length - layer2Unique;
|
||||||
|
|
||||||
|
|
@ -351,7 +370,7 @@ describe('generatePointCrawlMap', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should minimize adjacent repetitions in wild→wild connections', () => {
|
it("should minimize adjacent repetitions in wild→wild connections", () => {
|
||||||
// Test that wild nodes connected by wild→wild edges have different types
|
// Test that wild nodes connected by wild→wild edges have different types
|
||||||
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
const map = generatePointCrawlMap(createRNG(12345), encounters);
|
||||||
const wildToWildPairs = [
|
const wildToWildPairs = [
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
|
||||||
|
"rootDir": ".",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"],
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
},
|
||||||
"exclude": ["node_modules", "dist", "tests"]
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { defineConfig } from 'tsup';
|
import { defineConfig } from "tsup";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from "url";
|
||||||
import * as fs from 'fs';
|
import * as fs from "fs";
|
||||||
import * as path from 'path';
|
import * as path from "path";
|
||||||
import {csvLoader} from 'inline-schema/csv-loader/esbuild';
|
import { csvLoader } from "inline-schema/csv-loader/esbuild";
|
||||||
import {yarnSpinnerPlugin} from 'yarn-spinner-loader/esbuild';
|
import { yarnSpinnerPlugin } from "yarn-spinner-loader/esbuild";
|
||||||
import type { Plugin } from 'esbuild';
|
import type { Plugin } from "esbuild";
|
||||||
|
|
||||||
const srcDir = fileURLToPath(new URL('./src', import.meta.url));
|
const srcDir = fileURLToPath(new URL("./src", import.meta.url));
|
||||||
const samplesDir = fileURLToPath(new URL('./src/samples', import.meta.url));
|
const samplesDir = fileURLToPath(new URL("./src/samples", import.meta.url));
|
||||||
|
|
||||||
// Auto-discover samples entry points
|
// Auto-discover samples entry points
|
||||||
function getSamplesEntries(): Record<string, string> {
|
function getSamplesEntries(): Record<string, string> {
|
||||||
|
|
@ -18,13 +18,13 @@ function getSamplesEntries(): Record<string, string> {
|
||||||
const fullPath = path.join(samplesDir, item);
|
const fullPath = path.join(samplesDir, item);
|
||||||
if (fs.statSync(fullPath).isDirectory()) {
|
if (fs.statSync(fullPath).isDirectory()) {
|
||||||
// Directory sample (e.g. boop) - look for index.ts
|
// Directory sample (e.g. boop) - look for index.ts
|
||||||
const indexPath = path.join(fullPath, 'index.ts');
|
const indexPath = path.join(fullPath, "index.ts");
|
||||||
if (fs.existsSync(indexPath)) {
|
if (fs.existsSync(indexPath)) {
|
||||||
entries[item] = indexPath;
|
entries[item] = indexPath;
|
||||||
}
|
}
|
||||||
} else if (item.endsWith('.ts')) {
|
} else if (item.endsWith(".ts")) {
|
||||||
// Single file sample (e.g. tic-tac-toe.ts)
|
// Single file sample (e.g. tic-tac-toe.ts)
|
||||||
entries[item.replace('.ts', '')] = fullPath;
|
entries[item.replace(".ts", "")] = fullPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
|
|
@ -37,20 +37,20 @@ const samplesEntries = getSamplesEntries();
|
||||||
*/
|
*/
|
||||||
function rewriteBoardgameImports(): Plugin {
|
function rewriteBoardgameImports(): Plugin {
|
||||||
return {
|
return {
|
||||||
name: 'rewrite-boardgame-imports',
|
name: "rewrite-boardgame-imports",
|
||||||
setup(build) {
|
setup(build) {
|
||||||
build.onResolve({ filter: /^@\/(core|utils)\// }, args => {
|
build.onResolve({ filter: /^@\/(core|utils)\// }, (args) => {
|
||||||
// Mark these as external and rewrite to 'boardgame-core'
|
// Mark these as external and rewrite to 'boardgame-core'
|
||||||
return {
|
return {
|
||||||
path: 'boardgame-core',
|
path: "boardgame-core",
|
||||||
external: true,
|
external: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also handle @/index imports
|
// Also handle @/index imports
|
||||||
build.onResolve({ filter: /^@\/index$/ }, args => {
|
build.onResolve({ filter: /^@\/index$/ }, (args) => {
|
||||||
return {
|
return {
|
||||||
path: 'boardgame-core',
|
path: "boardgame-core",
|
||||||
external: true,
|
external: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -60,11 +60,25 @@ function rewriteBoardgameImports(): Plugin {
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: samplesEntries,
|
entry: samplesEntries,
|
||||||
format: ['esm'],
|
format: ["esm"],
|
||||||
dts: true,
|
dts: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
outDir: 'dist/samples',
|
outDir: "dist/samples",
|
||||||
external: ['@preact/signals-core', 'mutative', 'inline-schema', 'boardgame-core'],
|
external: [
|
||||||
esbuildPlugins: [csvLoader({ writeToDisk: true }), rewriteBoardgameImports(), yarnSpinnerPlugin()],
|
"@preact/signals-core",
|
||||||
|
"mutative",
|
||||||
|
"inline-schema",
|
||||||
|
"boardgame-core",
|
||||||
|
],
|
||||||
|
esbuildPlugins: [
|
||||||
|
csvLoader({ writeToDisk: true }),
|
||||||
|
rewriteBoardgameImports(),
|
||||||
|
yarnSpinnerPlugin(),
|
||||||
|
],
|
||||||
|
esbuildOptions(options) {
|
||||||
|
options.alias = {
|
||||||
|
"@": srcDir,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue