Compare commits
No commits in common. "f7b59a1790860142086b2f7c6a9b32c080e5d337" and "8c783da857d785d113fa1f74af35f58fb7c22b03" have entirely different histories.
f7b59a1790
...
8c783da857
|
|
@ -1,2 +0,0 @@
|
|||
# 《背包爬塔》肉鸽
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import type { EffectDesert } from "../data/effectDesert.csv";
|
||||
import type { CardDesert } from "../data/cardDesert.csv";
|
||||
import { effectDesertData, cardDesertData } from "../data";
|
||||
import type { StatusCardDesert } from "../data/statusCardDesert.csv";
|
||||
import { effectDesertData, statusCardDesertData } from "../data";
|
||||
import { createStatusCard } from "../deck/factory";
|
||||
import type { PlayerDeck, GameCard } from "../deck/types";
|
||||
import type {
|
||||
|
|
@ -321,7 +321,7 @@ function resolveInstantEffect(
|
|||
}
|
||||
|
||||
function addStatusCardToDiscard(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
const cardDef = statusCardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -333,7 +333,7 @@ function addStatusCardToDiscard(state: CombatState, effectId: string, count: num
|
|||
}
|
||||
|
||||
function addStatusCardToDrawPile(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
const cardDef = statusCardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -345,7 +345,7 @@ function addStatusCardToDrawPile(state: CombatState, effectId: string, count: nu
|
|||
}
|
||||
|
||||
function addStatusCardToHand(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
const cardDef = statusCardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
@ -411,7 +411,7 @@ export function resolveCardEffects(
|
|||
|
||||
const sourceKey: "player" | string = "player";
|
||||
|
||||
const effects = card.itemData.onPlay as unknown as CombatEffectEntry[];
|
||||
const effects = card.itemData.effects as unknown as CombatEffectEntry[];
|
||||
for (const entry of effects) {
|
||||
const [target, effect, stacks] = entry;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export async function runCombat(
|
|||
): Promise<CombatResult> {
|
||||
const triggerRegistry = createCombatTriggerRegistry();
|
||||
|
||||
// TODO prefer await game.produceAsync over game.produce since produceAsync can await ui animations
|
||||
game.produce(state => {
|
||||
state.phase = "playerTurn";
|
||||
state.player.energy = state.player.maxEnergy;
|
||||
|
|
@ -94,7 +93,6 @@ async function runPlayerTurn(
|
|||
state.player.damagedThisTurn = false;
|
||||
state.player.cardsDiscardedThisTurn = 0;
|
||||
|
||||
// TODO avoid hardcoding here, prefer handling these in either the onTurnStart trigger or the onBuffUpdate trigger
|
||||
if (state.player.buffs["energyNext"]) {
|
||||
state.player.energy += state.player.buffs["energyNext"];
|
||||
}
|
||||
|
|
@ -152,7 +150,6 @@ async function runPlayerTurn(
|
|||
break;
|
||||
}
|
||||
|
||||
// TODO end is an alternative action to be taken by the player.
|
||||
const endAction = await game.prompt<{ action: "end" }>(
|
||||
prompts.endTurn,
|
||||
() => {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ import type { GridInventory } from "../grid-inventory/types";
|
|||
import type { GameItemMeta, PlayerState } from "../progress/types";
|
||||
import type { PlayerDeck } from "../deck/types";
|
||||
import type { EnemyDesert } from "../data/enemyDesert.csv";
|
||||
import type { EffectDesert } from "../data/effectDesert.csv";
|
||||
import type { EnemyIntentDesert } from "../data/enemyIntentDesert.csv";
|
||||
import type { EncounterDesert } from "../data/encounterDesert.csv";
|
||||
import type { EffectDesert } from "../data/effectDesert.csv";
|
||||
import { generateDeckFromInventory, createStatusCard } from "../deck/factory";
|
||||
import { enemyDesertData, effectDesertData } from "../data";
|
||||
import { enemyDesertData, enemyIntentDesertData, effectDesertData } from "../data";
|
||||
import type {
|
||||
BuffTable,
|
||||
CombatState,
|
||||
|
|
@ -22,50 +23,40 @@ const FATIGUE_CARDS_PER_SHUFFLE = 2;
|
|||
|
||||
export function createEnemyInstance(
|
||||
templateId: string,
|
||||
hp: number,
|
||||
initBuffs: [EffectDesert, number][],
|
||||
enemyData: EnemyDesert,
|
||||
bonusHp: number,
|
||||
idCounter: { value: number },
|
||||
): EnemyState {
|
||||
idCounter.value++;
|
||||
const id = `enemy-${idCounter.value}`;
|
||||
const maxHp = hp;
|
||||
const currentHp = hp;
|
||||
const maxHp = enemyData.initHp + bonusHp;
|
||||
const hp = maxHp;
|
||||
|
||||
const buffs: BuffTable = {};
|
||||
for (const [effect, stacks] of initBuffs) {
|
||||
for (const [effect, stacks] of enemyData.initBuffs) {
|
||||
buffs[effect.id] = (buffs[effect.id] ?? 0) + stacks;
|
||||
}
|
||||
|
||||
const intentData = buildIntentLookup(templateId);
|
||||
const initialIntent = findInitialIntent(templateId);
|
||||
|
||||
return {
|
||||
id,
|
||||
templateId,
|
||||
hp: currentHp,
|
||||
hp,
|
||||
maxHp,
|
||||
buffs,
|
||||
currentIntentId: initialIntent ?? "",
|
||||
currentIntentId: enemyData.initialIntent,
|
||||
intentData,
|
||||
isAlive: true,
|
||||
hadDefendBroken: false,
|
||||
};
|
||||
}
|
||||
|
||||
function findInitialIntent(enemyTemplateId: string): string | undefined {
|
||||
for (const row of enemyDesertData) {
|
||||
if (row.enemy === enemyTemplateId && row.initialIntent) {
|
||||
return row.intentId;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildIntentLookup(enemyTemplateId: string): Record<string, EnemyDesert> {
|
||||
const lookup: Record<string, EnemyDesert> = {};
|
||||
for (const row of enemyDesertData) {
|
||||
if (row.enemy === enemyTemplateId) {
|
||||
lookup[row.intentId] = row;
|
||||
function buildIntentLookup(enemyTemplateId: string): Record<string, EnemyIntentDesert> {
|
||||
const lookup: Record<string, EnemyIntentDesert> = {};
|
||||
for (const intent of enemyIntentDesertData) {
|
||||
if (intent.enemy.id === enemyTemplateId) {
|
||||
lookup[intent.id] = intent;
|
||||
}
|
||||
}
|
||||
return lookup;
|
||||
|
|
@ -101,26 +92,16 @@ export function createCombatState(
|
|||
const enemyOrder: string[] = [];
|
||||
const enemyTemplateData: Record<string, EnemyDesert> = {};
|
||||
|
||||
for (const [enemyId, hp, bonusHp] of encounter.enemies) {
|
||||
// Find initBuffs from enemyDesert (first row for this enemy type)
|
||||
const enemyRow = enemyDesertData.find(e => e.enemy === enemyId);
|
||||
const initBuffs: [EffectDesert, number][] = [];
|
||||
if (enemyRow) {
|
||||
for (const [effect, stacks] of enemyRow.initBuffs) {
|
||||
initBuffs.push([effect, stacks]);
|
||||
}
|
||||
}
|
||||
|
||||
const totalHp = hp + bonusHp;
|
||||
for (const [enemyRef, bonusHp] of encounter.enemies) {
|
||||
const enemyInstance = createEnemyInstance(
|
||||
enemyId,
|
||||
totalHp,
|
||||
initBuffs,
|
||||
enemyRef.id,
|
||||
enemyRef,
|
||||
bonusHp,
|
||||
idCounter,
|
||||
);
|
||||
enemies[enemyInstance.id] = enemyInstance;
|
||||
enemyOrder.push(enemyInstance.id);
|
||||
enemyTemplateData[enemyInstance.templateId] = enemyRow!;
|
||||
enemyTemplateData[enemyInstance.templateId] = enemyRef;
|
||||
}
|
||||
|
||||
shuffleDeck(player.deck.drawPile, buildSimpleRNG(0));
|
||||
|
|
@ -145,7 +126,6 @@ export function drawCardsToHand(deck: PlayerDeck, count: number): string[] {
|
|||
const drawn: string[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (deck.drawPile.length === 0) {
|
||||
// TODO think we should shuffle Fatigue into the deck here.
|
||||
reshuffleDiscardIntoDraw(deck);
|
||||
}
|
||||
if (deck.drawPile.length === 0) break;
|
||||
|
|
@ -173,7 +153,6 @@ export function addFatigueCards(deck: PlayerDeck, count: number, fatigueCounter:
|
|||
let added = 0;
|
||||
for (let i = 0; i < count; i++) {
|
||||
fatigueCounter.value++;
|
||||
// TODO avoid hard coding, expect a fatigue card to be in the CSV with a specific id.
|
||||
const card = createStatusCard(
|
||||
`fatigue-${fatigueCounter.value}`,
|
||||
"疲劳",
|
||||
|
|
@ -208,7 +187,7 @@ export function exhaustCard(deck: PlayerDeck, cardId: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
export function getEnemyCurrentIntent(enemy: EnemyState): EnemyDesert | undefined {
|
||||
export function getEnemyCurrentIntent(enemy: EnemyState): EnemyIntentDesert | undefined {
|
||||
return enemy.intentData[enemy.currentIntentId];
|
||||
}
|
||||
|
||||
|
|
@ -218,18 +197,18 @@ export function advanceEnemyIntent(enemy: EnemyState): void {
|
|||
|
||||
if (enemy.hadDefendBroken && current.brokenIntent.length > 0) {
|
||||
const idx = Math.floor(Math.random() * current.brokenIntent.length);
|
||||
enemy.currentIntentId = current.brokenIntent[idx];
|
||||
enemy.currentIntentId = current.brokenIntent[idx].id;
|
||||
enemy.hadDefendBroken = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.nextIntents.length > 0) {
|
||||
const idx = Math.floor(Math.random() * current.nextIntents.length);
|
||||
enemy.currentIntentId = current.nextIntents[idx];
|
||||
enemy.currentIntentId = current.nextIntents[idx].id;
|
||||
return;
|
||||
}
|
||||
|
||||
enemy.currentIntentId = current.intentId;
|
||||
enemy.currentIntentId = current.id;
|
||||
}
|
||||
|
||||
function shuffleDeck(drawPile: string[], rng: { nextInt: (n: number) => number }): void {
|
||||
|
|
@ -239,7 +218,6 @@ function shuffleDeck(drawPile: string[], rng: { nextInt: (n: number) => number }
|
|||
}
|
||||
}
|
||||
|
||||
// TODO why? use @/utils/rng.ts instead?
|
||||
function buildSimpleRNG(seed: number) {
|
||||
let s = seed;
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { EffectDesert } from "../data/effectDesert.csv";
|
||||
import { cardDesertData } from "../data";
|
||||
import { statusCardDesertData } from "../data";
|
||||
import { createStatusCard } from "../deck/factory";
|
||||
import type { BuffTable, CombatEffectEntry, CombatState } from "./types";
|
||||
import { applyDamage, removeBuff } from "./effects";
|
||||
|
|
@ -9,9 +9,6 @@ export type TriggerContext = {
|
|||
rng: { nextInt: (n: number) => number };
|
||||
};
|
||||
|
||||
// TODO add an onCardDrawn trigger
|
||||
// TODO refactor this to NOT implicitly correspond to an effect type, but generic event handler
|
||||
// TODO also, refactor this to be async to support prompts and produceAsync
|
||||
export type BuffTriggerBehavior = {
|
||||
onTurnStart?: (ctx: TriggerContext, entityKey: "player" | string, stacks: number) => void;
|
||||
onTurnEnd?: (ctx: TriggerContext, entityKey: "player" | string, stacks: number) => void;
|
||||
|
|
@ -24,7 +21,6 @@ export type BuffTriggerBehavior = {
|
|||
onCardDiscarded?: (ctx: TriggerContext, cardId: string, stacks: number) => void;
|
||||
};
|
||||
|
||||
// TODO refactor this to be keyed by event type
|
||||
export type CombatTriggerRegistry = Record<string, BuffTriggerBehavior>;
|
||||
|
||||
export function createCombatTriggerRegistry(): CombatTriggerRegistry {
|
||||
|
|
@ -197,7 +193,7 @@ export function createCombatTriggerRegistry(): CombatTriggerRegistry {
|
|||
}
|
||||
|
||||
function addStatusCardToHand(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
const cardDef = statusCardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
// TODO shouldn't rely on csv types. Use interfaces and expect csv types to match.
|
||||
import type { EnemyDesert } from "../data/enemyDesert.csv";
|
||||
import type { EnemyIntentDesert } from "../data/enemyIntentDesert.csv";
|
||||
import type { EffectDesert } from "../data/effectDesert.csv";
|
||||
import type { PlayerDeck, GameCard } from "../deck/types";
|
||||
import type { PlayerState } from "../progress/types";
|
||||
|
||||
export type BuffTable = Record<string, number>;
|
||||
|
||||
// TODO rename this to "lifecycle". Should use lifecycle in csv as well.
|
||||
export type EffectTiming = EffectDesert["timing"];
|
||||
|
||||
export type EffectTarget = "self" | "target" | "all" | "random" | "player" | "team";
|
||||
|
|
@ -26,7 +25,7 @@ export type EnemyState = {
|
|||
maxHp: number;
|
||||
buffs: BuffTable;
|
||||
currentIntentId: string;
|
||||
intentData: Record<string, EnemyDesert>;
|
||||
intentData: Record<string, EnemyIntentDesert>;
|
||||
isAlive: boolean;
|
||||
hadDefendBroken: boolean;
|
||||
};
|
||||
|
|
@ -59,13 +58,9 @@ export type CombatState = {
|
|||
player: PlayerCombatState;
|
||||
phase: CombatPhase;
|
||||
turnNumber: number;
|
||||
// TODO: "fled" is a per-enemy state. Should remove it here and expand the isAlive property on enemy instead.
|
||||
// TODO: CombatResult should just be "victory" "defeat" or null.
|
||||
result: CombatResult | null;
|
||||
loot: LootEntry[];
|
||||
// TODO: I think this belongs to the player combat state.
|
||||
itemBuffs: ItemBuff[];
|
||||
// TODO: Also belongs to the player combat state.
|
||||
fatigueAddedCount: number;
|
||||
enemyTemplateData: Record<string, EnemyDesert>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
# cardDesert: unified card definitions for item cards and status cards
|
||||
# type: 'item' = inventory item card, 'status' = status effect card
|
||||
# costType: 'energy' = costs energy per turn, 'uses' = limited uses, 'none' = free
|
||||
# targetType: 'single' = target one enemy, 'none' = no target
|
||||
# unplayable: true for status cards that cannot be played
|
||||
# onPlay: effects triggered when card is played
|
||||
# onDraw: effects triggered when card enters hand
|
||||
# onDiscard: effects triggered when card is discarded
|
||||
|
||||
id,name,desc,type,costType,costCount,targetType,unplayable,onPlay,onDraw,onDiscard
|
||||
string,string,string,'item'|'status','energy'|'uses'|'none',int,'single'|'none',boolean,['self'|'target'|'all'|'random'|'player';@effectDesert;number][],['self'|'target'|'all'|'random'|'player';@effectDesert;number][],['self'|'target'|'all'|'random'|'player';@effectDesert;number][]
|
||||
sword,剑,【攻击2】【攻击2】,item,energy,1,single,false,[target;attack;2];[target;attack;2],,
|
||||
greataxe,长斧,对全体【攻击5】,item,energy,2,none,false,[all;attack;5],,
|
||||
spear,长枪,【攻击2】【攻击2】【攻击2】,item,energy,1,single,false,[target;attack;2];[target;attack;2];[target;attack;2],,
|
||||
dagger,短刀,【攻击3】【攻击3】,item,energy,1,single,false,[target;attack;3];[target;attack;3],,
|
||||
dart,飞镖,【攻击1】抓一张牌,item,energy,0,single,false,[target;attack;1];[self;draw;1],,
|
||||
crossbow,十字弩,【攻击6】对同一目标打出其他十字弩,item,energy,2,single,false,[target;attack;6];[self;crossbow;0],,
|
||||
shield,盾,【防御3】,item,energy,1,none,false,[self;defend;3],,
|
||||
hat,斗笠,【防御8】,item,energy,2,none,false,[self;defend;8],,
|
||||
cape,披风,【防御2】下回合【防御2】,item,energy,1,none,false,[self;defend;2];[self;defendNext;2],,
|
||||
bracer,护腕,【防御1】抓1张牌,item,energy,0,none,false,[self;defend;1];[self;draw;1],,
|
||||
greatshield,大盾,【防御5】,item,energy,1,none,false,[self;defend;5],,
|
||||
chainmail,锁子甲,本回合受到伤害-3,item,energy,1,none,false,[self;damageReduce;3],,
|
||||
bandage,绷带,从牌堆或弃牌堆随机移除1张伤口,item,uses,3,none,false,[self;removeWound;1],,
|
||||
poisonPotion,淬毒药剂,周围物品的【攻击】+2,item,uses,3,none,false,[self;attackBuff;2],,
|
||||
fortifyPotion,强固药剂,周围物品的【防御】+2,item,uses,3,none,false,[self;defendBuff;2],,
|
||||
vitalityPotion,活力药剂,获得1点能量,item,uses,3,none,false,[self;gainEnergy;1],,
|
||||
focusPotion,集中药剂,抓2张牌,item,uses,3,none,false,[self;draw;2],,
|
||||
healingPotion,治疗药剂,从牌堆或弃牌堆移除3张伤口,item,uses,3,none,false,[self;removeWound;3],,
|
||||
waterBag,水袋,下回合开始时获得1能量抓2张牌,item,energy,1,none,false,[self;energyNext;1];[self;drawNext;2],,
|
||||
rope,绳索,周围物品的牌【防御】+2直到打出,item,energy,1,none,false,[self;defendBuffUntilPlay;2],,
|
||||
belt,腰带,从牌堆周围物品的牌当中选择一张加入手牌,item,energy,0,none,false,[self;drawChoice;1],,
|
||||
torch,火把,下次打出周围物品的牌时将其消耗并获得1能量,item,energy,1,none,false,[self;burnForEnergy;1],,
|
||||
whetstone,磨刀石,周围物品的牌【攻击】+3直到打出,item,energy,1,none,false,[self;attackBuffUntilPlay;3],,
|
||||
blacksmithHammer,铁匠锤,从牌堆/弃牌堆选择一张牌随机变为一张周围物品的牌,item,energy,1,none,false,[self;transformRandom;1],,
|
||||
wound,伤口,无效果占用手牌和牌堆,status,none,0,none,true,,,
|
||||
venom,蛇毒,弃掉时受到3点伤害,status,none,0,none,true,,,[self;attack;3]
|
||||
curse,诅咒,受攻击时物品攻击-1直到弃掉一张该物品的牌,status,none,0,none,true,,[self;curse;1],
|
||||
static,静电,在手里时受电击伤害+1,status,none,0,none,true,,[self;static;1],
|
||||
fatigue,疲劳,占用手牌,status,none,0,none,true,,,
|
||||
vultureEye,秃鹫之眼,抓到时获得3层暴露,status,none,0,none,true,,[self;expose;3],
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import type { EffectDesert } from './effectDesert.csv';
|
||||
|
||||
type CardDesertTable = readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly desc: string;
|
||||
readonly type: "item" | "status";
|
||||
readonly costType: "energy" | "uses" | "none";
|
||||
readonly costCount: number;
|
||||
readonly targetType: "single" | "none";
|
||||
readonly unplayable: boolean;
|
||||
readonly onPlay: readonly ["self" | "target" | "all" | "random" | "player", EffectDesert, number];
|
||||
readonly onDraw: readonly ["self" | "target" | "all" | "random" | "player", EffectDesert, number];
|
||||
readonly onDiscard: readonly ["self" | "target" | "all" | "random" | "player", EffectDesert, number];
|
||||
}[];
|
||||
|
||||
export type CardDesert = CardDesertTable[number];
|
||||
|
||||
declare function getData(): CardDesertTable;
|
||||
export default getData;
|
||||
|
|
@ -4,23 +4,22 @@
|
|||
# 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; bonusHp] - bonusHp for scaling
|
||||
type,name,description,enemies,dialogue
|
||||
'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[string; int; int][],string
|
||||
minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;20;0];[仙人掌怪;20;0],
|
||||
minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;14;0];[蛇;14;0],
|
||||
minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;18;0];[仙人掌怪;20;0],
|
||||
minion,枪手,概念:单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】,[枪手;16;0],
|
||||
minion,风卷草,概念:防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。,[风卷草;22;0];[风卷草;22;0],
|
||||
minion,秃鹫,概念:攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。,[秃鹫;16;0];[仙人掌怪;20;0],
|
||||
minion,沙蝎,概念:攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。,[沙蝎;14;0];[蛇;14;0],
|
||||
minion,幼沙虫,概念:防+强化。每回合第一次受伤时,玩家失去1点能量。,[幼沙虫;24;0],
|
||||
minion,蜥蜴,概念:攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。,[蜥蜴;20;0];[蜥蜴;20;0],
|
||||
minion,沙匪,概念:弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。,[沙匪;16;0];[枪手;16;0],
|
||||
elite,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1),[风暴之灵;44;0],
|
||||
elite,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。,[骑马枪手;50;0];[枪手;20;4],
|
||||
elite,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。,[沙虫王;55;0],
|
||||
elite,沙漠守卫,召唤木乃伊;会复活木乃伊2次。,[沙漠守卫;48;0];[木乃伊;20;0],
|
||||
'minion'|'elite'|'event'|'shop'|'camp'|'curio',string,string,[enemy: @enemyDesert;bonusHp: number][],string
|
||||
minion,仙人掌怪,概念:防+强化。【尖刺X】:对攻击者造成X点伤害。,[仙人掌怪;0];[仙人掌怪;0],
|
||||
minion,蛇,概念:攻+强化。给玩家塞入蛇毒牌(1费:打出时移除此牌。弃掉时受到3点伤害)。,[蛇;0];[蛇;0],
|
||||
minion,木乃伊,概念:攻+防。【诅咒】:受攻击时物品【攻击】-1,直到弃掉一张该物品的牌。,[木乃伊;0];[仙人掌怪;0],
|
||||
minion,枪手,概念:单回高攻。【瞄准X】:造成双倍伤害。受伤时失去等量【瞄准】,[枪手;0],
|
||||
minion,风卷草,概念:防+强化。【滚动X】:攻击时,每消耗10点【滚动】,造成等量伤害。,[风卷草;0];[风卷草;0],
|
||||
minion,秃鹫,概念:攻+防。若造成伤害,玩家获得秃鹫之眼(0费状态牌:打出时移除。抓到时获得3层暴露)。,[秃鹫;0];[仙人掌怪;0],
|
||||
minion,沙蝎,概念:攻+强化。【尾刺X】:姿态buff,攻击时,伤害提升X。,[沙蝎;0];[蛇;0],
|
||||
minion,幼沙虫,概念:防+强化。每回合第一次受伤时,玩家失去1点能量。,[幼沙虫;0],
|
||||
minion,蜥蜴,概念:攻+防+逃跑。【脱皮】:若脱皮达到生命上限,则怪物逃跑,玩家不能获得战斗奖励。,[蜥蜴;0];[蜥蜴;0],
|
||||
minion,沙匪,概念:弱化玩家。【劫掠】:对玩家施加的延时debuff。回合开始时,随机弃掉一张手牌。,[沙匪;0];[枪手;0],
|
||||
elite,风暴之灵,【风暴X】:攻击时,玩家获得1张静电。受伤时失去等量【风暴】。(静电:在手里时受【电击】伤害+1),[风暴之灵;0],
|
||||
elite,骑马枪手,【冲锋X】:受到或造成的伤害翻倍并消耗等量的冲锋。,[骑马枪手;0];[枪手;4],
|
||||
elite,沙虫王,召唤幼体沙虫;每当玩家弃掉一张牌,恢复1生命。,[沙虫王;0],
|
||||
elite,沙漠守卫,召唤木乃伊;会复活木乃伊2次。,[沙漠守卫;0];[木乃伊;2],
|
||||
shop,沙漠商人,商店:可以恢复生命、出售装备、附魔物品。,,
|
||||
shop,游牧商队,商队:出售稀有物品、移除牌组中一张牌。,,
|
||||
camp,绿洲篝火,篝火:可以恢复生命、补充药水使用次数、获得下次战斗Buff。,,
|
||||
|
|
|
|||
|
|
|
@ -1,8 +1,10 @@
|
|||
import type { EnemyDesert } from './enemyDesert.csv';
|
||||
|
||||
type EncounterDesertTable = readonly {
|
||||
readonly type: "minion" | "elite" | "event" | "shop" | "camp" | "curio";
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly enemies: readonly [string, number, number];
|
||||
readonly enemies: readonly [readonly enemy: EnemyDesert, readonly bonusHp: number];
|
||||
readonly dialogue: string;
|
||||
}[];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +1,16 @@
|
|||
# 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)
|
||||
# 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)
|
||||
# effects: effects executed when this intent is active
|
||||
|
||||
enemy,intentId,initialIntent,nextIntents,brokenIntent,initBuffs,effects
|
||||
string,string,boolean,string[],string[],[@effectDesert;stacks: int][],['self'|'player'|'team';@effectDesert;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,initHp,initBuffs,initialIntent
|
||||
string, int, [effect: @effectDesert; stacks: int][], string
|
||||
仙人掌怪,20,[spike;1],boost
|
||||
蛇,14,,poison
|
||||
木乃伊,18,,attack
|
||||
枪手,16,,aim
|
||||
风卷草,22,,boost
|
||||
秃鹫,16,,attack
|
||||
沙蝎,14,[tailSting;1],boost
|
||||
幼沙虫,24,[energyDrain;1],defend
|
||||
蜥蜴,20,,attack
|
||||
沙匪,16,,attack
|
||||
风暴之灵,44,,storm
|
||||
骑马枪手,50,,charge
|
||||
沙虫王,55,,summon
|
||||
沙漠守卫,48,,summon
|
||||
|
|
|
|||
|
|
|
@ -1,13 +1,10 @@
|
|||
import type { EffectDesert } from './effectDesert.csv';
|
||||
|
||||
type EnemyDesertTable = readonly {
|
||||
readonly enemy: string;
|
||||
readonly intentId: string;
|
||||
readonly initialIntent: boolean;
|
||||
readonly nextIntents: readonly string[];
|
||||
readonly brokenIntent: readonly string[];
|
||||
readonly initBuffs: readonly [EffectDesert, readonly stacks: number];
|
||||
readonly effects: readonly ["self" | "player" | "team", EffectDesert, number];
|
||||
readonly id: string;
|
||||
readonly initHp: number;
|
||||
readonly initBuffs: readonly [readonly effect: EffectDesert, readonly stacks: number];
|
||||
readonly initialIntent: string;
|
||||
}[];
|
||||
|
||||
export type EnemyDesert = EnemyDesertTable[number];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
# 敌人行动状态机,敌人行动时会执行当前的意图,然后将意图变为下一个意图
|
||||
# 若敌人防御值被打空,则将意图变为brokenIntent,除非brokenIntent为空
|
||||
|
||||
# enemy: 敌人id
|
||||
# intent: 意图id
|
||||
# nextIntents: 下一个意图id, 多个意图则从中随机
|
||||
# brokenIntent: 防御被打空后改变的意图id,多个意图则从中随机
|
||||
# effects:技能效果,目标+buff/debuff/攻击/防御+数值/层数
|
||||
enemy,id,nextIntents,brokenIntent,effects
|
||||
@enemyDesert,string,@enemyIntentDesert[],@enemyIntentDesert[],['self' | 'player' | 'team';@effectDesert;number][]
|
||||
|
||||
仙人掌怪,boost,boost;defend;defend,,[self;spike;1];[self;defend;4]
|
||||
仙人掌怪,defend,attack,,[self;defend;8]
|
||||
仙人掌怪,attack,boost,,[player;attack;5]
|
||||
蛇,poison,attack;attack,,[player;venom;1];[player;attack;4]
|
||||
蛇,attack,poison;boost,,[player;attack;6]
|
||||
蛇,boost,poison;attack,,[self;defend;3];[player;venom;1]
|
||||
木乃伊,attack,defend;curse,,[player;attack;6]
|
||||
木乃伊,defend,attack,,[self;defend;6]
|
||||
木乃伊,curse,defend;attack,attack,[player;curse;1]
|
||||
枪手,aim,attack,,[self;aim;2]
|
||||
枪手,attack,aim;defend,aim,[player;attack;8]
|
||||
枪手,defend,aim,aim,[self;defend;5]
|
||||
风卷草,boost,defend;defend;boost,,[self;roll;5];[self;defend;4]
|
||||
风卷草,defend,boost;attack,,[self;defend;8]
|
||||
风卷草,attack,boost,,[player;rollDamage;0]
|
||||
秃鹫,attack,defend;defend,,[player;attack;6];[player;vultureEye;1]
|
||||
秃鹫,defend,attack;attack,,[self;defend;5]
|
||||
沙蝎,boost,attack;attack,,[self;tailSting;2]
|
||||
沙蝎,attack,boost;attack,,[player;attack;6]
|
||||
幼沙虫,defend,defend;boost,,[self;defend;6]
|
||||
幼沙虫,boost,attack;defend,,[self;energyDrain;1];[self;defend;4]
|
||||
幼沙虫,attack,defend;defend,,[player;attack;5]
|
||||
蜥蜴,attack,defend;molt,,[player;attack;5]
|
||||
蜥蜴,defend,attack;attack,,[self;defend;6]
|
||||
蜥蜴,molt,defend;attack,,[self;molt;3]
|
||||
沙匪,attack,attack;heavyAttack,,[player;attack;6]
|
||||
沙匪,heavyAttack,attack;attack;debuff,,[player;attack;10]
|
||||
沙匪,debuff,attack;attack,,[player;discard;1]
|
||||
风暴之灵,storm,attack;storm,,[self;storm;2];[self;defend;3]
|
||||
风暴之灵,attack,storm;defend,,[player;attack;8];[player;static;1]
|
||||
风暴之灵,defend,storm;attack,,[self;defend;8]
|
||||
骑马枪手,charge,attack,,[self;charge;2]
|
||||
骑马枪手,attack,charge;defend,charge,[player;attack;6]
|
||||
骑马枪手,defend,charge;attack,charge,[self;defend;5]
|
||||
沙虫王,summon,attack;defend,,[self;summonSandwormLarva;1]
|
||||
沙虫王,attack,summon;defend,,[player;attack;9]
|
||||
沙虫王,defend,attack;summon,,[self;defend;6]
|
||||
沙漠守卫,summon,attack;defend,,[self;summonMummy;1]
|
||||
沙漠守卫,attack,defend;summon,,[player;attack;8]
|
||||
沙漠守卫,defend,attack;revive,,[self;defend;8]
|
||||
沙漠守卫,revive,attack;summon,,[self;reviveMummy;1]
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import type { EnemyDesert } from './enemyDesert.csv';
|
||||
import type { EffectDesert } from './effectDesert.csv';
|
||||
|
||||
type EnemyIntentDesertTable = readonly {
|
||||
readonly enemy: EnemyDesert;
|
||||
readonly id: string;
|
||||
readonly nextIntents: readonly EnemyIntentDesert[];
|
||||
readonly brokenIntent: readonly EnemyIntentDesert[];
|
||||
readonly effects: readonly ["self" | "player" | "team", EffectDesert, number];
|
||||
}[];
|
||||
|
||||
export type EnemyIntentDesert = EnemyIntentDesertTable[number];
|
||||
|
||||
declare function getData(): EnemyIntentDesertTable;
|
||||
export default getData;
|
||||
|
|
@ -1,26 +1,36 @@
|
|||
type,name,shape,card,price
|
||||
string,string,string,@cardDesert,int
|
||||
weapon,剑,oee,sword,50
|
||||
weapon,长斧,oees,greataxe,80
|
||||
weapon,长枪,oeee,spear,75
|
||||
weapon,短刀,oe,dagger,40
|
||||
weapon,飞镖,o,dart,30
|
||||
weapon,十字弩,onrersrw,crossbow,120
|
||||
armor,盾,oesw,shield,50
|
||||
armor,斗笠,oerwrn,hat,90
|
||||
armor,披风,oers,cape,45
|
||||
armor,护腕,o,bracer,25
|
||||
armor,大盾,oesswn,greatshield,70
|
||||
armor,锁子甲,oesw,chainmail,60
|
||||
consumable,绷带,o,bandage,20
|
||||
consumable,淬毒药剂,o,poisonPotion,30
|
||||
consumable,强固药剂,o,fortifyPotion,30
|
||||
consumable,活力药剂,o,vitalityPotion,25
|
||||
consumable,集中药剂,o,focusPotion,25
|
||||
consumable,治疗药剂,o,healingPotion,35
|
||||
tool,水袋,os,waterBag,35
|
||||
tool,绳索,ose,rope,30
|
||||
tool,腰带,owre,belt,40
|
||||
tool,火把,on,torch,25
|
||||
tool,磨刀石,o,whetstone,30
|
||||
tool,铁匠锤,oerwrs,blacksmithHammer,45
|
||||
# type can be one of: weapon, armor, consumable, tool
|
||||
#
|
||||
# shape represents a tetris-like shape, consult ../utils/parse-shape.ts
|
||||
# n
|
||||
# w o e
|
||||
# s
|
||||
# costType can be one of: energy, uses
|
||||
#
|
||||
# targetType can be one of: single, none
|
||||
|
||||
type,name,shape,costType,costCount,targetType,price,desc,effects
|
||||
string,string,string,'energy'|'uses',int,'single'|'none',int,string,['self'|'target'|'all'|'random'; @effectDesert; number][]
|
||||
weapon,剑,oee,energy,1,single,50,【攻击2】【攻击2】,[target;attack;2];[target;attack;2]
|
||||
weapon,长斧,oees,energy,2,none,80,对全体【攻击5】,[all;attack;5]
|
||||
weapon,长枪,oeee,energy,1,single,75,【攻击2】【攻击2】【攻击2】,[target;attack;2];[target;attack;2];[target;attack;2]
|
||||
weapon,短刀,oe,energy,1,single,40,【攻击3】【攻击3】,[target;attack;3];[target;attack;3]
|
||||
weapon,飞镖,o,energy,0,single,30,【攻击1】,抓一张牌,[target;attack;1];[self;draw;1]
|
||||
weapon,十字弩,onrersrw,energy,2,single,120,【攻击6】。对同一目标打出其他十字弩,[target;attack;6];[self;crossbow;0]
|
||||
armor,盾,oesw,energy,1,none,50,【防御3】,[self;defend;3]
|
||||
armor,斗笠,oerwrn,energy,2,none,90,【防御8】,[self;defend;8]
|
||||
armor,披风,oers,energy,1,none,45,【防御2】,下回合【防御2】,[self;defend;2];[self;defendNext;2]
|
||||
armor,护腕,o,energy,0,none,25,【防御1】,抓1张牌,[self;defend;1];[self;draw;1]
|
||||
armor,大盾,oesswn,energy,1,none,70,【防御5】,[self;defend;5]
|
||||
armor,锁子甲,oesw,energy,1,none,60,本回合受到伤害-3,[self;damageReduce;3]
|
||||
consumable,绷带,o,uses,3,none,20,从牌堆或弃牌堆随机移除1张伤口,[self;removeWound;1]
|
||||
consumable,淬毒药剂,o,uses,3,none,30,周围物品的【攻击】+2,[self;attackBuff;2]
|
||||
consumable,强固药剂,o,uses,3,none,30,周围物品的【防御】+2,[self;defendBuff;2]
|
||||
consumable,活力药剂,o,uses,3,none,25,获得1点能量,[self;gainEnergy;1]
|
||||
consumable,集中药剂,o,uses,3,none,25,抓2张牌,[self;draw;2]
|
||||
consumable,治疗药剂,o,uses,3,none,35,从牌堆或弃牌堆移除3张伤口,[self;removeWound;3]
|
||||
tool,水袋,os,energy,1,none,35,下回合开始时,获得1能量,抓2张牌,[self;energyNext;1];[self;drawNext;2]
|
||||
tool,绳索,ose,energy,1,none,30,周围物品的牌【防御】+2直到打出,[self;defendBuffUntilPlay;2]
|
||||
tool,腰带,owre,energy,0,none,40,从牌堆周围物品的牌当中选择一张加入手牌,[self;drawChoice;1]
|
||||
tool,火把,on,energy,1,none,25,下次打出周围物品的牌时,将其消耗并获得1能量,[self;burnForEnergy;1]
|
||||
tool,磨刀石,o,energy,1,none,30,周围物品的牌【攻击】+3直到打出,[self;attackBuffUntilPlay;3]
|
||||
tool,铁匠锤,oerwrs,energy,1,none,45,从牌堆/弃牌堆选择一张牌,随机变为一张周围物品的牌,[self;transformRandom;1]
|
||||
|
|
|
|||
|
|
|
@ -1,11 +1,15 @@
|
|||
import type { CardDesert } from './cardDesert.csv';
|
||||
import type { EffectDesert } from './effectDesert.csv';
|
||||
|
||||
type HeroItemFighter1Table = readonly {
|
||||
readonly type: string;
|
||||
readonly name: string;
|
||||
readonly shape: string;
|
||||
readonly card: CardDesert;
|
||||
readonly costType: "energy" | "uses";
|
||||
readonly costCount: number;
|
||||
readonly targetType: "single" | "none";
|
||||
readonly price: number;
|
||||
readonly desc: string;
|
||||
readonly effects: readonly ["self" | "target" | "all" | "random", EffectDesert, number];
|
||||
}[];
|
||||
|
||||
export type HeroItemFighter1 = HeroItemFighter1Table[number];
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
import heroItemFighter1Csv from './heroItemFighter1.csv';
|
||||
import encounterDesertCsv from './encounterDesert.csv';
|
||||
import enemyDesertCsv from './enemyDesert.csv';
|
||||
import enemyIntentDesertCsv from './enemyIntentDesert.csv';
|
||||
import effectDesertCsv from './effectDesert.csv';
|
||||
import cardDesertCsv from './cardDesert.csv';
|
||||
import statusCardDesertCsv from './statusCardDesert.csv';
|
||||
|
||||
export const heroItemFighter1Data = heroItemFighter1Csv();
|
||||
export const encounterDesertData = encounterDesertCsv();
|
||||
export const enemyDesertData = enemyDesertCsv();
|
||||
export const enemyIntentDesertData = enemyIntentDesertCsv();
|
||||
export const effectDesertData = effectDesertCsv();
|
||||
export const cardDesertData = cardDesertCsv();
|
||||
export const statusCardDesertData = statusCardDesertCsv();
|
||||
|
||||
export { default as encounterDesertCsv, type EncounterDesert } from './encounterDesert.csv';
|
||||
export { default as enemyDesertCsv, type EnemyDesert } from './enemyDesert.csv';
|
||||
export { default as enemyIntentDesertCsv, type EnemyIntentDesert } from './enemyIntentDesert.csv';
|
||||
export { default as effectDesertCsv, type EffectDesert } from './effectDesert.csv';
|
||||
export { default as cardDesertCsv, type CardDesert } from './cardDesert.csv';
|
||||
export { default as statusCardDesertCsv, type StatusCardDesert } from './statusCardDesert.csv';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# 状态牌:由某些效果创建,洗入玩家牌堆或手牌
|
||||
# unplayable: 是否不可打出
|
||||
# effects: 状态牌在场时施加的效果
|
||||
|
||||
id, name, desc, unplayable, effects
|
||||
string, string, string, boolean, ['self'; @effectDesert; number][]
|
||||
wound, 伤口, 无效果,占用手牌和牌堆, true,
|
||||
venom, 蛇毒, 1费:打出时移除此牌。弃掉时受到3点伤害, true,
|
||||
curse, 诅咒, 受攻击时物品攻击-1,直到弃掉一张该物品的牌, true, [self; curse; 1]
|
||||
static, 静电, 在手里时受电击伤害+1, true, [self; static; 1]
|
||||
fatigue, 疲劳, 1费/消耗,占用手牌, true,
|
||||
vultureEye, 秃鹫之眼, 0费:打出时移除。抓到时获得3层暴露, true, [self; expose; 3]
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import type { EffectDesert } from './effectDesert.csv';
|
||||
|
||||
type StatusCardDesertTable = readonly {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly desc: string;
|
||||
readonly unplayable: boolean;
|
||||
readonly effects: readonly ["self", EffectDesert, number];
|
||||
}[];
|
||||
|
||||
export type StatusCardDesert = StatusCardDesertTable[number];
|
||||
|
||||
declare function getData(): StatusCardDesertTable;
|
||||
export default getData;
|
||||
|
|
@ -2,7 +2,6 @@ import type { CellKey, GridInventory, InventoryItem } from '../grid-inventory/ty
|
|||
import type { GameItemMeta } from '../progress/types';
|
||||
import { createRegion, createRegionAxis } from '@/core/region';
|
||||
import type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './types';
|
||||
import { cardDesertData } from '../data';
|
||||
|
||||
/**
|
||||
* Generates a unique card ID for a cell within an item.
|
||||
|
|
@ -71,6 +70,11 @@ function getItemCells(item: InventoryItem<GameItemMeta>): CellKey[] {
|
|||
|
||||
/**
|
||||
* Creates a single card from an inventory item.
|
||||
*
|
||||
* @param itemId - The inventory item instance ID
|
||||
* @param itemData - The CSV item data
|
||||
* @param cellKey - The cell key ("x,y") this card represents
|
||||
* @param cellIndex - Index of the cell for unique ID generation
|
||||
*/
|
||||
function createItemCard(
|
||||
itemId: string,
|
||||
|
|
@ -79,23 +83,26 @@ function createItemCard(
|
|||
cellIndex: number
|
||||
): GameCard {
|
||||
const cardId = generateCardId(itemId, cellIndex);
|
||||
const cardData = cardDesertData.find(c => c.id === itemData.card.id);
|
||||
|
||||
return {
|
||||
id: cardId,
|
||||
regionId: '',
|
||||
position: [],
|
||||
sourceItemId: itemId,
|
||||
itemData: cardData ?? null,
|
||||
itemData,
|
||||
cellKey,
|
||||
displayName: cardData?.name ?? itemData.name,
|
||||
description: cardData?.desc ?? '',
|
||||
displayName: itemData.name,
|
||||
description: itemData.desc,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a status card that does not correspond to any inventory item.
|
||||
* Status cards represent temporary effects like wounds, stuns, etc.
|
||||
*
|
||||
* @param id - Unique identifier for the card instance
|
||||
* @param displayName - Display name (e.g. "伤口", "眩晕")
|
||||
* @param description - Card description / ability text
|
||||
*/
|
||||
function createStatusCard(
|
||||
id: string,
|
||||
|
|
@ -116,6 +123,14 @@ function createStatusCard(
|
|||
|
||||
/**
|
||||
* Generates a complete player deck from the current inventory state.
|
||||
*
|
||||
* For each item in the inventory, N cards are generated where N is the
|
||||
* number of cells the item occupies (shape.count).
|
||||
*
|
||||
* All generated cards are placed in the draw pile initially.
|
||||
*
|
||||
* @param inventory - The player's grid inventory
|
||||
* @returns A PlayerDeck with all cards in the draw pile
|
||||
*/
|
||||
function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): PlayerDeck {
|
||||
const cards: Record<string, GameCard> = {};
|
||||
|
|
@ -148,11 +163,12 @@ function generateDeckFromInventory(inventory: GridInventory<GameItemMeta>): Play
|
|||
|
||||
/**
|
||||
* Creates region definitions for deck management.
|
||||
* Returns regions for draw pile, hand, discard pile, and exhaust pile.
|
||||
*/
|
||||
function createDeckRegions(): DeckRegions {
|
||||
return {
|
||||
drawPile: createRegion('drawPile', [
|
||||
createRegionAxis('index', 0, 0),
|
||||
createRegionAxis('index', 0, 0), // unbounded
|
||||
]),
|
||||
hand: createRegion('hand', [
|
||||
createRegionAxis('index', 0, 0),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { Part } from '@/core/part';
|
||||
import type { Region } from '@/core/region';
|
||||
import type { HeroItemFighter1 } from '../data/heroItemFighter1.csv';
|
||||
import type { CardDesert } from '../data/cardDesert.csv';
|
||||
import type { CellKey } from '../grid-inventory/types';
|
||||
|
||||
/**
|
||||
|
|
@ -15,9 +14,9 @@ export interface GameCardMeta {
|
|||
*/
|
||||
sourceItemId: string | null;
|
||||
/**
|
||||
* Original card data from cardDesert.csv. `null` for status cards not in the CSV.
|
||||
* Original CSV item data. `null` for status cards.
|
||||
*/
|
||||
itemData: CardDesert | null;
|
||||
itemData: HeroItemFighter1 | null;
|
||||
/**
|
||||
* The cell key ("x,y") this card represents within the source item's shape.
|
||||
* `null` for status cards.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Data
|
||||
export { heroItemFighter1Data, encounterDesertData, enemyDesertData, effectDesertData, cardDesertData } from './data';
|
||||
export { heroItemFighter1Data, encounterDesertData } from './data';
|
||||
export { default as encounterDesertCsv } from './data/encounterDesert.csv';
|
||||
export type { EncounterDesert, CardDesert } from './data';
|
||||
export type { EncounterDesert } from './data/encounterDesert.csv';
|
||||
|
||||
// Deck
|
||||
export type { GameCard, GameCardMeta, PlayerDeck, DeckRegions } from './deck';
|
||||
|
|
|
|||
|
|
@ -220,19 +220,19 @@ describe('combat/effects', () => {
|
|||
});
|
||||
|
||||
it('should decrement lingering buffs', () => {
|
||||
const buffs: Record<string, number> = { curse: 3, energyDrain: 1 };
|
||||
const buffs: Record<string, number> = { spike: 3, aim: 1 };
|
||||
updateBuffs(buffs);
|
||||
|
||||
expect(buffs['curse']).toBe(2);
|
||||
expect(buffs['energyDrain']).toBeUndefined();
|
||||
expect(buffs['spike']).toBe(2);
|
||||
expect(buffs['aim']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not affect permanent or posture buffs', () => {
|
||||
const buffs: Record<string, number> = { defend: 5, spike: 1 };
|
||||
const buffs: Record<string, number> = { defend: 5, energyDrain: 1 };
|
||||
updateBuffs(buffs);
|
||||
|
||||
expect(buffs['defend']).toBe(5);
|
||||
expect(buffs['spike']).toBe(1);
|
||||
expect(buffs['energyDrain']).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -285,8 +285,8 @@ describe('combat/state', () => {
|
|||
it('should return timing for known effects', () => {
|
||||
expect(getEffectTiming('attack')).toBe('instant');
|
||||
expect(getEffectTiming('defend')).toBe('posture');
|
||||
expect(getEffectTiming('spike')).toBe('permanent');
|
||||
expect(getEffectTiming('energyDrain')).toBe('lingering');
|
||||
expect(getEffectTiming('spike')).toBe('lingering');
|
||||
expect(getEffectTiming('energyDrain')).toBe('permanent');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown effects', () => {
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ describe('effectDesert.csv import', () => {
|
|||
});
|
||||
|
||||
it('should have expected number of effects', () => {
|
||||
expect(effectDesertData.length).toBe(35);
|
||||
expect(effectDesertData.length).toBe(34);
|
||||
});
|
||||
|
||||
it('should have correct fields for each effect', () => {
|
||||
|
|
@ -267,7 +267,7 @@ describe('statusCardDesert.csv import', () => {
|
|||
});
|
||||
|
||||
it('should have expected number of status cards', () => {
|
||||
expect(statusCardDesertData.length).toBe(6);
|
||||
expect(statusCardDesertData.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should have correct fields for each status card', () => {
|
||||
|
|
|
|||
|
|
@ -66,5 +66,5 @@ export default defineConfig({
|
|||
sourcemap: true,
|
||||
outDir: 'dist/samples',
|
||||
external: ['@preact/signals-core', 'mutative', 'inline-schema', 'boardgame-core'],
|
||||
esbuildPlugins: [csvLoader({ writeToDisk: true }), rewriteBoardgameImports(), yarnSpinnerPlugin()],
|
||||
esbuildPlugins: [csvLoader(), rewriteBoardgameImports(), yarnSpinnerPlugin()],
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue