Compare commits
4 Commits
fb66ec55c4
...
a80852bc59
| Author | SHA1 | Date |
|---|---|---|
|
|
a80852bc59 | |
|
|
af0906561c | |
|
|
aedf82d264 | |
|
|
2f085cc0b6 |
|
|
@ -8,7 +8,7 @@ type CardTable = readonly {
|
|||
readonly costType: "energy" | "uses" | "none";
|
||||
readonly costCount: number;
|
||||
readonly targetType: "single" | "none";
|
||||
readonly effects: readonly ["onPlay" | "onDraw" | "onDiscard", "self" | "target" | "all" | "random", Effect, number];
|
||||
readonly effects: ["onPlay" | "onDraw" | "onDiscard", "self" | "target" | "all" | "random", Effect, number][];
|
||||
}[];
|
||||
|
||||
export type Card = CardTable[number];
|
||||
|
|
|
|||
|
|
@ -4,24 +4,24 @@
|
|||
# shop (2): merchant who sells different stuff
|
||||
# camp (2): consumable restock and heal
|
||||
# curio (8): random pickup of treasure or resources
|
||||
# enemies: array of [enemyId; hp; buffs[]]
|
||||
# enemies: array of [enemyId; initialHp; buffs[]]
|
||||
|
||||
id,type,name,description,enemies,dialogue
|
||||
string,'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[@enemy; int; [effect: @effect;stacks: int]][],string
|
||||
cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;20;[]];[仙人掌怪;20;[]],
|
||||
snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;14;[]];[蛇;14;[]],
|
||||
mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;18;[]];[仙人掌怪;20;[]],
|
||||
gunslinger,minion,枪手,概念:单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】,[枪手;16;[]],
|
||||
tumbleweed_pair,minion,风卷草,概念:防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。,[风卷草;22;[]];[风卷草;22;[]],
|
||||
vulture_cactus,minion,秃鹫,概念:攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。,[秃鹫;16;[]];[仙人掌怪;20;[]],
|
||||
scorpion_snake,minion,沙蝎,概念:攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。,[沙蝎;14;[]];[蛇;14;[]],
|
||||
sandworm_larva,minion,幼沙虫,概念:防+强化。每回合第一次受伤时,玩家失去1点能量。,[幼沙虫;24;[]],
|
||||
lizard_pair,minion,蜥蜴,概念:攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。,[蜥蜴;20;[]];[蜥蜴;20;[]],
|
||||
bandit_gunslinger,minion,沙匪,概念:弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。,[沙匪;16;[]];[枪手;16;[]],
|
||||
storm_spirit,elite,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1),[风暴之灵;44;[]],
|
||||
mounted_gunslinger,elite,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。,[骑马枪手;50;[]];[枪手;20;[]],
|
||||
sandworm_king,elite,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。,[沙虫王;55;[]],
|
||||
desert_guard,elite,沙漠守卫,召唤木乃伊;会复活木乃伊2次。,[沙漠守卫;48;[]];[木乃伊;20;[]],
|
||||
string,'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[data: @enemy; hp: int; effects: [effect: @effect;stacks: int][]][],string
|
||||
cactus_pair,minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;12;[]];[仙人掌怪;12;[]],
|
||||
snake_pair,minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;10;[]],
|
||||
mummy_cactus,minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;14;[]];[仙人掌怪;12;[]],
|
||||
gunslinger,minion,枪手,概念:单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】,[枪手;12;[]],
|
||||
tumbleweed_pair,minion,风卷草,概念:防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。,[风卷草;16;[]],
|
||||
vulture_cactus,minion,秃鹫,概念:攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。,[秃鹫;12;[]];[仙人掌怪;12;[]],
|
||||
scorpion_snake,minion,沙蝎,概念:攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。,[沙蝎;10;[]];[蛇;10;[]],
|
||||
sandworm_larva,minion,幼沙虫,概念:防+强化。每回合第一次受伤时,玩家失去1点能量。,[幼沙虫;18;[]],
|
||||
lizard_pair,minion,蜥蜴,概念:攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。,[蜥蜴;14;[]],
|
||||
bandit_gunslinger,minion,沙匪,概念:弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。,[沙匪;12;[]];[枪手;12;[]],
|
||||
storm_spirit,elite,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1),[风暴之灵;30;[]],
|
||||
mounted_gunslinger,elite,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。,[骑马枪手;25;[]];[枪手;12;[]],
|
||||
sandworm_king,elite,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。,[沙虫王;40;[]],
|
||||
desert_guard,elite,沙漠守卫,召唤木乃伊;会复活木乃伊2次。,[沙漠守卫;35;[]];[木乃伊;14;[]],
|
||||
desert_merchant,shop,沙漠商人,商店:可以恢复生命、出售装备、附魔物品。,,
|
||||
nomad_caravan,shop,游牧商队,商队:出售稀有物品、移除牌组中一张牌。,,
|
||||
oasis_campfire,camp,绿洲篝火,篝火:可以恢复生命、补充药水使用次数、获得下次战斗Buff。,,
|
||||
|
|
|
|||
|
|
|
@ -6,7 +6,7 @@ type EncounterTable = readonly {
|
|||
readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly enemies: readonly [Enemy, number, readonly [readonly effect: Effect, readonly stacks: number]];
|
||||
readonly enemies: [data: Enemy, hp: number, effects: [effect: Effect, stacks: number][]][];
|
||||
readonly dialogue: string;
|
||||
}[];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
id,name,description
|
||||
string,string,string
|
||||
仙人掌怪,仙人掌怪,防+强化。【尖刺X】:对攻击者造成X点伤害。
|
||||
蛇,蛇,攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。
|
||||
木乃伊,木乃伊,攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。
|
||||
枪手,枪手,单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】。
|
||||
风卷草,风卷草,防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。
|
||||
秃鹫,秃鹫,攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。
|
||||
沙蝎,沙蝎,攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。
|
||||
幼沙虫,幼沙虫,防+强化。每回合第一次受伤时,玩家失去1点能量。
|
||||
蜥蜴,蜥蜴,攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。
|
||||
沙匪,沙匪,弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。
|
||||
风暴之灵,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1)
|
||||
骑马枪手,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。
|
||||
沙虫王,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。
|
||||
沙漠守卫,沙漠守卫,召唤木乃伊;会复活木乃伊2次。
|
||||
id,name,intents,description
|
||||
string,string,@intent[],string
|
||||
仙人掌怪,仙人掌怪,[仙人掌怪-boost;仙人掌怪-defend;仙人掌怪-attack],防+强化。【尖刺X】:对攻击者造成X点伤害。
|
||||
蛇,蛇,[蛇-poison;蛇-attack;蛇-boost],攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。
|
||||
木乃伊,木乃伊,[木乃伊-attack;木乃伊-defend;木乃伊-curse],攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。
|
||||
枪手,枪手,[枪手-aim;枪手-attack;枪手-defend],单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】。
|
||||
风卷草,风卷草,[风卷草-boost;风卷草-defend;风卷草-attack],防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。
|
||||
秃鹫,秃鹫,[秃鹫-attack;秃鹫-defend],攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。
|
||||
沙蝎,沙蝎,[沙蝎-boost;沙蝎-attack],攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。
|
||||
幼沙虫,幼沙虫,[幼沙虫-defend;幼沙虫-boost;幼沙虫-attack],防+强化。每回合第一次受伤时,玩家失去1点能量。
|
||||
蜥蜴,蜥蜴,[蜥蜴-attack;蜥蜴-defend;蜥蜴-molt],攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。
|
||||
沙匪,沙匪,[沙匪-attack;沙匪-heavyAttack;沙匪-debuff],弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。
|
||||
风暴之灵,风暴之灵,[风暴之灵-storm;风暴之灵-attack;风暴之灵-defend],【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1)
|
||||
骑马枪手,骑马枪手,[骑马枪手-charge;骑马枪手-attack;骑马枪手-defend],【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。
|
||||
沙虫王,沙虫王,[沙虫王-summon;沙虫王-attack;沙虫王-defend],召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。
|
||||
沙漠守卫,沙漠守卫,[沙漠守卫-summon;沙漠守卫-attack;沙漠守卫-defend;沙漠守卫-revive],召唤木乃伊;会复活木乃伊2次。
|
||||
|
|
|
|||
|
|
|
@ -1,6 +1,9 @@
|
|||
import type { Intent } from './intent.csv';
|
||||
|
||||
type EnemyTable = readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly intents: Intent[];
|
||||
readonly description: string;
|
||||
}[];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +1,51 @@
|
|||
# enemyDesert: enemy templates and their intent state machines
|
||||
# enemy: enemy template ID (unique identifier for this enemy type)
|
||||
# intentId: intent state ID (unique within each enemy)
|
||||
# intentId: unique intent state ID (e.g. "仙人掌怪-boost")
|
||||
# enemy: enemy template this intent belongs to
|
||||
# initialIntent: true if this is the enemy's starting intent
|
||||
# nextIntents: possible next intent IDs after this intent resolves
|
||||
# brokenIntent: possible intent IDs when defend is broken
|
||||
# initBuffs: initial buffs for this enemy type (applied to all instances)
|
||||
# initBuffs: initial buffs for this intent (applied when intent becomes active)
|
||||
# effects: effects executed when this intent is active
|
||||
|
||||
enemy,intentId,initialIntent,nextIntents,brokenIntent,initBuffs,effects
|
||||
@enemy,string,boolean,string[],string[],[@effect;stacks: int][],['self'|'player'|'team';@effect;number][]
|
||||
仙人掌怪,boost,true,boost;defend;defend,,[spike;1],[self;spike;1];[self;defend;4]
|
||||
仙人掌怪,defend,false,attack,,[spike;1],[self;defend;8]
|
||||
仙人掌怪,attack,false,boost,,[spike;1],[player;attack;5]
|
||||
蛇,poison,true,attack;attack,,,,[player;venom;1];[player;attack;4]
|
||||
蛇,attack,false,poison;boost,,,,[player;attack;6]
|
||||
蛇,boost,false,poison;attack,,,,[self;defend;3];[player;venom;1]
|
||||
木乃伊,attack,true,defend;curse,,,,[player;attack;6]
|
||||
木乃伊,defend,false,attack,,,,[self;defend;6]
|
||||
木乃伊,curse,false,defend;attack,attack,,[player;curse;1]
|
||||
枪手,aim,true,attack,,,,[self;aim;2]
|
||||
枪手,attack,false,aim;defend,aim,,[player;attack;8]
|
||||
枪手,defend,false,aim,aim,,[self;defend;5]
|
||||
风卷草,boost,true,defend;defend;boost,,,,[self;roll;5];[self;defend;4]
|
||||
风卷草,defend,false,boost;attack,,,,[self;defend;8]
|
||||
风卷草,attack,false,boost,,,,[player;rollDamage;0]
|
||||
秃鹫,attack,true,defend;defend,,,,[player;attack;6];[player;vultureEye;1]
|
||||
秃鹫,defend,false,attack;attack,,,,[self;defend;5]
|
||||
沙蝎,boost,true,attack;attack,,[tailSting;1],[self;tailSting;2]
|
||||
沙蝎,attack,false,boost;attack,,[tailSting;1],[player;attack;6]
|
||||
幼沙虫,defend,true,defend;boost,,[energyDrain;1],[self;defend;6]
|
||||
幼沙虫,boost,false,attack;defend,,[energyDrain;1],[self;energyDrain;1];[self;defend;4]
|
||||
幼沙虫,attack,false,defend;defend,,[energyDrain;1],[player;attack;5]
|
||||
蜥蜴,attack,true,defend;molt,,,,[player;attack;5]
|
||||
蜥蜴,defend,false,attack;attack,,,,[self;defend;6]
|
||||
蜥蜴,molt,false,defend;attack,,,,[self;molt;3]
|
||||
沙匪,attack,true,attack;heavyAttack,,,,[player;attack;6]
|
||||
沙匪,heavyAttack,false,attack;attack;debuff,,,,[player;attack;10]
|
||||
沙匪,debuff,false,attack;attack,,,,[player;discard;1]
|
||||
风暴之灵,storm,true,attack;storm,,,,[self;storm;2];[self;defend;3]
|
||||
风暴之灵,attack,false,storm;defend,,,,[player;attack;8];[player;static;1]
|
||||
风暴之灵,defend,false,storm;attack,,,,[self;defend;8]
|
||||
骑马枪手,charge,true,attack,,,,[self;charge;2]
|
||||
骑马枪手,attack,false,charge;defend,charge,,[player;attack;6]
|
||||
骑马枪手,defend,false,charge;attack,charge,,[self;defend;5]
|
||||
沙虫王,summon,true,attack;defend,,,,[self;summonSandwormLarva;1]
|
||||
沙虫王,attack,false,summon;defend,,,,[player;attack;9]
|
||||
沙虫王,defend,false,attack;summon,,,,[self;defend;6]
|
||||
沙漠守卫,summon,true,attack;defend,,,,[self;summonMummy;1]
|
||||
沙漠守卫,attack,false,defend;summon,,,,[player;attack;8]
|
||||
沙漠守卫,defend,false,attack;revive,,,,[self;defend;8]
|
||||
沙漠守卫,revive,false,attack;summon,,,,[self;reviveMummy;1]
|
||||
id,enemy,initialIntent,nextIntents,brokenIntent,effects
|
||||
string,@enemy,boolean,@intent[],@intent[],['self'|'player'|'team';@effect;number][]
|
||||
仙人掌怪-boost,仙人掌怪,true,仙人掌怪-boost;仙人掌怪-defend,,[self;spike;1];[self;defend;4]
|
||||
仙人掌怪-defend,仙人掌怪,false,仙人掌怪-attack,,[self;defend;8]
|
||||
仙人掌怪-attack,仙人掌怪,false,仙人掌怪-boost,,[player;attack;5]
|
||||
蛇-poison,蛇,true,蛇-attack;蛇-attack,,[player;venom;1];[player;attack;4]
|
||||
蛇-attack,蛇,false,蛇-poison;蛇-boost,,[player;attack;6]
|
||||
蛇-boost,蛇,false,蛇-poison;蛇-attack,,[self;defend;3];[player;venom;1]
|
||||
木乃伊-attack,木乃伊,true,木乃伊-defend;木乃伊-curse,,[player;attack;6]
|
||||
木乃伊-defend,木乃伊,false,木乃伊-attack,,[self;defend;6]
|
||||
木乃伊-curse,木乃伊,false,木乃伊-defend;木乃伊-attack,木乃伊-attack,[player;curse;1]
|
||||
枪手-aim,枪手,true,枪手-attack,,[self;aim;2]
|
||||
枪手-attack,枪手,false,枪手-aim;枪手-defend,枪手-aim,[player;attack;8]
|
||||
枪手-defend,枪手,false,枪手-aim,枪手-aim,[self;defend;5]
|
||||
风卷草-boost,风卷草,true,风卷草-defend;风卷草-defend;风卷草-boost,,[self;roll;5];[self;defend;4]
|
||||
风卷草-defend,风卷草,false,风卷草-boost;风卷草-attack,,[self;defend;8]
|
||||
风卷草-attack,风卷草,false,风卷草-boost,,[player;rollDamage;0]
|
||||
秃鹫-attack,秃鹫,true,秃鹫-defend;秃鹫-defend,,[player;attack;6];[player;vultureEye;1]
|
||||
秃鹫-defend,秃鹫,false,秃鹫-attack;秃鹫-attack,,[self;defend;5]
|
||||
沙蝎-boost,沙蝎,true,沙蝎-attack;沙蝎-attack,,[self;tailSting;2]
|
||||
沙蝎-attack,沙蝎,false,沙蝎-boost;沙蝎-attack,,[player;attack;6]
|
||||
幼沙虫-defend,幼沙虫,true,幼沙虫-defend;幼沙虫-boost,,[self;defend;6]
|
||||
幼沙虫-boost,幼沙虫,false,幼沙虫-attack;幼沙虫-defend,,[self;energyDrain;1];[self;defend;4]
|
||||
幼沙虫-attack,幼沙虫,false,幼沙虫-defend;幼沙虫-defend,,[player;attack;5]
|
||||
蜥蜴-attack,蜥蜴,true,蜥蜴-defend;蜥蜴-molt,,[player;attack;5]
|
||||
蜥蜴-defend,蜥蜴,false,蜥蜴-attack;蜥蜴-attack,,[self;defend;6]
|
||||
蜥蜴-molt,蜥蜴,false,蜥蜴-defend;蜥蜴-attack,,[self;molt;3]
|
||||
沙匪-attack,沙匪,true,沙匪-attack;沙匪-heavyAttack,,[player;attack;6]
|
||||
沙匪-heavyAttack,沙匪,false,沙匪-attack;沙匪-attack;沙匪-debuff,,[player;attack;10]
|
||||
沙匪-debuff,沙匪,false,沙匪-attack;沙匪-attack,,[player;discard;1]
|
||||
风暴之灵-storm,风暴之灵,true,风暴之灵-attack;风暴之灵-storm,,[self;storm;2];[self;defend;3]
|
||||
风暴之灵-attack,风暴之灵,false,风暴之灵-storm;风暴之灵-defend,,[player;attack;8];[player;static;1]
|
||||
风暴之灵-defend,风暴之灵,false,风暴之灵-storm;风暴之灵-attack,,[self;defend;8]
|
||||
骑马枪手-charge,骑马枪手,true,骑马枪手-attack,,[self;charge;2]
|
||||
骑马枪手-attack,骑马枪手,false,骑马枪手-charge;骑马枪手-defend,骑马枪手-charge,[player;attack;6]
|
||||
骑马枪手-defend,骑马枪手,false,骑马枪手-charge;骑马枪手-attack,骑马枪手-charge,[self;defend;5]
|
||||
沙虫王-summon,沙虫王,true,沙虫王-attack;沙虫王-defend,,[self;summonSandwormLarva;1]
|
||||
沙虫王-attack,沙虫王,false,沙虫王-summon;沙虫王-defend,,[player;attack;9]
|
||||
沙虫王-defend,沙虫王,false,沙虫王-attack;沙虫王-summon,,[self;defend;6]
|
||||
沙漠守卫-summon,沙漠守卫,true,沙漠守卫-attack;沙漠守卫-defend,,[self;summonMummy;1]
|
||||
沙漠守卫-attack,沙漠守卫,false,沙漠守卫-defend;沙漠守卫-summon,,[player;attack;8]
|
||||
沙漠守卫-defend,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-revive,,[self;defend;8]
|
||||
沙漠守卫-revive,沙漠守卫,false,沙漠守卫-attack;沙漠守卫-summon,,[self;reviveMummy;1]
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 1 and column 42.
|
|
|
@ -2,13 +2,12 @@ import type { Enemy } from './enemy.csv';
|
|||
import type { Effect } from './effect.csv';
|
||||
|
||||
type IntentTable = readonly {
|
||||
readonly id: string;
|
||||
readonly enemy: Enemy;
|
||||
readonly intentId: string;
|
||||
readonly initialIntent: boolean;
|
||||
readonly nextIntents: readonly string[];
|
||||
readonly brokenIntent: readonly string[];
|
||||
readonly initBuffs: readonly [Effect, readonly stacks: number];
|
||||
readonly effects: readonly ["self" | "player" | "team", Effect, number];
|
||||
readonly nextIntents: Intent[];
|
||||
readonly brokenIntent: Intent[];
|
||||
readonly effects: ["self" | "player" | "team", Effect, number][];
|
||||
}[];
|
||||
|
||||
export type Intent = IntentTable[number];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import {CombatEntity, CombatState, EffectTable, PlayerEntity} from "./types";
|
||||
import {CardData, EffectData} from "@/samples/slay-the-spire-like/system/types";
|
||||
import {CombatEntity, CombatGameContext, CombatState, EffectTable, PlayerEntity} from "./types";
|
||||
import {
|
||||
CardData,
|
||||
CardEffectTarget,
|
||||
CardTargetType,
|
||||
EffectData,
|
||||
EffectTarget
|
||||
} from "@/samples/slay-the-spire-like/system/types";
|
||||
import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress/types";
|
||||
import {GridInventory} from "@/samples/slay-the-spire-like/system/grid-inventory/types";
|
||||
|
||||
|
|
@ -80,6 +86,25 @@ export function* getAliveEnemies(state: CombatState) {
|
|||
}
|
||||
}
|
||||
|
||||
export function* getEffectTargets(target: CardEffectTarget | EffectTarget, game: CombatGameContext, targetId?: string){
|
||||
if(target === 'all' || target === 'team'){
|
||||
for(const enemy of getAliveEnemies(game.value)){
|
||||
yield enemy;
|
||||
}
|
||||
} else if(target === 'self') {
|
||||
yield game.value.player;
|
||||
} else if(target === 'target'){
|
||||
if(!targetId) return;
|
||||
const entity = getCombatEntity(game.value, targetId);
|
||||
if(entity) yield entity;
|
||||
} else if(target === 'random'){
|
||||
const aliveEnemies = [...getAliveEnemies(game.value)];
|
||||
if(aliveEnemies.length === 0) return;
|
||||
const index = game.rng.nextInt(aliveEnemies.length);
|
||||
yield aliveEnemies[index];
|
||||
}
|
||||
}
|
||||
|
||||
export function getCombatEntity(state: CombatState, entityKey: string){
|
||||
return entityKey === 'player' ? state.player : state.enemies.find(e => e.id === entityKey);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
addItemEffect,
|
||||
getAliveEnemies, onEntityPostureDamage,
|
||||
onEntityEffectUpkeep,
|
||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost
|
||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost, getCombatEntity, getEffectTargets
|
||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||
import {promptMainAction} from "@/samples/slay-the-spire-like/system/combat/prompts";
|
||||
import {moveToRegion, shuffle} from "@/core/region";
|
||||
|
|
@ -34,7 +34,7 @@ function createTriggers(){
|
|||
onCombatStart: createTrigger("onCombatStart"),
|
||||
onTurnStart: createTrigger("onTurnStart", async ctx => {
|
||||
await ctx.game.produceAsync(draft => {
|
||||
const entity = ctx.entityKey === "player" ? draft.player : draft.enemies.find(e => e.id === ctx.entityKey);
|
||||
const entity = getCombatEntity(draft, ctx.entityKey);
|
||||
if(entity) onEntityEffectUpkeep(entity);
|
||||
if(entity === draft.player)
|
||||
onPlayerItemEffectUpkeep(draft.player);
|
||||
|
|
@ -65,6 +65,13 @@ function createTriggers(){
|
|||
moveToRegion(card, regions.hand, regions.discardPile);
|
||||
onItemPlay(draft.player, card.itemId);
|
||||
});
|
||||
const {cards, regions} = ctx.game.value.player.deck;
|
||||
const card = cards[ctx.cardId];
|
||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
||||
if(trigger !== 'onPlay') continue;
|
||||
for(const entity of getEffectTargets(target, ctx.game, ctx.targetId))
|
||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId});
|
||||
}
|
||||
}),
|
||||
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
|
||||
await ctx.game.produceAsync(draft => {
|
||||
|
|
@ -72,12 +79,26 @@ function createTriggers(){
|
|||
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||
onItemDiscard(draft.player, cards[ctx.cardId].itemId);
|
||||
});
|
||||
const {cards, regions} = ctx.game.value.player.deck;
|
||||
const card = cards[ctx.cardId];
|
||||
for(const [trigger, target, effect, stacks] of card.cardData.effects){
|
||||
if(trigger !== 'onDiscard') continue;
|
||||
for(const entity of getEffectTargets(target, ctx.game))
|
||||
await triggers.onEffectApplied.execute(ctx.game,{effect, entityKey: entity.id, stacks, cardId: ctx.cardId});
|
||||
}
|
||||
}),
|
||||
onCardDrawn: createTrigger("onCardDrawn", async ctx => {
|
||||
await ctx.game.produceAsync(draft => {
|
||||
const {cards, regions} = draft.player.deck;
|
||||
moveToRegion(cards[ctx.cardId], regions.drawPile, regions.hand);
|
||||
});
|
||||
const {cards, regions} = ctx.game.value.player.deck;
|
||||
const card = cards[ctx.cardId];
|
||||
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});
|
||||
}
|
||||
}),
|
||||
onDraw: createTrigger("onDraw", async ctx => {
|
||||
let toDraw = ctx.count;
|
||||
|
|
@ -138,26 +159,12 @@ function createTriggers(){
|
|||
const enemy = ctx.game.value.enemies.find(e => e.id === ctx.enemyId);
|
||||
if(!enemy || !enemy.isAlive) return;
|
||||
|
||||
const intent = enemy.intents[enemy.currentIntentId];
|
||||
const intent = enemy.currentIntent;
|
||||
if(!intent) return;
|
||||
|
||||
for(const [target, effect, stacks] of intent.effects){
|
||||
if(target === 'team'){
|
||||
for(const enemy of getAliveEnemies(ctx.game.value)){
|
||||
await triggers.onEffectApplied.execute(ctx.game, {
|
||||
effect,
|
||||
entityKey: enemy.id,
|
||||
stacks,
|
||||
});
|
||||
}
|
||||
}else {
|
||||
const entityKey = target === 'self' ? ctx.enemyId : 'player';
|
||||
await triggers.onEffectApplied.execute(ctx.game, {
|
||||
effect,
|
||||
entityKey,
|
||||
stacks,
|
||||
});
|
||||
}
|
||||
for(const entity of getEffectTargets(target, ctx.game))
|
||||
await triggers.onEffectApplied.execute(ctx.game, { effect, entityKey: entity.id, stacks, });
|
||||
}
|
||||
}),
|
||||
onIntentUpdate: createTrigger("onIntentUpdate", async ctx => {
|
||||
|
|
@ -165,13 +172,13 @@ function createTriggers(){
|
|||
const enemy = draft.enemies.find(e => e.id === ctx.enemyId);
|
||||
if(!enemy) return;
|
||||
|
||||
const intent = enemy.intents[enemy.currentIntentId];
|
||||
const intent = enemy.currentIntent;
|
||||
if(!intent) return;
|
||||
|
||||
const nextIntents = intent.nextIntents;
|
||||
if(nextIntents.length > 0){
|
||||
const nextIndex = ctx.game.rng.nextInt(nextIntents.length);
|
||||
enemy.currentIntentId = nextIntents[nextIndex];
|
||||
enemy.currentIntent = nextIntents[nextIndex];
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress";
|
|||
export type EffectTable = Record<string, {data: EffectData, stacks: number}>;
|
||||
|
||||
export type CombatEntity = {
|
||||
id: string; // player is just "player"
|
||||
effects: EffectTable;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
|
|
@ -21,10 +22,9 @@ export type PlayerEntity = CombatEntity & {
|
|||
}
|
||||
|
||||
export type EnemyEntity = CombatEntity & {
|
||||
id: string;
|
||||
enemy: EnemyData;
|
||||
intents: Record<string, IntentData>;
|
||||
currentIntentId: string;
|
||||
currentIntent: IntentData;
|
||||
};
|
||||
|
||||
export type CombatPhase = "playerTurn" | "enemyTurn" | "combatEnd";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
import type { PointCrawlMap } from '../map/types';
|
||||
import type { CombatState, EnemyEntity, PlayerEntity, EffectTable } from '../combat/types';
|
||||
import type { EncounterData, EnemyData, EffectData, IntentData } from '../types';
|
||||
import type { RunState } from './types';
|
||||
import { generateDeckFromInventory } from '../deck/factory';
|
||||
import { ReadonlyRNG } from '@/utils/rng';
|
||||
|
||||
// -- Encounter assignment to nodes --
|
||||
|
||||
/**
|
||||
* Assigns an encounter from a pool to a specific node.
|
||||
* Replaces any existing encounter on that node.
|
||||
*/
|
||||
export function assignEncounterToNode(
|
||||
map: PointCrawlMap,
|
||||
nodeId: string,
|
||||
encounter: EncounterData
|
||||
): void {
|
||||
const node = map.nodes.get(nodeId);
|
||||
if (!node) {
|
||||
throw new Error(`Node "${nodeId}" not found`);
|
||||
}
|
||||
node.encounter = encounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns encounters from a typed pool to all unassigned nodes of matching type.
|
||||
* Uses RNG for random selection; each encounter can be assigned multiple times.
|
||||
*/
|
||||
export function assignEncountersFromPool(
|
||||
map: PointCrawlMap,
|
||||
encounterPool: EncounterData[],
|
||||
rng: ReadonlyRNG
|
||||
): void {
|
||||
if (encounterPool.length === 0) return;
|
||||
|
||||
for (const node of map.nodes.values()) {
|
||||
if (node.type === 'start' || node.type === 'end') continue;
|
||||
if (node.encounter) continue;
|
||||
|
||||
const assigned = encounterPool[rng.nextInt(encounterPool.length)];
|
||||
node.encounter = assigned;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-assigns encounters for all node types from a typed index.
|
||||
* Keys in the index should match encounter type strings (e.g. 'minion', 'elite').
|
||||
*/
|
||||
export function assignAllEncounters(
|
||||
map: PointCrawlMap,
|
||||
encounterIndex: Map<string, EncounterData[]>,
|
||||
rng: ReadonlyRNG
|
||||
): void {
|
||||
for (const node of map.nodes.values()) {
|
||||
if (node.type === 'start' || node.type === 'end') continue;
|
||||
if (node.encounter) continue;
|
||||
|
||||
const encounterType = node.type;
|
||||
const pool = encounterIndex.get(encounterType);
|
||||
if (!pool || pool.length === 0) continue;
|
||||
|
||||
node.encounter = pool[rng.nextInt(pool.length)];
|
||||
}
|
||||
}
|
||||
|
||||
// -- CombatState construction --
|
||||
|
||||
/**
|
||||
* Builds a full CombatState from an encounter and the current run state.
|
||||
* - Creates EnemyEntity instances with HP, initial buffs, and intents
|
||||
* - Creates PlayerEntity with energy (3), deck from inventory, and HP from run state
|
||||
* - Sets initial phase to 'playerTurn', turn 1
|
||||
*/
|
||||
export function buildCombatState(
|
||||
encounter: EncounterData,
|
||||
runState: RunState,
|
||||
): CombatState {
|
||||
const enemies = createEnemyEntities(encounter);
|
||||
const deck = generateDeckFromInventory(runState.inventory);
|
||||
const player = createPlayerEntity(runState, deck);
|
||||
|
||||
return {
|
||||
enemies,
|
||||
player,
|
||||
inventory: runState.inventory,
|
||||
phase: 'playerTurn',
|
||||
turnNumber: 1,
|
||||
result: null,
|
||||
loot: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates EnemyEntity instances from encounter enemy definitions.
|
||||
* Each enemy gets: HP from encounter tuple, initial buffs from encounter, intents from enemy definition.
|
||||
*/
|
||||
export function createEnemyEntities(
|
||||
encounter: EncounterData,
|
||||
): EnemyEntity[] {
|
||||
const enemies: EnemyEntity[] = [];
|
||||
let instanceCounter = 0;
|
||||
|
||||
for (const [enemyData, hp, encounterBuffs] of encounter.enemies) {
|
||||
const instanceId = `${enemyData.id}-${instanceCounter++}`;
|
||||
const intents = buildIntentMap(enemyData);
|
||||
const initialIntent = findInitialIntent(enemyData);
|
||||
const effects = buildEffectTable(encounterBuffs);
|
||||
|
||||
const entity: EnemyEntity = {
|
||||
id: instanceId,
|
||||
enemy: enemyData,
|
||||
hp,
|
||||
maxHp: hp,
|
||||
isAlive: true,
|
||||
effects,
|
||||
intents,
|
||||
currentIntent: initialIntent,
|
||||
};
|
||||
enemies.push(entity);
|
||||
}
|
||||
|
||||
return enemies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a map of intent ID -> IntentData for an enemy.
|
||||
*/
|
||||
function buildIntentMap(
|
||||
enemy: EnemyData,
|
||||
): Record<string, IntentData> {
|
||||
const intents: Record<string, IntentData> = {};
|
||||
for (const intent of enemy.intents) {
|
||||
intents[intent.id] = intent;
|
||||
}
|
||||
return intents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the initial intent ID for an enemy.
|
||||
*/
|
||||
function findInitialIntent(enemy: EnemyData): IntentData {
|
||||
for (const intent of enemy.intents) {
|
||||
if (intent.initialIntent) {
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
if (enemy.intents.length === 0) {
|
||||
throw new Error(`Enemy "${enemy.id}" has no intents`);
|
||||
}
|
||||
return enemy.intents[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an EffectTable from encounter buff definitions.
|
||||
*/
|
||||
function buildEffectTable(buffs: readonly [EffectData, number][]): EffectTable {
|
||||
const table: EffectTable = {};
|
||||
for (const [effect, stacks] of buffs) {
|
||||
table[effect.id] = { data: effect, stacks };
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PlayerEntity from the run state and deck.
|
||||
*/
|
||||
function createPlayerEntity(runState: RunState, deck: ReturnType<typeof generateDeckFromInventory>): PlayerEntity {
|
||||
return {
|
||||
id: "player",
|
||||
hp: runState.player.currentHp,
|
||||
maxHp: runState.player.maxHp,
|
||||
isAlive: runState.player.currentHp > 0,
|
||||
energy: 3,
|
||||
maxEnergy: 3,
|
||||
deck,
|
||||
itemEffects: {},
|
||||
effects: {},
|
||||
};
|
||||
}
|
||||
|
||||
// -- Encounter lifecycle --
|
||||
|
||||
/**
|
||||
* Gets the encounter data for the current node.
|
||||
*/
|
||||
export function getCurrentEncounterData(runState: RunState): EncounterData | undefined {
|
||||
const node = runState.map.nodes.get(runState.currentNodeId);
|
||||
return node?.encounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current node has a combat encounter.
|
||||
*/
|
||||
export function isCombatEncounter(runState: RunState): boolean {
|
||||
const encounter = getCurrentEncounterData(runState);
|
||||
return encounter !== undefined && encounter.enemies.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the encounter at the current node.
|
||||
* Returns the constructed CombatState, or null if no combat encounter.
|
||||
*/
|
||||
export function startEncounter(runState: RunState): CombatState | null {
|
||||
const encounter = getCurrentEncounterData(runState);
|
||||
if (!encounter || encounter.enemies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buildCombatState(encounter, runState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a completed combat and applies rewards to the run state.
|
||||
* Handles: gold loot, item rewards, HP changes.
|
||||
* Marks the encounter as resolved.
|
||||
*/
|
||||
export function resolveCombatEncounter(
|
||||
runState: RunState,
|
||||
combatState: CombatState
|
||||
): { success: true } | { success: false; reason: string } {
|
||||
if (runState.currentEncounter.resolved) {
|
||||
return { success: false, reason: '该遭遇已解决' };
|
||||
}
|
||||
|
||||
// Apply HP from combat state back to run state
|
||||
runState.player.currentHp = Math.max(0, combatState.player.hp);
|
||||
|
||||
// Apply loot
|
||||
for (const loot of combatState.loot) {
|
||||
if (loot.type === 'gold') {
|
||||
runState.player.gold += loot.amount;
|
||||
}
|
||||
// Item rewards are handled by the caller via addItem()
|
||||
}
|
||||
|
||||
// Mark as resolved
|
||||
runState.currentEncounter.resolved = true;
|
||||
runState.currentEncounter.result = {
|
||||
hpLost: runState.player.maxHp - runState.player.currentHp,
|
||||
};
|
||||
runState.resolvedNodeIds.add(runState.currentNodeId);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
|
@ -18,6 +18,19 @@ export type {
|
|||
RunState,
|
||||
} from './types';
|
||||
|
||||
// Re-export encounter construction functions
|
||||
export {
|
||||
assignEncounterToNode,
|
||||
assignEncountersFromPool,
|
||||
assignAllEncounters,
|
||||
buildCombatState,
|
||||
createEnemyEntities,
|
||||
getCurrentEncounterData,
|
||||
isCombatEncounter,
|
||||
startEncounter,
|
||||
resolveCombatEncounter,
|
||||
} from './encounter';
|
||||
|
||||
// -- Constants --
|
||||
|
||||
const INVENTORY_WIDTH = 6;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type EffectLifecycle = "instant" | "temporary" | "lingering" | "permanent
|
|||
export type EnemyData = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly intents: readonly IntentData[];
|
||||
readonly description: string;
|
||||
};
|
||||
|
||||
|
|
@ -36,17 +37,16 @@ export type EncounterData = {
|
|||
readonly type: EncounterType;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly enemies: readonly [EnemyData, number, readonly [effect: EffectData, stacks: number]];
|
||||
readonly enemies: readonly [data: EnemyData, hp: number, effects: [EffectData, stacks: number][]][];
|
||||
readonly dialogue: string;
|
||||
};
|
||||
|
||||
export type IntentData = {
|
||||
readonly id: string;
|
||||
readonly enemy: EnemyData;
|
||||
readonly intentId: string;
|
||||
readonly initialIntent: boolean;
|
||||
readonly nextIntents: readonly string[];
|
||||
readonly brokenIntent: readonly string[];
|
||||
readonly initBuffs: readonly [EffectData, stacks: number];
|
||||
readonly nextIntents: readonly IntentData[];
|
||||
readonly brokenIntent: readonly IntentData[];
|
||||
readonly effects: readonly [EffectTarget, EffectData, number][];
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue