refactor: decouple card effects from card data in desert sample
Moves card effects from `card.csv` to a dedicated `cardEffect.csv` file. This allows for more granular control over card triggers (onPlay, onDraw, onDiscard) and targets, improving the data model for the slay-the-spire-like sample. Also updates triggers and tests to reflect this new structure.
This commit is contained in:
parent
3840c3d739
commit
2f2e4e56b5
|
|
@ -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,剑,onPlay,target,[attack;2];[attack;2]
|
||||||
|
greataxe,长斧,onPlay,all,[attack;5]
|
||||||
|
spear,长枪,onPlay,target,[attack;2];[attack;2];[attack;2]
|
||||||
|
dagger,短刀,onPlay,target,[attack;3];[attack;3]
|
||||||
|
dart,飞镖,onPlay,target,[attack;1]
|
||||||
|
dart-draw,飞镖,onPlay,self,[draw;1]
|
||||||
|
crossbow,十字弩,onPlay,target,[attack;6]
|
||||||
|
crossbow-combo,十字弩,onPlay,self,[crossbow;0]
|
||||||
|
shield,盾,onPlay,self,[defend;3]
|
||||||
|
hat,斗笠,onPlay,self,[defend;8]
|
||||||
|
cape,披风,onPlay,self,[defend;2];[defendNext;2]
|
||||||
|
bracer,护腕,onPlay,self,[defend;1];[draw;1]
|
||||||
|
greatshield,大盾,onPlay,self,[defend;5]
|
||||||
|
chainmail,锁子甲,onPlay,self,[damageReduce;3]
|
||||||
|
bandage,绷带,onPlay,self,[removeWound;1]
|
||||||
|
poisonPotion,淬毒药剂,onPlay,self,[attackBuff;2]
|
||||||
|
fortifyPotion,强固药剂,onPlay,self,[defendBuff;2]
|
||||||
|
vitalityPotion,活力药剂,onPlay,self,[gainEnergy;1]
|
||||||
|
focusPotion,集中药剂,onPlay,self,[draw;2]
|
||||||
|
healingPotion,治疗药剂,onPlay,self,[removeWound;3]
|
||||||
|
waterBag,水袋,onPlay,self,[energyNext;1];[drawNext;2]
|
||||||
|
rope,绳索,onPlay,self,[defendBuffUntilPlay;2]
|
||||||
|
belt,腰带,onPlay,self,[drawChoice;1]
|
||||||
|
torch,火把,onPlay,self,[burnForEnergy;1]
|
||||||
|
whetstone,磨刀石,onPlay,self,[attackBuffUntilPlay;3]
|
||||||
|
blacksmithHammer,铁匠锤,onPlay,self,[transformRandom;1]
|
||||||
|
venom,蛇毒,onDiscard,self,[attack;3]
|
||||||
|
curse,诅咒,onDraw,self,[curse;1]
|
||||||
|
static,静电,onDraw,self,[static;1]
|
||||||
|
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;
|
||||||
|
|
@ -1,240 +1,327 @@
|
||||||
import {CombatGameContext} from "./types";
|
import { CombatGameContext } from "./types";
|
||||||
import {
|
import {
|
||||||
addEntityEffect,
|
addEntityEffect,
|
||||||
addItemEffect,
|
addItemEffect,
|
||||||
getAliveEnemies, onEntityPostureDamage,
|
getAliveEnemies,
|
||||||
onEntityEffectUpkeep,
|
onEntityPostureDamage,
|
||||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost, getCombatEntity, getEffectTargets
|
onEntityEffectUpkeep,
|
||||||
|
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";
|
||||||
import {createMiddlewareChain} from "@/utils/middleware";
|
import { createMiddlewareChain } from "@/utils/middleware";
|
||||||
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
import {getAdjacentItems} from "@/samples/slay-the-spire-like/system/grid-inventory";
|
import { getAdjacentItems } 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";
|
||||||
|
|
||||||
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(
|
||||||
await ctx.game.produceAsync(draft => draft.player.energy = draft.player.maxEnergy);
|
(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 => {
|
}),
|
||||||
await ctx.game.produceAsync(draft => {
|
onShuffle: createTrigger("onShuffle", async (ctx) => {
|
||||||
const {cards, regions} = draft.player.deck;
|
await ctx.game.produceAsync((draft) => {
|
||||||
for(const cardId of Object.values(regions.discardPile.childIds))
|
const { cards, regions } = draft.player.deck;
|
||||||
moveToRegion(cards[cardId], regions.discardPile, regions.drawPile);
|
for (const cardId of Object.values(regions.discardPile.childIds))
|
||||||
shuffle(regions.drawPile, cards, ctx.game.rng);
|
moveToRegion(cards[cardId], regions.discardPile, regions.drawPile);
|
||||||
|
shuffle(regions.drawPile, cards, ctx.game.rng);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onCardPlayed: createTrigger("onCardPlayed", async (ctx) => {
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
const { cards, regions } = draft.player.deck;
|
||||||
|
const card = cards[ctx.cardId];
|
||||||
|
payCardCost(
|
||||||
|
draft.player,
|
||||||
|
card.cardData.costType,
|
||||||
|
card.cardData.costCount,
|
||||||
|
card.itemId,
|
||||||
|
draft.inventory,
|
||||||
|
);
|
||||||
|
moveToRegion(card, regions.hand, regions.discardPile);
|
||||||
|
onItemPlay(draft.player, card.itemId);
|
||||||
|
});
|
||||||
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
|
const card = cards[ctx.cardId];
|
||||||
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
|
if (trigger !== "onPlay") continue;
|
||||||
|
for (const [effect, stacks] of effects)
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
}),
|
}
|
||||||
onCardPlayed: createTrigger("onCardPlayed", async ctx => {
|
}),
|
||||||
await ctx.game.produceAsync(draft => {
|
onCardDiscarded: createTrigger("onCardDiscarded", async (ctx) => {
|
||||||
const {cards, regions} = draft.player.deck;
|
await ctx.game.produceAsync((draft) => {
|
||||||
const card = cards[ctx.cardId];
|
const { cards, regions } = draft.player.deck;
|
||||||
payCardCost(draft.player, card.cardData.costType, card.cardData.costCount, card.itemId, draft.inventory);
|
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||||
moveToRegion(card, regions.hand, regions.discardPile);
|
onItemDiscard(draft.player, cards[ctx.cardId].itemId);
|
||||||
onItemPlay(draft.player, card.itemId);
|
});
|
||||||
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
|
const card = cards[ctx.cardId];
|
||||||
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
|
if (trigger !== "onDiscard") continue;
|
||||||
|
for (const [effect, stacks] of effects)
|
||||||
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
cardId: ctx.cardId,
|
||||||
|
sourceEntityKey: source,
|
||||||
});
|
});
|
||||||
const {cards, regions} = ctx.game.value.player.deck;
|
}
|
||||||
const card = cards[ctx.cardId];
|
}),
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
onCardDrawn: createTrigger("onCardDrawn", async (ctx) => {
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
await ctx.game.produceAsync((draft) => {
|
||||||
if(trigger !== 'onPlay') continue;
|
const { cards, regions } = draft.player.deck;
|
||||||
for(const entity of getEffectTargets(target, ctx.game, ctx.targetId))
|
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source, targetId: ctx.targetId});
|
});
|
||||||
}
|
const { cards, regions } = ctx.game.value.player.deck;
|
||||||
}),
|
const card = cards[ctx.cardId];
|
||||||
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
|
const source = ctx.sourceEntityKey ?? "player";
|
||||||
await ctx.game.produceAsync(draft => {
|
for (const { trigger, target, effects } of card.cardData.effects) {
|
||||||
const {cards, regions} = draft.player.deck;
|
if (trigger !== "onDraw") continue;
|
||||||
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
for (const [effect, stacks] of effects)
|
||||||
onItemDiscard(draft.player, cards[ctx.cardId].itemId);
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
cardId: ctx.cardId,
|
||||||
|
sourceEntityKey: source,
|
||||||
});
|
});
|
||||||
const {cards, regions} = ctx.game.value.player.deck;
|
}
|
||||||
const card = cards[ctx.cardId];
|
}),
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
onDraw: createTrigger("onDraw", async (ctx) => {
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
let toDraw = ctx.count;
|
||||||
if(trigger !== 'onDiscard') continue;
|
while (toDraw > 0) {
|
||||||
for(const entity of getEffectTargets(target, ctx.game))
|
let inDraw =
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
|
ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
}
|
if (inDraw <= 0) await triggers.onShuffle.execute(ctx.game, {});
|
||||||
}),
|
|
||||||
onCardDrawn: createTrigger("onCardDrawn", async ctx => {
|
inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
||||||
await ctx.game.produceAsync(draft => {
|
if (inDraw <= 0) break;
|
||||||
const {cards, regions} = draft.player.deck;
|
|
||||||
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
const children = ctx.game.value.player.deck.regions.drawPile.childIds;
|
||||||
|
const cardId = children[children.length - 1];
|
||||||
|
await triggers.onCardDrawn.execute(ctx.game, { cardId });
|
||||||
|
toDraw--;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onEffectApplied: createTrigger("onEffectApplied", async (ctx) => {
|
||||||
|
if (ctx.effect.lifecycle === "instant") return;
|
||||||
|
|
||||||
|
if (ctx.effect.lifecycle.startsWith("item")) {
|
||||||
|
if (ctx.cardId) {
|
||||||
|
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
||||||
|
const nearby = getAdjacentItems<GameItemMeta>(
|
||||||
|
ctx.game.value.inventory,
|
||||||
|
card.itemId,
|
||||||
|
);
|
||||||
|
for (const itemId of nearby.keys()) {
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
||||||
});
|
});
|
||||||
const {cards, regions} = ctx.game.value.player.deck;
|
}
|
||||||
const card = cards[ctx.cardId];
|
|
||||||
const source = ctx.sourceEntityKey ?? "player";
|
|
||||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
|
||||||
if(trigger !== 'onDraw') continue;
|
|
||||||
for(const entity of getEffectTargets(target, ctx.game))
|
|
||||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId, sourceEntityKey: source});
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onDraw: createTrigger("onDraw", async ctx => {
|
|
||||||
let toDraw = ctx.count;
|
|
||||||
while(toDraw > 0){
|
|
||||||
let inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
|
||||||
if(inDraw <= 0) await triggers.onShuffle.execute(ctx.game,{});
|
|
||||||
|
|
||||||
inDraw = ctx.game.value.player.deck.regions.drawPile.childIds.length;
|
|
||||||
if(inDraw <= 0) break;
|
|
||||||
|
|
||||||
const children = ctx.game.value.player.deck.regions.drawPile.childIds;
|
|
||||||
const cardId = children[children.length - 1];
|
|
||||||
await triggers.onCardDrawn.execute(ctx.game,{cardId});
|
|
||||||
toDraw--;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onEffectApplied: createTrigger("onEffectApplied", async ctx => {
|
|
||||||
if(ctx.effect.lifecycle === 'instant') return;
|
|
||||||
|
|
||||||
if(ctx.effect.lifecycle.startsWith("item")) {
|
|
||||||
if(ctx.cardId){
|
|
||||||
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
|
||||||
const nearby = getAdjacentItems<GameItemMeta>(ctx.game.value.inventory, card.itemId);
|
|
||||||
for(const itemId of nearby.keys()){
|
|
||||||
await ctx.game.produceAsync(draft => {
|
|
||||||
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.game.produceAsync(draft => {
|
|
||||||
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
|
||||||
if(entity) addEntityEffect(entity, ctx.effect, ctx.stacks);
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
onHpChange: createTrigger("onHpChange", async ctx => {
|
|
||||||
await ctx.game.produceAsync(draft => {
|
|
||||||
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
|
||||||
if(!entity) return;
|
|
||||||
entity.hp += ctx.amount;
|
|
||||||
entity.isAlive = entity.hp > 0;
|
|
||||||
draft.result = !draft.player.isAlive ? "defeat" : draft.enemies.every(e => !e.isAlive) ? "victory" : null;
|
|
||||||
});
|
|
||||||
if(ctx.game.value.result) throw ctx.game.value;
|
|
||||||
}),
|
|
||||||
onDamage: createTrigger("onDamage", async ctx => {
|
|
||||||
const entity = ctx.entityKey === "player" ? ctx.game.value.player : ctx.game.value.enemies.find(e => e.id === ctx.entityKey);
|
|
||||||
if(!entity || !entity.isAlive) return;
|
|
||||||
const dealt = Math.min(Math.max(0,entity.hp), ctx.amount - (ctx.prevented || 0));
|
|
||||||
await ctx.game.produceAsync(draft => {
|
|
||||||
onEntityPostureDamage(entity, dealt);
|
|
||||||
});
|
|
||||||
await triggers.onHpChange.execute(ctx.game,{entityKey: ctx.entityKey, amount: -dealt});
|
|
||||||
}),
|
|
||||||
onEnemyIntent: createTrigger("onEnemyIntent", async ctx => {
|
|
||||||
const enemy = ctx.game.value.enemies.find(e => e.id === ctx.enemyId);
|
|
||||||
if(!enemy || !enemy.isAlive) return;
|
|
||||||
|
|
||||||
const intent = enemy.currentIntent;
|
|
||||||
if(!intent) return;
|
|
||||||
|
|
||||||
const source = ctx.sourceEntityKey ?? enemy.id;
|
|
||||||
for(const [target, effect, stacks] of intent.effects){
|
|
||||||
for(const entity of getEffectTargets(target, ctx.game))
|
|
||||||
await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, sourceEntityKey: source });
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
onIntentUpdate: createTrigger("onIntentUpdate", async ctx => {
|
|
||||||
await ctx.game.produceAsync(draft => {
|
|
||||||
const enemy = draft.enemies.find(e => e.id === ctx.enemyId);
|
|
||||||
if(!enemy) return;
|
|
||||||
|
|
||||||
const intent = enemy.currentIntent;
|
|
||||||
if(!intent) return;
|
|
||||||
|
|
||||||
const nextIntents = intent.nextIntents;
|
|
||||||
if(nextIntents.length > 0){
|
|
||||||
const nextIndex = ctx.game.rng.nextInt(nextIntents.length);
|
|
||||||
enemy.currentIntent = nextIntents[nextIndex];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
return triggers;
|
|
||||||
}
|
|
||||||
export type Triggers = ReturnType<typeof createTriggers>
|
|
||||||
export function createStartWith(build: (triggers: Triggers) => void){
|
|
||||||
const triggers = createTriggers();
|
|
||||||
build(triggers);
|
|
||||||
return async function(game: CombatGameContext){
|
|
||||||
await triggers.onCombatStart.execute(game,{});
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
await triggers.onTurnStart.execute(game, {entityKey: "player"});
|
|
||||||
while (true) {
|
|
||||||
const action = await promptMainAction(game);
|
|
||||||
if (action.action === "end-turn") break;
|
|
||||||
if (action.action === "play") {
|
|
||||||
await triggers.onCardPlayed.execute(game, action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await triggers.onTurnEnd.execute(game, {entityKey: "player"});
|
|
||||||
|
|
||||||
for (const enemy of getAliveEnemies(game.value)) {
|
|
||||||
await triggers.onTurnStart.execute(game, {entityKey: enemy.id});
|
|
||||||
}
|
|
||||||
for (const enemy of getAliveEnemies(game.value)) {
|
|
||||||
await triggers.onEnemyIntent.execute(game, {enemyId: enemy.id});
|
|
||||||
await triggers.onIntentUpdate.execute(game, {enemyId: enemy.id});
|
|
||||||
}
|
|
||||||
for (const enemy of getAliveEnemies(game.value)) {
|
|
||||||
await triggers.onTurnEnd.execute(game, {entityKey: enemy.id});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}catch(e){
|
|
||||||
if(e === game.value) return game.value.result;
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? draft.player
|
||||||
|
: draft.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
|
if (entity) addEntityEffect(entity, ctx.effect, ctx.stacks);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onHpChange: createTrigger("onHpChange", async (ctx) => {
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? draft.player
|
||||||
|
: draft.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
|
if (!entity) return;
|
||||||
|
entity.hp += ctx.amount;
|
||||||
|
entity.isAlive = entity.hp > 0;
|
||||||
|
draft.result = !draft.player.isAlive
|
||||||
|
? "defeat"
|
||||||
|
: draft.enemies.every((e) => !e.isAlive)
|
||||||
|
? "victory"
|
||||||
|
: null;
|
||||||
|
});
|
||||||
|
if (ctx.game.value.result) throw ctx.game.value;
|
||||||
|
}),
|
||||||
|
onDamage: createTrigger("onDamage", async (ctx) => {
|
||||||
|
const entity =
|
||||||
|
ctx.entityKey === "player"
|
||||||
|
? ctx.game.value.player
|
||||||
|
: ctx.game.value.enemies.find((e) => e.id === ctx.entityKey);
|
||||||
|
if (!entity || !entity.isAlive) return;
|
||||||
|
const dealt = Math.min(
|
||||||
|
Math.max(0, entity.hp),
|
||||||
|
ctx.amount - (ctx.prevented || 0),
|
||||||
|
);
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
onEntityPostureDamage(entity, dealt);
|
||||||
|
});
|
||||||
|
await triggers.onHpChange.execute(ctx.game, {
|
||||||
|
entityKey: ctx.entityKey,
|
||||||
|
amount: -dealt,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
onEnemyIntent: createTrigger("onEnemyIntent", async (ctx) => {
|
||||||
|
const enemy = ctx.game.value.enemies.find((e) => e.id === ctx.enemyId);
|
||||||
|
if (!enemy || !enemy.isAlive) return;
|
||||||
|
|
||||||
|
const intent = enemy.currentIntent;
|
||||||
|
if (!intent) return;
|
||||||
|
|
||||||
|
const source = ctx.sourceEntityKey ?? enemy.id;
|
||||||
|
for (const [target, effect, stacks] of intent.effects) {
|
||||||
|
for (const entity of getEffectTargets(target, ctx.game))
|
||||||
|
await triggers.onEffectApplied.execute(ctx.game, {
|
||||||
|
effect,
|
||||||
|
entityKey: entity.id,
|
||||||
|
stacks,
|
||||||
|
sourceEntityKey: source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
onIntentUpdate: createTrigger("onIntentUpdate", async (ctx) => {
|
||||||
|
await ctx.game.produceAsync((draft) => {
|
||||||
|
const enemy = draft.enemies.find((e) => e.id === ctx.enemyId);
|
||||||
|
if (!enemy) return;
|
||||||
|
|
||||||
|
const intent = enemy.currentIntent;
|
||||||
|
if (!intent) return;
|
||||||
|
|
||||||
|
const nextIntents = intent.nextIntents;
|
||||||
|
if (nextIntents.length > 0) {
|
||||||
|
const nextIndex = ctx.game.rng.nextInt(nextIntents.length);
|
||||||
|
enemy.currentIntent = nextIntents[nextIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return triggers;
|
||||||
|
}
|
||||||
|
export type Triggers = ReturnType<typeof createTriggers>;
|
||||||
|
export function createStartWith(build: (triggers: Triggers) => void) {
|
||||||
|
const triggers = createTriggers();
|
||||||
|
build(triggers);
|
||||||
|
return async function (game: CombatGameContext) {
|
||||||
|
await triggers.onCombatStart.execute(game, {});
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
await triggers.onTurnStart.execute(game, { entityKey: "player" });
|
||||||
|
while (true) {
|
||||||
|
const action = await promptMainAction(game);
|
||||||
|
if (action.action === "end-turn") break;
|
||||||
|
if (action.action === "play") {
|
||||||
|
await triggers.onCardPlayed.execute(game, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await triggers.onTurnEnd.execute(game, { entityKey: "player" });
|
||||||
|
|
||||||
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
|
await triggers.onTurnStart.execute(game, { entityKey: enemy.id });
|
||||||
|
}
|
||||||
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
|
await triggers.onEnemyIntent.execute(game, { enemyId: enemy.id });
|
||||||
|
await triggers.onIntentUpdate.execute(game, { enemyId: enemy.id });
|
||||||
|
}
|
||||||
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
|
await triggers.onTurnEnd.execute(game, { entityKey: enemy.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e === game.value) return game.value.result;
|
||||||
|
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;
|
||||||
return {
|
};
|
||||||
use,
|
function createTrigger<TKey extends keyof TriggerTypes>(
|
||||||
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
|
event: TKey,
|
||||||
const param = {...ctx, game, event};
|
fallback?: (ctx: TriggerContext<TKey>) => Promise<void>,
|
||||||
await execute(param);
|
) {
|
||||||
return param;
|
const { use, execute } = createMiddlewareChain<TriggerContext<TKey>, void>(
|
||||||
},
|
fallback,
|
||||||
}
|
);
|
||||||
|
return {
|
||||||
|
use,
|
||||||
|
execute: async (game: CombatGameContext, ctx: TriggerTypes[TKey]) => {
|
||||||
|
const param = { ...ctx, game, event };
|
||||||
|
await execute(param);
|
||||||
|
return param;
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1,16 +1,26 @@
|
||||||
export type EffectData = {
|
export type EffectData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
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;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly intents: readonly IntentData[];
|
readonly intents: readonly IntentData[];
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CardType = "item" | "status";
|
export type CardType = "item" | "status";
|
||||||
|
|
@ -18,44 +28,62 @@ 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 CardData = {
|
|
||||||
readonly id: string;
|
|
||||||
readonly name: string;
|
|
||||||
readonly desc: string;
|
|
||||||
readonly type: CardType;
|
|
||||||
readonly costType: CardCostType;
|
|
||||||
readonly costCount: number;
|
|
||||||
readonly targetType: CardTargetType;
|
|
||||||
readonly effects: readonly [CardEffectTrigger, CardEffectTarget, EffectData, number][];
|
|
||||||
};
|
|
||||||
export type CardEffectTrigger = "onPlay" | "onDraw" | "onDiscard";
|
export type CardEffectTrigger = "onPlay" | "onDraw" | "onDiscard";
|
||||||
export type CardEffectTarget = "self" | "target" | "all" | "random"
|
export type CardEffectTarget = "self" | "target" | "all" | "random";
|
||||||
|
|
||||||
export type EncounterType = "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
export type CardEffect = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly trigger: CardEffectTrigger;
|
||||||
|
readonly target: CardEffectTarget;
|
||||||
|
readonly effects: readonly [EffectData, number][];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CardData = {
|
||||||
|
readonly id: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly desc: string;
|
||||||
|
readonly type: CardType;
|
||||||
|
readonly costType: CardCostType;
|
||||||
|
readonly costCount: number;
|
||||||
|
readonly targetType: CardTargetType;
|
||||||
|
readonly effects: readonly CardEffect[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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 [
|
||||||
readonly dialogue: string;
|
data: EnemyData,
|
||||||
|
hp: number,
|
||||||
|
effects: [EffectData, stacks: number][],
|
||||||
|
][];
|
||||||
|
readonly dialogue: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntentData = {
|
export type IntentData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly enemy: EnemyData;
|
readonly enemy: EnemyData;
|
||||||
readonly initialIntent: boolean;
|
readonly initialIntent: boolean;
|
||||||
readonly nextIntents: readonly IntentData[];
|
readonly nextIntents: readonly IntentData[];
|
||||||
readonly brokenIntent: readonly IntentData[];
|
readonly brokenIntent: readonly IntentData[];
|
||||||
readonly effects: readonly [EffectTarget, EffectData, number][];
|
readonly effects: readonly [EffectTarget, EffectData, number][];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ItemData = {
|
export type ItemData = {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly type: string;
|
readonly type: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly shape: string;
|
readonly shape: string;
|
||||||
readonly card: CardData;
|
readonly card: CardData;
|
||||||
readonly price: number;
|
readonly price: number;
|
||||||
readonly description: string;
|
readonly description: string;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,8 +1,8 @@
|
||||||
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";
|
||||||
|
|
||||||
describe('data import', () => {
|
describe("data import", () => {
|
||||||
it('should import properly', () => {
|
it("should import properly", () => {
|
||||||
expect(data.desert.effects).toBeDefined();
|
expect(data.desert.getEffects).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue