Compare commits

...

7 Commits

Author SHA1 Message Date
hypercross a5e2e4888e refactor(slay-the-spire-like): update combat targeting and effect logic
Refactor combat targeting types and effect application logic in the
Slay the Spire-like sample.

- Update `getEffectTargets` to use `IntentEffectTarget` and more
  descriptive target keys (e.g., `eachEnemy`, `randomEnemy`).
- Update `promptMainAction` to use `enemy` instead of `single` for
  card target types.
- Refactor `addInstantEffectTriggers` to remove unused effect loading
  and improve enemy/card instantiation logic
2026-04-22 19:35:37 +08:00
hypercross 38fd46618e refactor(slay-the-spire-like): use data definitions in system types
Remove redundant type definitions in `system/types.ts` and instead
import them from the generated CSV declaration files. Also update
declaration files to use single quotes for consistency with project
style rules.
2026-04-22 19:26:58 +08:00
hypercross 72647a8268 refactor(samples): export desert data types
Export type definitions from desert CSV declaration files and
update the content module to use these exported types instead of
generic system types.
2026-04-22 16:48:54 +08:00
hypercross dbbbba14e2 refactor(slay-the-spire-like): update card effect and intent schemas
Refactor the data schemas for desert card effects and intents to use
dedicated type aliases for triggers, targets, and effect lists. This
improves type safety and consistency across the sample data.
2026-04-22 16:13:48 +08:00
hypercross 888b7b822d refactor(slay-the-spire-like): extract types in desert data schemas 2026-04-22 16:00:15 +08:00
hypercross bb4528cd27 refactor(samples): update desert card and effect schemas 2026-04-22 15:59:41 +08:00
hypercross 877cc9d779 refactor: update schema definitions in slay-the-spire-like data
Refactor CSV schema headers in the desert sample to use named type
aliases instead of inline union strings. This improves readability and
maintains consistency with the project's use of `inline-schema`.

Changes include:
- Updating `card.csv` to use `CardType`, `CardCostType`, and
  `CardTargetType`.
- Updating `encounter.csv` to use `EncounterType` and `EnemyList`.
- Updating `intent.csv` to use `IntentEffectTarget` and replacing
  `self` with `user` for effect targets.
2026-04-22 15:57:52 +08:00
20 changed files with 378 additions and 397 deletions

View File

@ -2,37 +2,42 @@
# 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
# type CardType = 'item' | 'status'
# type CardCostType = 'energy' | 'uses' | 'none'
# type CardTargetType = 'player' | 'enemy' | 'enemies'
# inject effects = ~cardEffect(card) # inject effects = ~cardEffect(card)
id,name,desc,type,costType,costCount,targetType id,name,desc,type,costType,costCount,targetType
string,string,string,'item'|'status','energy'|'uses'|'none',int,'single'|'none' string,string,string,CardType,CardCostType,int,CardTargetType
sword,剑,【攻击2】【攻击2】,item,energy,1,single sword,剑,【攻击2】【攻击2】,item,energy,1,enemy
greataxe,长斧,对全体【攻击5】,item,energy,2,none greataxe,长斧,对全体【攻击5】,item,energy,2,enemies
spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,single spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,enemy
dagger,短刀,【攻击3】【攻击3】,item,energy,1,single dagger,短刀,【攻击3】【攻击3】,item,energy,1,enemy
dart,飞镖,【攻击1】抓一张牌,item,energy,0,single dart,飞镖,【攻击1】抓一张牌,item,energy,0,enemy
crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,single crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,enemy
shield,盾,【防御3】,item,energy,1,none shield,盾,【防御3】,item,energy,1,player
hat,斗笠,【防御8】,item,energy,2,none hat,斗笠,【防御8】,item,energy,2,player
cape,披风,【防御2】下回合【防御2】,item,energy,1,none cape,披风,【防御2】下回合【防御2】,item,energy,1,player
bracer,护腕,【防御1】抓1张牌,item,energy,0,none bracer,护腕,【防御1】抓1张牌,item,energy,0,player
greatshield,大盾,【防御5】,item,energy,1,none greatshield,大盾,【防御5】,item,energy,1,player
chainmail,锁子甲,本回合受到伤害-3,item,energy,1,none chainmail,锁子甲,本回合受到伤害-3,item,energy,1,player
bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,none bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,player
poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,none poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,player
fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,none fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,player
vitalityPotion,活力药剂,获得1点能量,item,uses,3,none vitalityPotion,活力药剂,获得1点能量,item,uses,3,player
focusPotion,集中药剂,抓2张牌,item,uses,3,none focusPotion,集中药剂,抓2张牌,item,uses,3,player
healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,none healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,player
waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,none waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,player
rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,none rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,player
belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,none belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,player
torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,none torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,player
whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,none whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,player
blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,none blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,player
wound,伤口,无效果占用手牌和牌堆,status,none,0,none wound,伤口,无效果占用手牌和牌堆,status,none,0,player
venom,蛇毒,弃掉时受到3点伤害,status,none,0,none venom,蛇毒,弃掉时受到3点伤害,status,none,0,player
curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,none curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,player
static,静电,在手里时受电击伤害+1,status,none,0,none static,静电,在手里时受电击伤害+1,status,none,0,player
fatigue,疲劳,占用手牌,status,none,0,none fatigue,疲劳,占用手牌,status,none,0,player
vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,none vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,player

1 # cardDesert: unified card definitions for item cards and status cards
2 # type: 'item' = inventory item card, 'status' = status effect card
3 # costType: 'energy' = costs energy per turn, 'uses' = limited uses, 'none' = free
4 # targetType: 'single' = target one enemy, 'none' = no target
5 # type CardType = 'item' | 'status'
6 # type CardCostType = 'energy' | 'uses' | 'none'
7 # type CardTargetType = 'player' | 'enemy' | 'enemies'
8 # inject effects = ~cardEffect(card)
9 id,name,desc,type,costType,costCount,targetType
10 # inject effects = ~cardEffect(card) string,string,string,CardType,CardCostType,int,CardTargetType
11 id,name,desc,type,costType,costCount,targetType sword,剑,【攻击2】【攻击2】,item,energy,1,enemy
12 string,string,string,'item'|'status','energy'|'uses'|'none',int,'single'|'none' greataxe,长斧,对全体【攻击5】,item,energy,2,enemies
13 sword,剑,【攻击2】【攻击2】,item,energy,1,single spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,enemy
14 greataxe,长斧,对全体【攻击5】,item,energy,2,none dagger,短刀,【攻击3】【攻击3】,item,energy,1,enemy
15 spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,single dart,飞镖,【攻击1】抓一张牌,item,energy,0,enemy
16 dagger,短刀,【攻击3】【攻击3】,item,energy,1,single crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,enemy
17 dart,飞镖,【攻击1】抓一张牌,item,energy,0,single shield,盾,【防御3】,item,energy,1,player
18 crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,single hat,斗笠,【防御8】,item,energy,2,player
19 shield,盾,【防御3】,item,energy,1,none cape,披风,【防御2】下回合【防御2】,item,energy,1,player
20 hat,斗笠,【防御8】,item,energy,2,none bracer,护腕,【防御1】抓1张牌,item,energy,0,player
21 cape,披风,【防御2】下回合【防御2】,item,energy,1,none greatshield,大盾,【防御5】,item,energy,1,player
22 bracer,护腕,【防御1】抓1张牌,item,energy,0,none chainmail,锁子甲,本回合受到伤害-3,item,energy,1,player
23 greatshield,大盾,【防御5】,item,energy,1,none bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,player
24 chainmail,锁子甲,本回合受到伤害-3,item,energy,1,none poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,player
25 bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,none fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,player
26 poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,none vitalityPotion,活力药剂,获得1点能量,item,uses,3,player
27 fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,none focusPotion,集中药剂,抓2张牌,item,uses,3,player
28 vitalityPotion,活力药剂,获得1点能量,item,uses,3,none healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,player
29 focusPotion,集中药剂,抓2张牌,item,uses,3,none waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,player
30 healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,none rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,player
31 waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,none belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,player
32 rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,none torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,player
33 belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,none whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,player
34 torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,none blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,player
35 whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,none wound,伤口,无效果占用手牌和牌堆,status,none,0,player
36 blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,none venom,蛇毒,弃掉时受到3点伤害,status,none,0,player
37 wound,伤口,无效果占用手牌和牌堆,status,none,0,none curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,player
38 venom,蛇毒,弃掉时受到3点伤害,status,none,0,none static,静电,在手里时受电击伤害+1,status,none,0,player
39 curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,none fatigue,疲劳,占用手牌,status,none,0,player
40 static,静电,在手里时受电击伤害+1,status,none,0,none vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,player
41
42
43

View File

@ -1,13 +1,17 @@
import type { CardEffect } from './cardEffect.csv'; import type { CardEffect } from './cardEffect.csv';
export type CardType = 'item' | 'status';
export type CardCostType = 'energy' | 'uses' | 'none';
export type CardTargetType = 'player' | 'enemy' | 'enemies';
type CardTable = readonly { type CardTable = readonly {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly desc: string; readonly desc: string;
readonly type: "item" | "status"; readonly type: CardType;
readonly costType: "energy" | "uses" | "none"; readonly costType: CardCostType;
readonly costCount: number; readonly costCount: number;
readonly targetType: "single" | "none"; readonly targetType: CardTargetType;
readonly effects: CardEffect[]; readonly effects: CardEffect[];
}[]; }[];

View File

@ -1,32 +1,36 @@
# type CardEffectTrigger = 'onPlay' | 'onDraw' | 'onDiscard'
# type CardEffectTarget = 'user' | 'eachTarget' | 'eachEnemy' | 'randomEnemy' | 'player'
# type CardEffectList = [effect: @effect; stacks: number][]
id,card,trigger,target,effects id,card,trigger,target,effects
string,@card,'onPlay'|'onDraw'|'onDiscard','self'|'target'|'all'|'random',[@effect;number][] string,@card,CardEffectTrigger,CardEffectTarget,CardEffectList
sword,sword,onPlay,target,[attack;2];[attack;2] sword,sword,onPlay,eachTarget,[attack;2];[attack;2]
greataxe,greataxe,onPlay,all,[attack;5] greataxe,greataxe,onPlay,eachTarget,[attack;5]
spear,spear,onPlay,target,[attack;2];[attack;2];[attack;2] spear,spear,onPlay,eachTarget,[attack;2];[attack;2];[attack;2]
dagger,dagger,onPlay,target,[attack;3];[attack;3] dagger,dagger,onPlay,eachTarget,[attack;3];[attack;3]
dart,dart,onPlay,target,[attack;1] dart,dart,onPlay,eachTarget,[attack;1]
dart-draw,dart,onPlay,self,[draw;1] dart-draw,dart,onPlay,user,[draw;1]
crossbow,crossbow,onPlay,target,[attack;6] crossbow,crossbow,onPlay,eachTarget,[attack;6]
crossbow-combo,crossbow,onPlay,self,[crossbow;0] crossbow-combo,crossbow,onPlay,user,[crossbow;0]
shield,shield,onPlay,self,[defend;3] shield,shield,onPlay,user,[defend;3]
hat,hat,onPlay,self,[defend;8] hat,hat,onPlay,user,[defend;8]
cape,cape,onPlay,self,[defend;2];[defendNext;2] cape,cape,onPlay,user,[defend;2];[defendNext;2]
bracer,bracer,onPlay,self,[defend;1];[draw;1] bracer,bracer,onPlay,user,[defend;1];[draw;1]
greatshield,greatshield,onPlay,self,[defend;5] greatshield,greatshield,onPlay,user,[defend;5]
chainmail,chainmail,onPlay,self,[damageReduce;3] chainmail,chainmail,onPlay,user,[damageReduce;3]
bandage,bandage,onPlay,self,[removeWound;1] bandage,bandage,onPlay,user,[removeWound;1]
poisonPotion,poisonPotion,onPlay,self,[attackBuff;2] poisonPotion,poisonPotion,onPlay,user,[attackBuff;2]
fortifyPotion,fortifyPotion,onPlay,self,[defendBuff;2] fortifyPotion,fortifyPotion,onPlay,user,[defendBuff;2]
vitalityPotion,vitalityPotion,onPlay,self,[gainEnergy;1] vitalityPotion,vitalityPotion,onPlay,user,[gainEnergy;1]
focusPotion,focusPotion,onPlay,self,[draw;2] focusPotion,focusPotion,onPlay,user,[draw;2]
healingPotion,healingPotion,onPlay,self,[removeWound;3] healingPotion,healingPotion,onPlay,user,[removeWound;3]
waterBag,waterBag,onPlay,self,[energyNext;1];[drawNext;2] waterBag,waterBag,onPlay,user,[energyNext;1];[drawNext;2]
rope,rope,onPlay,self,[defendBuffUntilPlay;2] rope,rope,onPlay,user,[defendBuffUntilPlay;2]
belt,belt,onPlay,self,[drawChoice;1] belt,belt,onPlay,user,[drawChoice;1]
torch,torch,onPlay,self,[burnForEnergy;1] torch,torch,onPlay,user,[burnForEnergy;1]
whetstone,whetstone,onPlay,self,[attackBuffUntilPlay;3] whetstone,whetstone,onPlay,user,[attackBuffUntilPlay;3]
blacksmithHammer,blacksmithHammer,onPlay,self,[transformRandom;1] blacksmithHammer,blacksmithHammer,onPlay,user,[transformRandom;1]
venom,venom,onDiscard,self,[attack;3] venom,venom,onDiscard,user,[attack;3]
curse,curse,onDraw,self,[curse;1] curse,curse,onDraw,user,[curse;1]
static,static,onDraw,self,[static;1] static,static,onDraw,user,[static;1]
vultureEye,vultureEye,onDraw,self,[expose;3] vultureEye,vultureEye,onDraw,user,[expose;3]

1 id # type CardEffectTrigger = 'onPlay' | 'onDraw' | 'onDiscard' card trigger target effects
1 # type CardEffectTrigger = 'onPlay' | 'onDraw' | 'onDiscard'
2 # type CardEffectTarget = 'user' | 'eachTarget' | 'eachEnemy' | 'randomEnemy' | 'player'
3 # type CardEffectList = [effect: @effect; stacks: number][]
4 id,card,trigger,target,effects
5 id string,@card,CardEffectTrigger,CardEffectTarget,CardEffectList card trigger target effects
6 string sword,sword,onPlay,eachTarget,[attack;2];[attack;2] @card 'onPlay'|'onDraw'|'onDiscard' 'self'|'target'|'all'|'random' [@effect;number][]
7 sword greataxe,greataxe,onPlay,eachTarget,[attack;5] sword onPlay target [attack;2];[attack;2]
8 greataxe spear,spear,onPlay,eachTarget,[attack;2];[attack;2];[attack;2] greataxe onPlay all [attack;5]
9 spear dagger,dagger,onPlay,eachTarget,[attack;3];[attack;3] spear onPlay target [attack;2];[attack;2];[attack;2]
10 dagger dart,dart,onPlay,eachTarget,[attack;1] dagger onPlay target [attack;3];[attack;3]
11 dart dart-draw,dart,onPlay,user,[draw;1] dart onPlay target [attack;1]
12 dart-draw crossbow,crossbow,onPlay,eachTarget,[attack;6] dart onPlay self [draw;1]
13 crossbow crossbow-combo,crossbow,onPlay,user,[crossbow;0] crossbow onPlay target [attack;6]
14 crossbow-combo shield,shield,onPlay,user,[defend;3] crossbow onPlay self [crossbow;0]
15 shield hat,hat,onPlay,user,[defend;8] shield onPlay self [defend;3]
16 hat cape,cape,onPlay,user,[defend;2];[defendNext;2] hat onPlay self [defend;8]
17 cape bracer,bracer,onPlay,user,[defend;1];[draw;1] cape onPlay self [defend;2];[defendNext;2]
18 bracer greatshield,greatshield,onPlay,user,[defend;5] bracer onPlay self [defend;1];[draw;1]
19 greatshield chainmail,chainmail,onPlay,user,[damageReduce;3] greatshield onPlay self [defend;5]
20 chainmail bandage,bandage,onPlay,user,[removeWound;1] chainmail onPlay self [damageReduce;3]
21 bandage poisonPotion,poisonPotion,onPlay,user,[attackBuff;2] bandage onPlay self [removeWound;1]
22 poisonPotion fortifyPotion,fortifyPotion,onPlay,user,[defendBuff;2] poisonPotion onPlay self [attackBuff;2]
23 fortifyPotion vitalityPotion,vitalityPotion,onPlay,user,[gainEnergy;1] fortifyPotion onPlay self [defendBuff;2]
24 vitalityPotion focusPotion,focusPotion,onPlay,user,[draw;2] vitalityPotion onPlay self [gainEnergy;1]
25 focusPotion healingPotion,healingPotion,onPlay,user,[removeWound;3] focusPotion onPlay self [draw;2]
26 healingPotion waterBag,waterBag,onPlay,user,[energyNext;1];[drawNext;2] healingPotion onPlay self [removeWound;3]
27 waterBag rope,rope,onPlay,user,[defendBuffUntilPlay;2] waterBag onPlay self [energyNext;1];[drawNext;2]
28 rope belt,belt,onPlay,user,[drawChoice;1] rope onPlay self [defendBuffUntilPlay;2]
29 belt torch,torch,onPlay,user,[burnForEnergy;1] belt onPlay self [drawChoice;1]
30 torch whetstone,whetstone,onPlay,user,[attackBuffUntilPlay;3] torch onPlay self [burnForEnergy;1]
31 whetstone blacksmithHammer,blacksmithHammer,onPlay,user,[transformRandom;1] whetstone onPlay self [attackBuffUntilPlay;3]
32 blacksmithHammer venom,venom,onDiscard,user,[attack;3] blacksmithHammer onPlay self [transformRandom;1]
33 venom curse,curse,onDraw,user,[curse;1] venom onDiscard self [attack;3]
34 curse static,static,onDraw,user,[static;1] curse onDraw self [curse;1]
35 static vultureEye,vultureEye,onDraw,user,[expose;3] static onDraw self [static;1]
36 vultureEye vultureEye onDraw self [expose;3]

View File

@ -1,12 +1,16 @@
import type { Card } from './card.csv'; import type { Card } from './card.csv';
import type { Effect } from './effect.csv'; import type { Effect } from './effect.csv';
export type CardEffectTrigger = 'onPlay' | 'onDraw' | 'onDiscard';
export type CardEffectTarget = 'user' | 'eachTarget' | 'eachEnemy' | 'randomEnemy' | 'player';
export type CardEffectList = [effect: Effect, stacks: number][];
type CardEffectTable = readonly { type CardEffectTable = readonly {
readonly id: string; readonly id: string;
readonly card: Card; readonly card: Card;
readonly trigger: "onPlay" | "onDraw" | "onDiscard"; readonly trigger: CardEffectTrigger;
readonly target: "self" | "target" | "all" | "random"; readonly target: CardEffectTarget;
readonly effects: [Effect, number][]; readonly effects: CardEffectList;
}[]; }[];
export type CardEffect = CardEffectTable[number]; export type CardEffect = CardEffectTable[number];

View File

@ -11,40 +11,42 @@
# itemUntilDiscard: 施加buff到周围物品物品被弃掉后失效 # itemUntilDiscard: 施加buff到周围物品物品被弃掉后失效
# itemPermanent: 施加buff到周围物品持续整场冒险 # itemPermanent: 施加buff到周围物品持续整场冒险
id, name, description, lifecycle # type EffectLifecycle = 'instant' | 'temporary' | 'lingering' | 'permanent' | 'posture' | 'item' | 'itemTemporary' | 'itemUntilPlay' | 'itemUntilDiscard' | 'itemPermanent'
string, string, string, 'instant'|'temporary'|'lingering'|'permanent'|'posture'|'item'|'itemTemporary'|'itemUntilPlay'|'itemUntilDiscard'|'itemPermanent'
attack, 攻击, 对对手造成伤害, instant id, name, description, lifecycle, emoji
defend, 防御, 抵消下次行动前受到的伤害, temporary string, string, string, EffectLifecycle, string
spike, 尖刺, 对攻击者造成X点伤害, permanent attack, 攻击, 对对手造成伤害, instant, ⚔️
venom, 蛇毒, 同名状态牌/1费打出时移除此牌。弃掉时受到3点伤害, instant defend, 防御, 抵消下次行动前受到的伤害, temporary, 🛡️
curse, 诅咒, 受攻击时物品攻击-1直到弃掉一张该物品的牌, lingering spike, 尖刺, 对攻击者造成X点伤害, permanent, 🌵
aim, 瞄准, 造成双倍伤害,受伤时失去等量瞄准, posture venom, 蛇毒, 同名状态牌/1费打出时移除此牌。弃掉时受到3点伤害, instant, 🧪
roll, 滚动, 攻击时每消耗10点滚动造成等量伤害, posture curse, 诅咒, 受攻击时物品攻击-1直到弃掉一张该物品的牌, lingering, 💀
rollDamage, 滚动攻击, 消耗滚动层数造成的伤害, instant aim, 瞄准, 造成双倍伤害,受伤时失去等量瞄准, posture, 🎯
vultureEye, 秃鹫之眼, 抓到时获得3层暴露临时debuff受到的伤害+1/每层), instant roll, 滚动, 攻击时每消耗10点滚动造成等量伤害, posture, 🎲
tailSting, 尾刺, 攻击时伤害提升X, posture rollDamage, 滚动攻击, 消耗滚动层数造成的伤害, instant, 💥
energyDrain, 能量吸取, 受伤时玩家失去X点能量, lingering vultureEye, 秃鹫之眼, 抓到时获得3层暴露临时debuff受到的伤害+1/每层), instant, 👁️
molt, 脱皮, 若脱皮达到生命上限则怪物逃跑, posture tailSting, 尾刺, 攻击时伤害提升X, posture, 🦂
discard, 劫掠, 回合开始时随机弃掉一张手牌, lingering energyDrain, 能量吸取, 受伤时玩家失去X点能量, lingering, 🔋
storm, 风暴, 攻击时给玩家塞入1张静电, permanent molt, 脱皮, 若脱皮达到生命上限则怪物逃跑, posture, 🐚
static, 静电, 在手里时受电击伤害+1, permanent discard, 劫掠, 回合开始时随机弃掉一张手牌, lingering, 🗑️
charge, 冲锋, 受到或造成的伤害翻倍并消耗等量冲锋, lingering storm, 风暴, 攻击时给玩家塞入1张静电, permanent, ⚡
summonMummy, 召唤木乃伊, 召唤1个木乃伊, instant static, 静电, 在手里时受电击伤害+1, permanent, ⚡
summonSandwormLarva, 召唤幼沙虫, 召唤1个幼沙虫, instant charge, 冲锋, 受到或造成的伤害翻倍并消耗等量冲锋, lingering, 🐎
reviveMummy, 复活木乃伊, 复活1个已死亡的木乃伊, instant summonMummy, 召唤木乃伊, 召唤1个木乃伊, instant, 🧟
draw, 抓牌, 抓X张牌, instant summonSandwormLarva, 召唤幼沙虫, 召唤1个幼沙虫, instant, 🐛
crossbow, 十字弩连击, 对同一目标打出其他十字弩, instant reviveMummy, 复活木乃伊, 复活1个已死亡的木乃伊, instant, 🌅
defendNext, 下回合防御, 下回合开始时获得防御, temporary draw, 抓牌, 抓X张牌, instant, 🃏
damageReduce, 减伤, 本回合受到的伤害减少X, temporary crossbow, 十字弩连击, 对同一目标打出其他十字弩, instant, 🏹
removeWound, 移除伤口, 从牌堆或弃牌堆移除X张伤口, instant defendNext, 下回合防御, 下回合开始时获得防御, temporary, 🛡️
attackBuff, 攻击增益, 周围物品的攻击+X, itemUntilPlay damageReduce, 减伤, 本回合受到的伤害减少X, temporary, 📉
defendBuff, 防御增益, 周围物品的防御+X, itemUntilPlay removeWound, 移除伤口, 从牌堆或弃牌堆移除X张伤口, instant, 🩹
gainEnergy, 获得能量, 获得X点能量, instant attackBuff, 攻击增益, 周围物品的攻击+X, itemUntilPlay, ⬆️
energyNext, 下回合获能量, 下回合开始时获得X点能量, temporary defendBuff, 防御增益, 周围物品的防御+X, itemUntilPlay, ⬆️
drawNext, 下回合抓牌, 下回合开始时抓X张牌, temporary gainEnergy, 获得能量, 获得X点能量, instant, ⚡
defendBuffUntilPlay, 防御增益直到打出, 周围物品的牌防御+X直到打出, itemUntilPlay energyNext, 下回合获能量, 下回合开始时获得X点能量, temporary, ⚡
drawChoice, 选择抓牌, 从牌堆周围物品的牌中选择一张加入手牌, instant drawNext, 下回合抓牌, 下回合开始时抓X张牌, temporary, 🃏
burnForEnergy, 消耗获能量, 打出周围物品的牌时消耗并获得X能量, itemUntilPlay defendBuffUntilPlay, 防御增益直到打出, 周围物品的牌防御+X直到打出, itemUntilPlay, 🛡️
attackBuffUntilPlay, 攻击增益直到打出, 周围物品的牌攻击+X直到打出, itemUntilPlay drawChoice, 选择抓牌, 从牌堆周围物品的牌中选择一张加入手牌, instant, 🔍
transformRandom, 随机变牌, 选择一张牌随机变为周围物品的牌, instant burnForEnergy, 消耗获能量, 打出周围物品的牌时消耗并获得X能量, itemUntilPlay, 🔥
expose, 暴露, 受到的伤害+1/每层, temporary attackBuffUntilPlay, 攻击增益直到打出, 周围物品的牌攻击+X直到打出, itemUntilPlay, ⚔️
transformRandom, 随机变牌, 选择一张牌随机变为周围物品的牌, instant, 🌀
expose, 暴露, 受到的伤害+1/每层, temporary, 👁️

Can't render this file because it has a wrong number of fields in line 14.

View File

@ -1,8 +1,11 @@
export type EffectLifecycle = 'instant' | 'temporary' | 'lingering' | 'permanent' | 'posture' | 'item' | 'itemTemporary' | 'itemUntilPlay' | 'itemUntilDiscard' | 'itemPermanent';
type EffectTable = readonly { type EffectTable = readonly {
readonly id: string; readonly id: string;
readonly name: string; readonly name: string;
readonly description: string; readonly description: string;
readonly lifecycle: "instant" | "temporary" | "lingering" | "permanent" | "posture" | "item" | "itemTemporary" | "itemUntilPlay" | "itemUntilDiscard" | "itemPermanent"; readonly lifecycle: EffectLifecycle;
readonly emoji: string;
}[]; }[];
export type Effect = EffectTable[number]; export type Effect = EffectTable[number];

View File

@ -6,8 +6,11 @@
# curio (8): random pickup of treasure or resources # curio (8): random pickup of treasure or resources
# enemies: array of [enemyId; initialHp; buffs[]] # enemies: array of [enemyId; initialHp; buffs[]]
# type EncounterType = 'minion' | 'elite' | 'event' | 'shop' | 'camp' | 'curio'
# type EnemyList = [data: @enemy; hp: int; effects: [effect: @effect; stacks: int][]][]
id,type,name,description,enemies,dialogue id,type,name,description,enemies,dialogue
string,'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[data: @enemy; hp: int; effects: [effect: @effect;stacks: int][]][],string string,EncounterType,string,string,EnemyList,string
cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】对攻击者造成X点伤害。,[仙人掌怪;12;[]];[仙人掌怪;12;[]], cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】对攻击者造成X点伤害。,[仙人掌怪;12;[]];[仙人掌怪;12;[]],
snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌1费打出时移除此牌。弃掉时受到3点伤害。,[蛇;10;[]], snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌1费打出时移除此牌。弃掉时受到3点伤害。,[蛇;10;[]],
mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1直到弃掉一张该物品的牌。,[木乃伊;14;[]];[仙人掌怪;12;[]], mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1直到弃掉一张该物品的牌。,[木乃伊;14;[]];[仙人掌怪;12;[]],

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -1,12 +1,15 @@
import type { Enemy } from './enemy.csv'; import type { Enemy } from './enemy.csv';
import type { Effect } from './effect.csv'; import type { Effect } from './effect.csv';
export type EncounterType = 'minion' | 'elite' | 'event' | 'shop' | 'camp' | 'curio';
export type EnemyList = [data: Enemy, hp: int, effects: [effect: Effect, stacks: int][]][];
type EncounterTable = readonly { type EncounterTable = readonly {
readonly id: string; readonly id: string;
readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio"; readonly type: EncounterType;
readonly name: string; readonly name: string;
readonly description: string; readonly description: string;
readonly enemies: [data: Enemy, hp: number, effects: [effect: Effect, stacks: number][]][]; readonly enemies: EnemyList;
readonly dialogue: string; readonly dialogue: string;
}[]; }[];

View File

@ -1,5 +1,13 @@
import getItems, { Item } from "./item.csv"; import getItems, { Item } from "./item.csv";
export type * from "./card.csv";
export type * from "./cardEffect.csv";
export type * from "./effect.csv";
export type * from "./encounter.csv";
export type * from "./enemy.csv";
export type * from "./intent.csv";
export type * from "./item.csv";
export { default as getCards } from "./card.csv"; export { default as getCards } from "./card.csv";
export { default as getEffects } from "./effect.csv"; export { default as getEffects } from "./effect.csv";
export { default as getEncounters } from "./encounter.csv"; export { default as getEncounters } from "./encounter.csv";

View File

@ -6,46 +6,50 @@
# initBuffs: initial buffs for this intent (applied when intent becomes active) # initBuffs: initial buffs for this intent (applied when intent becomes active)
# effects: effects executed when this intent is active # effects: effects executed when this intent is active
# type IntentEffectTarget = 'user' | 'eachEnemy' | 'randomEnemy' | 'player'
# type IntentEffect = [IntentEffectTarget;@effect;number]
# type IntentEffectList = IntentEffect[]
id,enemy,initialIntent,nextIntents,brokenIntent,effects id,enemy,initialIntent,nextIntents,brokenIntent,effects
string,@enemy,boolean,@intent[],@intent[],['self'|'player'|'team';@effect;number][] string,@enemy,boolean,@intent[],@intent[],IntentEffectList
仙人掌怪-boost,仙人掌怪,true,仙人掌怪-boost;仙人掌怪-defend,,[self;spike;1];[self;defend;4] 仙人掌怪-boost,仙人掌怪,true,仙人掌怪-boost;仙人掌怪-defend,,[user;spike;1];[user;defend;4]
仙人掌怪-defend,仙人掌怪,false,仙人掌怪-attack,,[self;defend;8] 仙人掌怪-defend,仙人掌怪,false,仙人掌怪-attack,,[user;defend;8]
仙人掌怪-attack,仙人掌怪,false,仙人掌怪-boost,,[player;attack;5] 仙人掌怪-attack,仙人掌怪,false,仙人掌怪-boost,,[player;attack;5]
蛇-poison,蛇,true,蛇-attack;蛇-attack,,[player;venom;1];[player;attack;4] 蛇-poison,蛇,true,蛇-attack;蛇-attack,,[player;venom;1];[player;attack;4]
蛇-attack,蛇,false,蛇-poison;蛇-boost,,[player;attack;6] 蛇-attack,蛇,false,蛇-poison;蛇-boost,,[player;attack;6]
蛇-boost,蛇,false,蛇-poison;蛇-attack,,[self;defend;3];[player;venom;1] 蛇-boost,蛇,false,蛇-poison;蛇-attack,,[user;defend;3];[player;venom;1]
木乃伊-attack,木乃伊,true,木乃伊-defend;木乃伊-curse,,[player;attack;6] 木乃伊-attack,木乃伊,true,木乃伊-defend;木乃伊-curse,,[player;attack;6]
木乃伊-defend,木乃伊,false,木乃伊-attack,,[self;defend;6] 木乃伊-defend,木乃伊,false,木乃伊-attack,,[user;defend;6]
木乃伊-curse,木乃伊,false,木乃伊-defend;木乃伊-attack,木乃伊-attack,[player;curse;1] 木乃伊-curse,木乃伊,false,木乃伊-defend;木乃伊-attack,木乃伊-attack,[player;curse;1]
枪手-aim,枪手,true,枪手-attack,,[self;aim;2] 枪手-aim,枪手,true,枪手-attack,,[user;aim;2]
枪手-attack,枪手,false,枪手-aim;枪手-defend,枪手-aim,[player;attack;8] 枪手-attack,枪手,false,枪手-aim;枪手-defend,枪手-aim,[player;attack;8]
枪手-defend,枪手,false,枪手-aim,枪手-aim,[self;defend;5] 枪手-defend,枪手,false,枪手-aim,枪手-aim,[user;defend;5]
风卷草-boost,风卷草,true,风卷草-defend;风卷草-defend;风卷草-boost,,[self;roll;5];[self;defend;4] 风卷草-boost,风卷草,true,风卷草-defend;风卷草-defend;风卷草-boost,,[user;roll;5];[user;defend;4]
风卷草-defend,风卷草,false,风卷草-boost;风卷草-attack,,[self;defend;8] 风卷草-defend,风卷草,false,风卷草-boost;风卷草-attack,,[user;defend;8]
风卷草-attack,风卷草,false,风卷草-boost,,[player;rollDamage;0] 风卷草-attack,风卷草,false,风卷草-boost,,[player;rollDamage;0]
秃鹫-attack,秃鹫,true,秃鹫-defend;秃鹫-defend,,[player;attack;6];[player;vultureEye;1] 秃鹫-attack,秃鹫,true,秃鹫-defend;秃鹫-defend,,[player;attack;6];[player;vultureEye;1]
秃鹫-defend,秃鹫,false,秃鹫-attack;秃鹫-attack,,[self;defend;5] 秃鹫-defend,秃鹫,false,秃鹫-attack;秃鹫-attack,,[user;defend;5]
沙蝎-boost,沙蝎,true,沙蝎-attack;沙蝎-attack,,[self;tailSting;2] 沙蝎-boost,沙蝎,true,沙蝎-attack;沙蝎-attack,,[user;tailSting;2]
沙蝎-attack,沙蝎,false,沙蝎-boost;沙蝎-attack,,[player;attack;6] 沙蝎-attack,沙蝎,false,沙蝎-boost;沙蝎-attack,,[player;attack;6]
幼沙虫-defend,幼沙虫,true,幼沙虫-defend;幼沙虫-boost,,[self;defend;6] 幼沙虫-defend,幼沙虫,true,幼沙虫-defend;幼沙虫-boost,,[user;defend;6]
幼沙虫-boost,幼沙虫,false,幼沙虫-attack;幼沙虫-defend,,[self;energyDrain;1];[self;defend;4] 幼沙虫-boost,幼沙虫,false,幼沙虫-attack;幼沙虫-defend,,[user;energyDrain;1];[user;defend;4]
幼沙虫-attack,幼沙虫,false,幼沙虫-defend;幼沙虫-defend,,[player;attack;5] 幼沙虫-attack,幼沙虫,false,幼沙虫-defend;幼沙虫-defend,,[player;attack;5]
蜥蜴-attack,蜥蜴,true,蜥蜴-defend;蜥蜴-molt,,[player;attack;5] 蜥蜴-attack,蜥蜴,true,蜥蜴-defend;蜥蜴-molt,,[player;attack;5]
蜥蜴-defend,蜥蜴,false,蜥蜴-attack;蜥蜴-attack,,[self;defend;6] 蜥蜴-defend,蜥蜴,false,蜥蜴-attack;蜥蜴-attack,,[user;defend;6]
蜥蜴-molt,蜥蜴,false,蜥蜴-defend;蜥蜴-attack,,[self;molt;3] 蜥蜴-molt,蜥蜴,false,蜥蜴-defend;蜥蜴-attack,,[user;molt;3]
沙匪-attack,沙匪,true,沙匪-attack;沙匪-heavyAttack,,[player;attack;6] 沙匪-attack,沙匪,true,沙匪-attack;沙匪-heavyAttack,,[player;attack;6]
沙匪-heavyAttack,沙匪,false,沙匪-attack;沙匪-attack;沙匪-debuff,,[player;attack;10] 沙匪-heavyAttack,沙匪,false,沙匪-attack;沙匪-attack;沙匪-debuff,,[player;attack;10]
沙匪-debuff,沙匪,false,沙匪-attack;沙匪-attack,,[player;discard;1] 沙匪-debuff,沙匪,false,沙匪-attack;沙匪-attack,,[player;discard;1]
风暴之灵-storm,风暴之灵,true,风暴之灵-attack;风暴之灵-storm,,[self;storm;2];[self;defend;3] 风暴之灵-storm,风暴之灵,true,风暴之灵-attack;风暴之灵-storm,,[user;storm;2];[user;defend;3]
风暴之灵-attack,风暴之灵,false,风暴之灵-storm;风暴之灵-defend,,[player;attack;8];[player;static;1] 风暴之灵-attack,风暴之灵,false,风暴之灵-storm;风暴之灵-defend,,[player;attack;8];[player;static;1]
风暴之灵-defend,风暴之灵,false,风暴之灵-storm;风暴之灵-attack,,[self;defend;8] 风暴之灵-defend,风暴之灵,false,风暴之灵-storm;风暴之灵-attack,,[user;defend;8]
骑马枪手-charge,骑马枪手,true,骑马枪手-attack,,[self;charge;2] 骑马枪手-charge,骑马枪手,true,骑马枪手-attack,,[user;charge;2]
骑马枪手-attack,骑马枪手,false,骑马枪手-charge;骑马枪手-defend,骑马枪手-charge,[player;attack;6] 骑马枪手-attack,骑马枪手,false,骑马枪手-charge;骑马枪手-defend,骑马枪手-charge,[player;attack;6]
骑马枪手-defend,骑马枪手,false,骑马枪手-charge;骑马枪手-attack,骑马枪手-charge,[self;defend;5] 骑马枪手-defend,骑马枪手,false,骑马枪手-charge;骑马枪手-attack,骑马枪手-charge,[user;defend;5]
沙虫王-summon,沙虫王,true,沙虫王-attack;沙虫王-defend,,[self;summonSandwormLarva;18] 沙虫王-summon,沙虫王,true,沙虫王-attack;沙虫王-defend,,[user;summonSandwormLarva;18]
沙虫王-attack,沙虫王,false,沙虫王-summon;沙虫王-defend,,[player;attack;9] 沙虫王-attack,沙虫王,false,沙虫王-summon;沙虫王-defend,,[player;attack;9]
沙虫王-defend,沙虫王,false,沙虫王-attack;沙虫王-summon,,[self;defend;6] 沙虫王-defend,沙虫王,false,沙虫王-attack;沙虫王-summon,,[user;defend;6]
沙漠守卫-summon,沙漠守卫,true,沙漠守卫-attack;沙漠守卫-defend,,[self;summonMummy;14] 沙漠守卫-summon,沙漠守卫,true,沙漠守卫-attack;沙漠守卫-defend,,[user;summonMummy;14]
沙漠守卫-attack,沙漠守卫,false,沙漠守卫-defend;沙漠守卫-summon,,[player;attack;8] 沙漠守卫-attack,沙漠守卫,false,沙漠守卫-defend;沙漠守卫-summon,,[player;attack;8]
沙漠守卫-defend,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-revive,,[self;defend;8] 沙漠守卫-defend,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-revive,,[user;defend;8]
沙漠守卫-revive,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-summon,,[self;reviveMummy;1] 沙漠守卫-revive,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-summon,,[user;reviveMummy;1]

Can't render this file because it has a wrong number of fields in line 13.

View File

@ -1,13 +1,17 @@
import type { Enemy } from './enemy.csv'; import type { Enemy } from './enemy.csv';
import type { Effect } from './effect.csv'; import type { Effect } from './effect.csv';
export type IntentEffectTarget = 'user' | 'eachEnemy' | 'randomEnemy' | 'player';
export type IntentEffect = [IntentEffectTarget, Effect, number];
export type IntentEffectList = IntentEffect[];
type IntentTable = readonly { type IntentTable = readonly {
readonly id: string; readonly id: string;
readonly enemy: Enemy; readonly enemy: Enemy;
readonly initialIntent: boolean; readonly initialIntent: boolean;
readonly nextIntents: Intent[]; readonly nextIntents: Intent[];
readonly brokenIntent: Intent[]; readonly brokenIntent: Intent[];
readonly effects: ["self" | "player" | "team", Effect, number][]; readonly effects: IntentEffectList;
}[]; }[];
export type Intent = IntentTable[number]; export type Intent = IntentTable[number];

View File

@ -2,26 +2,20 @@ import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
import { addEntityEffect } from "@/samples/slay-the-spire-like/system/combat/effects"; import { addEntityEffect } from "@/samples/slay-the-spire-like/system/combat/effects";
import { moveToRegion } from "@/core/region"; import { moveToRegion } from "@/core/region";
import { CombatGameContext } from "@/samples/slay-the-spire-like/system/combat/types"; import { CombatGameContext } from "@/samples/slay-the-spire-like/system/combat/types";
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
import { GameCard } from "@/samples/slay-the-spire-like/system/deck"; import { GameCard } from "@/samples/slay-the-spire-like/system/deck";
import getEffects from "../effect.csv";
import getCards from "../card.csv"; import getCards from "../card.csv";
export function addInstantEffectTriggers(triggers: Triggers) { export function addInstantEffectTriggers(triggers: Triggers) {
const effects = getEffects();
const cards = getCards(); const cards = getCards();
function findEffect(id: string): EffectData {
const found = effects.find((e: EffectData) => e.id === id);
if (found) return found;
return { id, name: id, description: "", lifecycle: "instant" } as EffectData;
}
function findCard(id: string) { function findCard(id: string) {
return cards.find(c => c.id === id); return cards.find((c) => c.id === id);
} }
function createStatusCard(draft: CombatGameContext["value"], cardId: string): void { function createStatusCard(
draft: CombatGameContext["value"],
cardId: string,
): void {
const cardData = findCard(cardId); const cardData = findCard(cardId);
if (!cardData) return; if (!cardData) return;
@ -48,7 +42,7 @@ export function addInstantEffectTriggers(triggers: Triggers) {
function getAllEnemyDataFromState(state: CombatGameContext["value"]) { function getAllEnemyDataFromState(state: CombatGameContext["value"]) {
const seen = new Set<string>(); const seen = new Set<string>();
const result: typeof state.enemies[number]["enemy"][] = []; const result: (typeof state.enemies)[number]["enemy"][] = [];
for (const enemy of state.enemies) { for (const enemy of state.enemies) {
if (!seen.has(enemy.enemy.id)) { if (!seen.has(enemy.enemy.id)) {
seen.add(enemy.enemy.id); seen.add(enemy.enemy.id);
@ -58,16 +52,24 @@ export function addInstantEffectTriggers(triggers: Triggers) {
return result; return result;
} }
function summonEnemy(draft: CombatGameContext["value"], enemyId: string, hp: number) { function summonEnemy(
draft: CombatGameContext["value"],
enemyId: string,
hp: number,
) {
for (const enemyData of getAllEnemyDataFromState(draft)) { for (const enemyData of getAllEnemyDataFromState(draft)) {
if (enemyData.id === enemyId) { if (enemyData.id === enemyId) {
const existing = draft.enemies.find(e => e.enemy.id === enemyId && !e.isAlive); const existing = draft.enemies.find(
(e) => e.enemy.id === enemyId && !e.isAlive,
);
if (existing) { if (existing) {
existing.isAlive = true; existing.isAlive = true;
existing.hp = existing.maxHp; existing.hp = existing.maxHp;
return; return;
} }
const intent = enemyData.intents.find(i => i.initialIntent) ?? enemyData.intents[0]; const intent =
enemyData.intents.find((i) => i.initialIntent) ??
enemyData.intents[0];
const instanceId = `${enemyData.id}-${draft.enemies.length}`; const instanceId = `${enemyData.id}-${draft.enemies.length}`;
const intents: Record<string, typeof intent> = {}; const intents: Record<string, typeof intent> = {};
for (const i of enemyData.intents) { for (const i of enemyData.intents) {
@ -93,16 +95,19 @@ export function addInstantEffectTriggers(triggers: Triggers) {
await triggers.onDamage.execute(ctx.game, { await triggers.onDamage.execute(ctx.game, {
entityKey: ctx.entityKey, entityKey: ctx.entityKey,
amount: ctx.stacks, amount: ctx.stacks,
sourceEntityKey: ctx.sourceEntityKey ?? ctx.entityKey === "player" ? undefined : "player", sourceEntityKey:
(ctx.sourceEntityKey ?? ctx.entityKey === "player")
? undefined
: "player",
}); });
} else if (ctx.effect.id === "draw") { } else if (ctx.effect.id === "draw") {
await triggers.onDraw.execute(ctx.game, { count: ctx.stacks }); await triggers.onDraw.execute(ctx.game, { count: ctx.stacks });
} else if (ctx.effect.id === "gainEnergy") { } else if (ctx.effect.id === "gainEnergy") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
draft.player.energy += ctx.stacks; draft.player.energy += ctx.stacks;
}); });
} else if (ctx.effect.id === "removeWound") { } else if (ctx.effect.id === "removeWound") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
const { cards, regions } = draft.player.deck; const { cards, regions } = draft.player.deck;
let removed = 0; let removed = 0;
const allPileIds = [ const allPileIds = [
@ -113,7 +118,10 @@ export function addInstantEffectTriggers(triggers: Triggers) {
if (removed >= ctx.stacks) break; if (removed >= ctx.stacks) break;
const card = cards[cardId]; const card = cards[cardId];
if (card && card.cardData.id === "wound") { if (card && card.cardData.id === "wound") {
const sourceRegion = card.regionId === "drawPile" ? regions.drawPile : regions.discardPile; const sourceRegion =
card.regionId === "drawPile"
? regions.drawPile
: regions.discardPile;
moveToRegion(card, sourceRegion, null); moveToRegion(card, sourceRegion, null);
delete cards[cardId]; delete cards[cardId];
removed++; removed++;
@ -121,35 +129,37 @@ export function addInstantEffectTriggers(triggers: Triggers) {
} }
}); });
} else if (ctx.effect.id === "venom") { } else if (ctx.effect.id === "venom") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
createStatusCard(draft, "venom"); createStatusCard(draft, "venom");
}); });
} else if (ctx.effect.id === "vultureEye") { } else if (ctx.effect.id === "vultureEye") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
createStatusCard(draft, "vultureEye"); createStatusCard(draft, "vultureEye");
}); });
} else if (ctx.effect.id === "static") { } else if (ctx.effect.id === "static") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
createStatusCard(draft, "static"); createStatusCard(draft, "static");
}); });
} else if (ctx.effect.id === "summonMummy") { } else if (ctx.effect.id === "summonMummy") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
summonEnemy(draft, "木乃伊", ctx.stacks); summonEnemy(draft, "木乃伊", ctx.stacks);
}); });
} else if (ctx.effect.id === "summonSandwormLarva") { } else if (ctx.effect.id === "summonSandwormLarva") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
summonEnemy(draft, "幼沙虫", ctx.stacks); summonEnemy(draft, "幼沙虫", ctx.stacks);
}); });
} else if (ctx.effect.id === "reviveMummy") { } else if (ctx.effect.id === "reviveMummy") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
const deadMummy = draft.enemies.find(e => e.enemy.id === "木乃伊" && !e.isAlive); const deadMummy = draft.enemies.find(
(e) => e.enemy.id === "木乃伊" && !e.isAlive,
);
if (deadMummy) { if (deadMummy) {
deadMummy.isAlive = true; deadMummy.isAlive = true;
deadMummy.hp = deadMummy.maxHp; deadMummy.hp = deadMummy.maxHp;
} }
}); });
} else if (ctx.effect.id === "curse") { } else if (ctx.effect.id === "curse") {
await ctx.game.produceAsync(draft => { await ctx.game.produceAsync((draft) => {
addEntityEffect(draft.player, ctx.effect, ctx.stacks); addEntityEffect(draft.player, ctx.effect, ctx.stacks);
}); });
} }

View File

@ -1,22 +1,16 @@
import { LoadResult as YarnDialogues } from "yarn-spinner-loader"; import { LoadResult as YarnDialogues } from "yarn-spinner-loader";
import {
CardData,
EffectData,
EncounterData,
EnemyData,
IntentData,
ItemData,
} from "../system/types";
import { Triggers } from "../system/combat/triggers"; import { Triggers } from "../system/combat/triggers";
import type * as desert from "./desert";
export type ContentModule = { export type ContentModule = {
getCards: () => CardData[]; getCards: () => desert.Card[];
getEffects: () => EffectData[]; getEffects: () => desert.Effect[];
getEncounters: () => EncounterData[]; getEncounters: () => desert.Encounter[];
getEnemies: () => EnemyData[]; getEnemies: () => desert.Enemy[];
getIntents: () => IntentData[]; getIntents: () => desert.Intent[];
getItems: () => ItemData[]; getItems: () => desert.Item[];
getStartingItems: () => ItemData[]; getStartingItems: () => desert.Item[];
dialogues: YarnDialogues; dialogues: YarnDialogues;
addTriggers: (triggers: Triggers) => void; addTriggers: (triggers: Triggers) => void;

View File

@ -9,9 +9,8 @@ import {
import { import {
CardData, CardData,
CardEffectTarget, CardEffectTarget,
CardTargetType,
EffectData, EffectData,
EffectTarget, IntentEffectTarget,
} from "@/samples/slay-the-spire-like/system/types"; } from "@/samples/slay-the-spire-like/system/types";
export function addEffect( export function addEffect(
@ -104,25 +103,25 @@ export function* getAliveEnemies(state: CombatState) {
} }
export function* getEffectTargets( export function* getEffectTargets(
target: CardEffectTarget | EffectTarget, target: CardEffectTarget | IntentEffectTarget,
game: CombatGameContext, game: CombatGameContext,
targetId?: string, targetId?: string,
sourceEntityKey: "player" | string = "player", sourceEntityKey: "player" | string = "player",
) { ) {
if (target === "all" || target === "team") { if (target === "eachEnemy") {
for (const enemy of getAliveEnemies(game.value)) { for (const enemy of getAliveEnemies(game.value)) {
yield enemy; yield enemy;
} }
} else if (target === "self") { } else if (target === "user") {
const entity = getCombatEntity(game.value, sourceEntityKey); const entity = getCombatEntity(game.value, sourceEntityKey);
if (entity) yield entity; if (entity) yield entity;
} else if (target === "player") { } else if (target === "player") {
yield game.value.player; yield game.value.player;
} else if (target === "target") { } else if (target === "eachTarget") {
if (!targetId) return; if (!targetId) return;
const entity = getCombatEntity(game.value, targetId); const entity = getCombatEntity(game.value, targetId);
if (entity) yield entity; if (entity) yield entity;
} else if (target === "random") { } else if (target === "randomEnemy") {
const aliveEnemies = [...getAliveEnemies(game.value)]; const aliveEnemies = [...getAliveEnemies(game.value)];
if (aliveEnemies.length === 0) return; if (aliveEnemies.length === 0) return;
const index = game.rng.nextInt(aliveEnemies.length); const index = game.rng.nextInt(aliveEnemies.length);

View File

@ -38,12 +38,12 @@ export async function promptMainAction(
} }
const { targetType } = cardData; const { targetType } = cardData;
if (targetType === "single") { if (targetType === "enemy") {
if (!targetId) throw `请指定目标`; if (!targetId) throw `请指定目标`;
const target = game.value.enemies.find((e) => e.id === targetId); const target = game.value.enemies.find((e) => e.id === targetId);
if (!target) throw `目标"${targetId}"不存在`; if (!target) throw `目标"${targetId}"不存在`;
if (!target.isAlive) throw `目标"${targetId}"已死亡`; if (!target.isAlive) throw `目标"${targetId}"已死亡`;
} else if (targetType === "none") { } else if (targetType === "enemies" || targetType === "player") {
if (targetId) throw `目标"${targetId}"无效`; if (targetId) throw `目标"${targetId}"无效`;
} }

View File

@ -1,89 +1,23 @@
export type EffectData = { import {
readonly id: string; Card,
readonly name: string; Effect,
readonly description: string; Encounter,
readonly lifecycle: EffectLifecycle; EncounterType,
}; Enemy,
export type EffectLifecycle = Intent,
| "instant" Item,
| "temporary" } from "../data/desert";
| "lingering"
| "permanent"
| "posture"
| "item"
| "itemTemporary"
| "itemUntilPlay"
| "itemUntilDiscard"
| "itemPermanent";
export type EnemyData = { export type CardData = Card;
readonly id: string; export type ItemData = Item;
readonly name: string; export type EffectData = Effect;
readonly intents: readonly IntentData[]; export type IntentData = Intent;
readonly description: string; export type EnemyData = Enemy;
}; export type EncounterData<T extends EncounterType = EncounterType> =
Encounter & { type: T };
export type CardType = "item" | "status"; export {
export type CardCostType = "energy" | "uses" | "none"; CardTargetType,
export type CardTargetType = "single" | "none"; CardEffectTarget,
export type EffectTarget = "self" | "player" | "team"; IntentEffectTarget,
} from "../data/desert";
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 = {
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<T extends EncounterType = EncounterType> = {
readonly id: string;
readonly type: T;
readonly name: string;
readonly description: string;
readonly enemies: readonly [
data: EnemyData,
hp: number,
effects: [EffectData, stacks: number][],
][];
readonly dialogue: string;
};
export type IntentData = {
readonly id: string;
readonly enemy: EnemyData;
readonly initialIntent: boolean;
readonly nextIntents: readonly IntentData[];
readonly brokenIntent: readonly IntentData[];
readonly effects: readonly [EffectTarget, EffectData, number][];
};
export type ItemData = {
readonly id: string;
readonly type: string;
readonly name: string;
readonly shape: string;
readonly card: CardData;
readonly price: number;
readonly description: string;
};

View File

@ -33,6 +33,7 @@ import type {
import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types"; import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventory/types";
import type { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape"; 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"; import type { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
import { CardEffect } from "@/samples/slay-the-spire-like/data/desert";
function createRunContext( function createRunContext(
items: Map<string, InventoryItem<GameItemMeta>>, items: Map<string, InventoryItem<GameItemMeta>>,
@ -63,7 +64,7 @@ function createEffect(
id: string, id: string,
lifecycle: EffectData["lifecycle"], lifecycle: EffectData["lifecycle"],
): EffectData { ): EffectData {
return { id, name: id, description: "", lifecycle }; return { id, name: id, description: "", lifecycle, emoji: "" };
} }
function createCard( function createCard(
@ -78,8 +79,8 @@ function createCard(
type: "item" as const, type: "item" as const,
costType, costType,
costCount, costCount,
targetType: "none" as const, targetType: "player" as const,
effects: [] as const, effects: [] as CardEffect[],
}; };
} }

View File

@ -6,7 +6,6 @@ import {
} from "@/core/game"; } from "@/core/game";
import { createRegion } from "@/core/region"; import { createRegion } from "@/core/region";
import { import {
createStartWith,
createTriggers, createTriggers,
Triggers, Triggers,
} from "@/samples/slay-the-spire-like/system/combat/triggers"; } from "@/samples/slay-the-spire-like/system/combat/triggers";
@ -30,6 +29,7 @@ import { GameItemMeta } from "@/samples/slay-the-spire-like/system/grid-inventor
import { ParsedShape } from "@/samples/slay-the-spire-like/system/utils/parse-shape"; 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 { Transform2D } from "@/samples/slay-the-spire-like/system/utils/shape-collision";
import { import {
CardEffect,
getCards, getCards,
getEffects, getEffects,
getEncounters, getEncounters,
@ -48,7 +48,7 @@ function createEffect(
): EffectData { ): 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 }; return { id, name: id, description: "", lifecycle, emoji: "" };
} }
function createDeckRegions(): DeckRegions { function createDeckRegions(): DeckRegions {
@ -73,8 +73,8 @@ function createCard(
type: "item" as const, type: "item" as const,
costType, costType,
costCount, costCount,
targetType: "none" as const, targetType: "player" as const,
effects: [], effects: [] as CardEffect[],
}; };
return { return {
id, id,
@ -285,7 +285,7 @@ describe("desert triggers", () => {
}), }),
); );
const triggers = getTriggers(); const triggers = getTriggers();
const defendEffect = createEffect("defend", "posture"); const defendEffect = createEffect("defend", "temporary");
ctx._state.produce((draft) => { ctx._state.produce((draft) => {
draft.player.effects.defend = { data: defendEffect, stacks: 5 }; draft.player.effects.defend = { data: defendEffect, stacks: 5 };

View File

@ -33,7 +33,7 @@ function createTestCardData(id: string, name: string, desc: string): CardData {
type: "item", type: "item",
costType: "energy", costType: "energy",
costCount: 1, costCount: 1,
targetType: "single", targetType: "enemy",
effects: [], effects: [],
}; };
} }

View File

@ -12,7 +12,6 @@ import {
getItemAtCell, getItemAtCell,
getAdjacentItems, getAdjacentItems,
validatePlacement, validatePlacement,
type GridInventory,
type InventoryItem, type InventoryItem,
} from "@/samples/slay-the-spire-like/system/grid-inventory"; } from "@/samples/slay-the-spire-like/system/grid-inventory";
import { createItemIn } from "@/samples/slay-the-spire-like/system/grid-inventory/factory"; import { createItemIn } from "@/samples/slay-the-spire-like/system/grid-inventory/factory";
@ -36,7 +35,7 @@ function createTestCardData(id: string, name: string, desc: string): CardData {
type: "item", type: "item",
costType: "energy", costType: "energy",
costCount: 1, costCount: 1,
targetType: "single", targetType: "enemy",
effects: [], effects: [],
}; };
} }