Compare commits
5 Commits
08c6a67d16
...
88d31430a6
| Author | SHA1 | Date |
|---|---|---|
|
|
88d31430a6 | |
|
|
90cb97e0ae | |
|
|
5e172c61bb | |
|
|
5019bc6324 | |
|
|
9bed2ca13e |
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/signals-core": "^1.5.1",
|
"@preact/signals-core": "^1.5.1",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"inline-schema": "file:../inline-schema",
|
"inline-schema": "file:../inline-schema",
|
||||||
"mutative": "^1.3.0",
|
"mutative": "^1.3.0",
|
||||||
"tsup": "^8.0.2",
|
"tsup": "^8.0.2",
|
||||||
|
|
@ -958,6 +959,16 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz",
|
||||||
|
|
@ -2469,6 +2480,13 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@preact/signals-core": "^1.5.1",
|
"@preact/signals-core": "^1.5.1",
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
"inline-schema": "file:../inline-schema",
|
"inline-schema": "file:../inline-schema",
|
||||||
"mutative": "^1.3.0",
|
"mutative": "^1.3.0",
|
||||||
"tsup": "^8.0.2",
|
"tsup": "^8.0.2",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
||||||
import { getCombatEntity } from "@/samples/slay-the-spire-like/system/combat/effects";
|
import { getCombatEntity } from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
import { getAdjacentItems } from "@/samples/slay-the-spire-like/system/grid-inventory";
|
|
||||||
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
|
||||||
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
import getEffects from "../effect.csv";
|
import getEffects from "../effect.csv";
|
||||||
|
import { IRunContext } from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
|
|
||||||
export function addCardEventTriggers(triggers: Triggers) {
|
export function addCardEventTriggers(triggers: Triggers, run: IRunContext) {
|
||||||
const effects = getEffects();
|
const effects = getEffects();
|
||||||
|
|
||||||
function findEffect(id: string): EffectData {
|
function findEffect(id: string): EffectData {
|
||||||
|
|
@ -67,21 +66,21 @@ export function addCardEventTriggers(triggers: Triggers) {
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const playedItemId = card.itemId;
|
const playedItemId = card.itemId;
|
||||||
|
|
||||||
const adjacent = getAdjacentItems<GameItemMeta>(
|
const adjacent = run.getNeighborItems(playedItemId);
|
||||||
ctx.game.value.inventory,
|
for (const adjItemId of adjacent) {
|
||||||
playedItemId,
|
|
||||||
);
|
|
||||||
for (const [adjItemId] of adjacent) {
|
|
||||||
const adjEffects = ctx.game.value.player.itemEffects[adjItemId];
|
const adjEffects = ctx.game.value.player.itemEffects[adjItemId];
|
||||||
if (!adjEffects) continue;
|
if (!adjEffects) continue;
|
||||||
const burn = adjEffects.burnForEnergy;
|
const burn = adjEffects.burnForEnergy;
|
||||||
if (!burn || burn.stacks <= 0) continue;
|
if (!burn || burn.stacks <= 0) continue;
|
||||||
|
|
||||||
|
const item = run.getItemData(adjItemId);
|
||||||
|
const maxUses =
|
||||||
|
item?.card.costType === "energy" ? item.card.costCount : 0;
|
||||||
|
const consumed = run.getConsumedUses(adjItemId);
|
||||||
|
const toConsume = Math.min(maxUses - consumed, burn.stacks);
|
||||||
|
|
||||||
|
await run.setConsumedUsesAsync(adjItemId, consumed + toConsume);
|
||||||
await ctx.game.produceAsync((draft) => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
const item = draft.inventory.items.get(adjItemId);
|
|
||||||
if (item) {
|
|
||||||
draft.inventory.items.delete(adjItemId);
|
|
||||||
}
|
|
||||||
draft.player.energy += burn.stacks;
|
draft.player.energy += burn.stacks;
|
||||||
delete draft.player.itemEffects[adjItemId];
|
delete draft.player.itemEffects[adjItemId];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
|
import { IRunContext } from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
import { Triggers } from "@/samples/slay-the-spire-like/system/combat/triggers";
|
||||||
import { addInstantEffectTriggers } from "./instant";
|
import { addInstantEffectTriggers } from "./instant";
|
||||||
import { addDamageTriggers } from "./damage";
|
import { addDamageTriggers } from "./damage";
|
||||||
import { addTurnStartTriggers } from "./turn-start";
|
import { addTurnStartTriggers } from "./turn-start";
|
||||||
import { addCardEventTriggers } from "./card-events";
|
import { addCardEventTriggers } from "./card-events";
|
||||||
|
|
||||||
export function addDesertTriggers(triggers: Triggers) {
|
export function addDesertTriggers(triggers: Triggers, run: IRunContext) {
|
||||||
addInstantEffectTriggers(triggers);
|
addInstantEffectTriggers(triggers);
|
||||||
addDamageTriggers(triggers);
|
addDamageTriggers(triggers);
|
||||||
addTurnStartTriggers(triggers);
|
addTurnStartTriggers(triggers);
|
||||||
addCardEventTriggers(triggers);
|
addCardEventTriggers(triggers, run);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,134 +1,168 @@
|
||||||
import {CombatEntity, CombatGameContext, CombatState, EffectTable, PlayerEntity} from "./types";
|
|
||||||
import {
|
import {
|
||||||
CardData,
|
CombatEntity,
|
||||||
CardEffectTarget,
|
CombatGameContext,
|
||||||
CardTargetType,
|
CombatState,
|
||||||
EffectData,
|
EffectTable,
|
||||||
EffectTarget
|
IRunContext,
|
||||||
|
PlayerEntity,
|
||||||
|
} from "./types";
|
||||||
|
import {
|
||||||
|
CardData,
|
||||||
|
CardEffectTarget,
|
||||||
|
CardTargetType,
|
||||||
|
EffectData,
|
||||||
|
EffectTarget,
|
||||||
} from "@/samples/slay-the-spire-like/system/types";
|
} 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";
|
|
||||||
|
|
||||||
export function addEffect(effects: EffectTable, effect: EffectData, stacks: number){
|
export function addEffect(
|
||||||
let current = effects[effect.id];
|
effects: EffectTable,
|
||||||
|
effect: EffectData,
|
||||||
if(!current) current = {data: effect, stacks};
|
stacks: number,
|
||||||
else current.stacks += stacks;
|
) {
|
||||||
|
let current = effects[effect.id];
|
||||||
if(current.stacks === 0 && effects[effect.id])
|
|
||||||
delete effects[effect.id];
|
if (!current) current = { data: effect, stacks };
|
||||||
else if(current.stacks !== 0 && !effects[effect.id])
|
else current.stacks += stacks;
|
||||||
effects[effect.id] = current;
|
|
||||||
|
if (current.stacks === 0 && effects[effect.id]) delete effects[effect.id];
|
||||||
|
else if (current.stacks !== 0 && !effects[effect.id])
|
||||||
|
effects[effect.id] = current;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addEntityEffect(entity: CombatEntity, effect: EffectData, stacks: number){
|
export function addEntityEffect(
|
||||||
addEffect(entity.effects, effect, stacks);
|
entity: CombatEntity,
|
||||||
|
effect: EffectData,
|
||||||
|
stacks: number,
|
||||||
|
) {
|
||||||
|
addEffect(entity.effects, effect, stacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addItemEffect(entity: PlayerEntity, itemKey: string, effect: EffectData, stacks: number){
|
export function addItemEffect(
|
||||||
entity.itemEffects[itemKey] = entity.itemEffects[itemKey] || {};
|
entity: PlayerEntity,
|
||||||
addEffect(entity.itemEffects[itemKey], effect, stacks);
|
itemKey: string,
|
||||||
|
effect: EffectData,
|
||||||
|
stacks: number,
|
||||||
|
) {
|
||||||
|
entity.itemEffects[itemKey] = entity.itemEffects[itemKey] || {};
|
||||||
|
addEffect(entity.itemEffects[itemKey], effect, stacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onEntityEffectUpkeep(entity: CombatEntity){
|
export function onEntityEffectUpkeep(entity: CombatEntity) {
|
||||||
for(const effect of Object.values(entity.effects)){
|
for (const effect of Object.values(entity.effects)) {
|
||||||
const lifecycle = effect.data.lifecycle;
|
const lifecycle = effect.data.lifecycle;
|
||||||
if(lifecycle === 'temporary')
|
if (lifecycle === "temporary")
|
||||||
addEntityEffect(entity, effect.data, -effect.stacks);
|
addEntityEffect(entity, effect.data, -effect.stacks);
|
||||||
else if(lifecycle === 'lingering')
|
else if (lifecycle === "lingering")
|
||||||
addEntityEffect(entity, effect.data, effect.stacks >= 0 ? -1 : 1);
|
addEntityEffect(entity, effect.data, effect.stacks >= 0 ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onEntityPostureDamage(entity: CombatEntity, damage: number) {
|
||||||
|
for (const effect of Object.values(entity.effects)) {
|
||||||
|
const lifecycle = effect.data.lifecycle;
|
||||||
|
if (lifecycle === "posture")
|
||||||
|
addEntityEffect(entity, effect.data, -Math.min(damage, effect.stacks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPlayerItemEffectUpkeep(entity: PlayerEntity) {
|
||||||
|
for (const [itemKey, itemEffects] of Object.entries(entity.itemEffects)) {
|
||||||
|
for (const effect of Object.values(itemEffects)) {
|
||||||
|
const lifecycle = effect.data.lifecycle;
|
||||||
|
if (lifecycle === "itemTemporary")
|
||||||
|
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onEntityPostureDamage(entity: CombatEntity, damage: number){
|
export function onItemPlay(entity: PlayerEntity, itemKey: string) {
|
||||||
for(const effect of Object.values(entity.effects)){
|
const effects = entity.itemEffects[itemKey];
|
||||||
const lifecycle = effect.data.lifecycle;
|
if (!effects) return;
|
||||||
if(lifecycle === 'posture')
|
for (const effect of Object.values(effects)) {
|
||||||
addEntityEffect(entity, effect.data, -Math.min(damage, effect.stacks));
|
if (effect.data.lifecycle === "itemUntilPlay") {
|
||||||
|
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPlayerItemEffectUpkeep(entity: PlayerEntity){
|
export function onItemDiscard(entity: PlayerEntity, itemKey: string) {
|
||||||
for(const [itemKey, itemEffects] of Object.entries(entity.itemEffects)){
|
const effects = entity.itemEffects[itemKey];
|
||||||
for(const effect of Object.values(itemEffects)){
|
if (!effects) return;
|
||||||
const lifecycle = effect.data.lifecycle;
|
for (const effect of Object.values(effects)) {
|
||||||
if(lifecycle === 'itemTemporary')
|
if (effect.data.lifecycle === "itemUntilDiscard") {
|
||||||
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onItemPlay(entity: PlayerEntity, itemKey: string){
|
|
||||||
const effects = entity.itemEffects[itemKey];
|
|
||||||
if(!effects)return;
|
|
||||||
for(const effect of Object.values(effects)){
|
|
||||||
if(effect.data.lifecycle === 'itemUntilPlay'){
|
|
||||||
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onItemDiscard(entity: PlayerEntity, itemKey: string){
|
|
||||||
const effects = entity.itemEffects[itemKey];
|
|
||||||
if(!effects)return;
|
|
||||||
for(const effect of Object.values(effects)){
|
|
||||||
if(effect.data.lifecycle === 'itemUntilDiscard'){
|
|
||||||
addItemEffect(entity, itemKey, effect.data, -effect.stacks);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* getAliveEnemies(state: CombatState) {
|
export function* getAliveEnemies(state: CombatState) {
|
||||||
for (let enemy of state.enemies) {
|
for (let enemy of state.enemies) {
|
||||||
if (enemy.isAlive) {
|
if (enemy.isAlive) {
|
||||||
yield enemy;
|
yield enemy;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* getEffectTargets(target: CardEffectTarget | EffectTarget, game: CombatGameContext, targetId?: string){
|
export function* getEffectTargets(
|
||||||
if(target === 'all' || target === 'team'){
|
target: CardEffectTarget | EffectTarget,
|
||||||
for(const enemy of getAliveEnemies(game.value)){
|
game: CombatGameContext,
|
||||||
yield enemy;
|
targetId?: string,
|
||||||
}
|
) {
|
||||||
} else if(target === 'self') {
|
if (target === "all" || target === "team") {
|
||||||
yield game.value.player;
|
for (const enemy of getAliveEnemies(game.value)) {
|
||||||
} else if(target === 'target'){
|
yield enemy;
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
} 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){
|
export function getCombatEntity(state: CombatState, entityKey: string) {
|
||||||
return entityKey === 'player' ? state.player : state.enemies.find(e => e.id === entityKey);
|
return entityKey === "player"
|
||||||
|
? state.player
|
||||||
|
: state.enemies.find((e) => e.id === entityKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canPlayCard(player: PlayerEntity, costType: CardData['costType'], costCount: number, itemId: string, inventory: GridInventory<GameItemMeta>): boolean {
|
export function canPlayCard(
|
||||||
if (costType === 'energy') {
|
player: PlayerEntity,
|
||||||
return player.energy >= costCount;
|
costType: CardData["costType"],
|
||||||
}
|
costCount: number,
|
||||||
if (costType === 'uses') {
|
itemId: string,
|
||||||
const item = inventory.items.get(itemId);
|
run: IRunContext,
|
||||||
if (!item || !item.meta) return false;
|
): boolean {
|
||||||
const depletion = item.meta.depletion ?? 0;
|
if (costType === "energy") {
|
||||||
return depletion < costCount;
|
return player.energy >= costCount;
|
||||||
}
|
}
|
||||||
return true;
|
if (costType === "uses") {
|
||||||
|
const item = run.getItemData(itemId);
|
||||||
|
if (!item) return false;
|
||||||
|
const maxUses = item?.card.costType === "uses" ? item.card.costCount : 0;
|
||||||
|
const consumed = run.getConsumedUses(itemId);
|
||||||
|
return consumed < maxUses;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function payCardCost(player: PlayerEntity, costType: CardData['costType'], costCount: number, itemId: string, inventory: GridInventory<GameItemMeta>): void {
|
export async function payCardCost(
|
||||||
if (costType === 'energy') {
|
player: PlayerEntity,
|
||||||
player.energy -= costCount;
|
costType: CardData["costType"],
|
||||||
} else if (costType === 'uses') {
|
costCount: number,
|
||||||
const item = inventory.items.get(itemId);
|
itemId: string,
|
||||||
if (item && item.meta) {
|
run: IRunContext,
|
||||||
item.meta.depletion = (item.meta.depletion ?? 0) + costCount;
|
): Promise<void> {
|
||||||
}
|
if (costType === "energy") {
|
||||||
}
|
player.energy -= costCount;
|
||||||
|
} else if (costType === "uses") {
|
||||||
|
const consumed = run.getConsumedUses(itemId);
|
||||||
|
await run.setConsumedUsesAsync(itemId, consumed + costCount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,56 @@
|
||||||
import { createPromptDef } from "@/core/game";
|
import { createPromptDef } from "@/core/game";
|
||||||
import {CombatGameContext} from "./types";
|
import { CombatGameContext, IRunContext } from "./types";
|
||||||
import {canPlayCard} from "@/samples/slay-the-spire-like/system/combat/effects";
|
import { canPlayCard } from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||||
|
|
||||||
export const prompts = {
|
export const prompts = {
|
||||||
mainAction: createPromptDef<[string, string?]>(
|
mainAction: createPromptDef<[string, string?]>(
|
||||||
"main-action <cardId:string> [targetId:string]",
|
"main-action <cardId:string> [targetId:string]",
|
||||||
"选择卡牌并指定目标"
|
"选择卡牌并指定目标",
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function promptMainAction(game: CombatGameContext){
|
export async function promptMainAction(
|
||||||
return await game.prompt(prompts.mainAction, (cardId, targetId) => {
|
game: CombatGameContext,
|
||||||
if(cardId === 'end-turn') return {
|
run: IRunContext,
|
||||||
action: 'end-turn' as 'end-turn'
|
) {
|
||||||
};
|
return await game.prompt(prompts.mainAction, (cardId, targetId) => {
|
||||||
|
if (cardId === "end-turn")
|
||||||
const exists = game.value.player.deck.regions.hand.childIds.includes(cardId);
|
return {
|
||||||
if(!exists) throw `卡牌"${cardId}"不在手牌中`;
|
action: "end-turn" as "end-turn",
|
||||||
|
};
|
||||||
const card = game.value.player.deck.cards[cardId];
|
|
||||||
const {cardData, itemId} = card;
|
const exists =
|
||||||
if(!canPlayCard(game.value.player, cardData.costType, cardData.costCount, itemId, game.value.inventory)){
|
game.value.player.deck.regions.hand.childIds.includes(cardId);
|
||||||
throw `无法支付卡牌"${cardId}"的费用`;
|
if (!exists) throw `卡牌"${cardId}"不在手牌中`;
|
||||||
}
|
|
||||||
|
const card = game.value.player.deck.cards[cardId];
|
||||||
const {targetType} = cardData;
|
const { cardData, itemId } = card;
|
||||||
if(targetType === 'single'){
|
if (
|
||||||
if(!targetId) throw `请指定目标`;
|
!canPlayCard(
|
||||||
const target = game.value.enemies.find(e => e.id === targetId);
|
game.value.player,
|
||||||
if(!target) throw `目标"${targetId}"不存在`;
|
cardData.costType,
|
||||||
if(!target.isAlive) throw `目标"${targetId}"已死亡`;
|
cardData.costCount,
|
||||||
}else if(targetType === 'none'){
|
itemId,
|
||||||
if(targetId) throw `目标"${targetId}"无效`;
|
run,
|
||||||
}
|
)
|
||||||
|
) {
|
||||||
return {
|
throw `无法支付卡牌"${cardId}"的费用`;
|
||||||
action: 'play' as 'play',
|
}
|
||||||
cardId,
|
|
||||||
targetId
|
const { targetType } = cardData;
|
||||||
};
|
if (targetType === "single") {
|
||||||
});
|
if (!targetId) throw `请指定目标`;
|
||||||
}
|
const target = game.value.enemies.find((e) => e.id === targetId);
|
||||||
|
if (!target) throw `目标"${targetId}"不存在`;
|
||||||
|
if (!target.isAlive) throw `目标"${targetId}"已死亡`;
|
||||||
|
} else if (targetType === "none") {
|
||||||
|
if (targetId) throw `目标"${targetId}"无效`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: "play" as "play",
|
||||||
|
cardId,
|
||||||
|
targetId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { CombatGameContext } from "./types";
|
import { CombatGameContext, IRunContext } from "./types";
|
||||||
import {
|
import {
|
||||||
addEntityEffect,
|
addEntityEffect,
|
||||||
addItemEffect,
|
addItemEffect,
|
||||||
|
|
@ -51,7 +51,7 @@ type TriggerTypes = {
|
||||||
onIntentUpdate: { enemyId: string };
|
onIntentUpdate: { enemyId: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createTriggers() {
|
export function createTriggers(run: IRunContext) {
|
||||||
const triggers = {
|
const triggers = {
|
||||||
onCombatStart: createTrigger("onCombatStart"),
|
onCombatStart: createTrigger("onCombatStart"),
|
||||||
onTurnStart: createTrigger("onTurnStart", async (ctx) => {
|
onTurnStart: createTrigger("onTurnStart", async (ctx) => {
|
||||||
|
|
@ -89,7 +89,7 @@ export function createTriggers() {
|
||||||
card.cardData.costType,
|
card.cardData.costType,
|
||||||
card.cardData.costCount,
|
card.cardData.costCount,
|
||||||
card.itemId,
|
card.itemId,
|
||||||
draft.inventory,
|
run,
|
||||||
);
|
);
|
||||||
moveToRegion(card, regions.hand, regions.discardPile);
|
moveToRegion(card, regions.hand, regions.discardPile);
|
||||||
onItemPlay(draft.player, card.itemId);
|
onItemPlay(draft.player, card.itemId);
|
||||||
|
|
@ -176,11 +176,8 @@ export function createTriggers() {
|
||||||
if (ctx.effect.lifecycle.startsWith("item")) {
|
if (ctx.effect.lifecycle.startsWith("item")) {
|
||||||
if (ctx.cardId) {
|
if (ctx.cardId) {
|
||||||
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
const card = ctx.game.value.player.deck.cards[ctx.cardId];
|
||||||
const nearby = getAdjacentItems<GameItemMeta>(
|
const nearby = run.getNeighborItems(card.itemId);
|
||||||
ctx.game.value.inventory,
|
for (const itemId of nearby) {
|
||||||
card.itemId,
|
|
||||||
);
|
|
||||||
for (const itemId of nearby.keys()) {
|
|
||||||
await ctx.game.produceAsync((draft) => {
|
await ctx.game.produceAsync((draft) => {
|
||||||
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
addItemEffect(draft.player, itemId, ctx.effect, ctx.stacks);
|
||||||
});
|
});
|
||||||
|
|
@ -269,9 +266,12 @@ export function createTriggers() {
|
||||||
return triggers;
|
return triggers;
|
||||||
}
|
}
|
||||||
export type Triggers = ReturnType<typeof createTriggers>;
|
export type Triggers = ReturnType<typeof createTriggers>;
|
||||||
export function createStartWith(build: (triggers: Triggers) => void) {
|
export function createStartWith(
|
||||||
const triggers = createTriggers();
|
build: (triggers: Triggers, run: IRunContext) => void,
|
||||||
build(triggers);
|
run: IRunContext,
|
||||||
|
) {
|
||||||
|
const triggers = createTriggers(run);
|
||||||
|
build(triggers, run);
|
||||||
return async function (game: CombatGameContext) {
|
return async function (game: CombatGameContext) {
|
||||||
await triggers.onCombatStart.execute(game, {});
|
await triggers.onCombatStart.execute(game, {});
|
||||||
|
|
||||||
|
|
@ -279,7 +279,7 @@ export function createStartWith(build: (triggers: Triggers) => void) {
|
||||||
while (true) {
|
while (true) {
|
||||||
await triggers.onTurnStart.execute(game, { entityKey: "player" });
|
await triggers.onTurnStart.execute(game, { entityKey: "player" });
|
||||||
while (true) {
|
while (true) {
|
||||||
const action = await promptMainAction(game);
|
const action = await promptMainAction(game, run);
|
||||||
if (action.action === "end-turn") break;
|
if (action.action === "end-turn") break;
|
||||||
if (action.action === "play") {
|
if (action.action === "play") {
|
||||||
await triggers.onCardPlayed.execute(game, action);
|
await triggers.onCardPlayed.execute(game, action);
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import type { PlayerDeck } from "../deck/types";
|
||||||
import {
|
import {
|
||||||
EnemyData,
|
EnemyData,
|
||||||
IntentData,
|
IntentData,
|
||||||
|
ItemData,
|
||||||
} from "@/samples/slay-the-spire-like/system/types";
|
} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
import { GridInventory } from "@/samples/slay-the-spire-like/system/grid-inventory";
|
|
||||||
import { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress";
|
|
||||||
|
|
||||||
export type EffectTable = Record<string, { data: EffectData; stacks: number }>;
|
export type EffectTable = Record<string, { data: EffectData; stacks: number }>;
|
||||||
|
|
||||||
|
|
@ -46,7 +45,6 @@ export type LootEntry =
|
||||||
export type CombatState = {
|
export type CombatState = {
|
||||||
enemies: EnemyEntity[];
|
enemies: EnemyEntity[];
|
||||||
player: PlayerEntity;
|
player: PlayerEntity;
|
||||||
inventory: GridInventory<GameItemMeta>;
|
|
||||||
|
|
||||||
phase: CombatPhase;
|
phase: CombatPhase;
|
||||||
turnNumber: number;
|
turnNumber: number;
|
||||||
|
|
@ -55,5 +53,13 @@ export type CombatState = {
|
||||||
loot: LootEntry[];
|
loot: LootEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IRunContext {
|
||||||
|
getItemData(id: string): ItemData | null;
|
||||||
|
getNeighborItems(id: string): Iterable<string>;
|
||||||
|
|
||||||
|
getConsumedUses(id: string): number;
|
||||||
|
setConsumedUsesAsync(id: string, uses: number): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export type CombatGameContext =
|
export type CombatGameContext =
|
||||||
import("@/core/game").IGameContextExport<CombatState>;
|
import("@/core/game").IGameContextExport<CombatState>;
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,6 @@ export function buildCombatState(runState: RunState): CombatState {
|
||||||
return {
|
return {
|
||||||
enemies,
|
enemies,
|
||||||
player,
|
player,
|
||||||
inventory: runState.inventory,
|
|
||||||
phase: "playerTurn",
|
phase: "playerTurn",
|
||||||
turnNumber: 1,
|
turnNumber: 1,
|
||||||
result: null,
|
result: null,
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,32 @@
|
||||||
import type { PointCrawlMap } from '../map/types';
|
import type { PointCrawlMap } from "../map/types";
|
||||||
import type { GridInventory, InventoryItem } from '../grid-inventory/types';
|
import type { GridInventory, InventoryItem } from "../grid-inventory/types";
|
||||||
import type { ParsedShape } from '../utils/parse-shape';
|
import type { ParsedShape } from "../utils/parse-shape";
|
||||||
import {ItemData} from "@/samples/slay-the-spire-like/system/types";
|
import { ItemData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of an encounter (combat, event, etc.).
|
* Result of an encounter (combat, event, etc.).
|
||||||
*/
|
*/
|
||||||
export interface EncounterResult {
|
export interface EncounterResult {
|
||||||
/** Gold earned from the encounter */
|
/** Gold earned from the encounter */
|
||||||
goldEarned?: number;
|
goldEarned?: number;
|
||||||
/** HP lost during the encounter */
|
/** HP lost during the encounter */
|
||||||
hpLost?: number;
|
hpLost?: number;
|
||||||
/** HP gained (e.g., from camp heal) */
|
/** HP gained (e.g., from camp heal) */
|
||||||
hpGained?: number;
|
hpGained?: number;
|
||||||
/** Item IDs rewarded */
|
/** Item IDs rewarded */
|
||||||
itemRewards?: string[];
|
itemRewards?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime state of an encounter at a specific node.
|
* Runtime state of an encounter at a specific node.
|
||||||
*/
|
*/
|
||||||
export interface EncounterState {
|
export interface EncounterState {
|
||||||
/** The node ID where this encounter is located */
|
/** The node ID where this encounter is located */
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
/** Whether the encounter has been resolved */
|
/** Whether the encounter has been resolved */
|
||||||
resolved: boolean;
|
resolved: boolean;
|
||||||
/** Optional result data after resolution */
|
/** Optional result data after resolution */
|
||||||
result?: EncounterResult;
|
result?: EncounterResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,12 +34,14 @@ export interface EncounterState {
|
||||||
* Bridges CSV item data with the grid inventory system.
|
* Bridges CSV item data with the grid inventory system.
|
||||||
*/
|
*/
|
||||||
export interface GameItemMeta {
|
export interface GameItemMeta {
|
||||||
/** Original CSV item data */
|
/** Original CSV item data */
|
||||||
itemData: ItemData;
|
itemData: ItemData;
|
||||||
/** Parsed shape for grid placement */
|
/** Parsed shape for grid placement */
|
||||||
shape: ParsedShape;
|
shape: ParsedShape;
|
||||||
/** Consumed uses, if card cost type is uses**/
|
/** Consumed uses, if card cost type is uses**/
|
||||||
depletion?: number;
|
consumedUses?: number;
|
||||||
|
/** Effects applied to the item */
|
||||||
|
effects?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -52,12 +54,12 @@ export type GameItem = InventoryItem<GameItemMeta>;
|
||||||
* Player runtime state.
|
* Player runtime state.
|
||||||
*/
|
*/
|
||||||
export interface PlayerState {
|
export interface PlayerState {
|
||||||
/** Maximum HP */
|
/** Maximum HP */
|
||||||
maxHp: number;
|
maxHp: number;
|
||||||
/** Current HP */
|
/** Current HP */
|
||||||
currentHp: number;
|
currentHp: number;
|
||||||
/** Current gold */
|
/** Current gold */
|
||||||
gold: number;
|
gold: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,23 +67,25 @@ export interface PlayerState {
|
||||||
* Designed to be used inside `MutableSignal.produce()` callbacks.
|
* Designed to be used inside `MutableSignal.produce()` callbacks.
|
||||||
*/
|
*/
|
||||||
export interface RunState {
|
export interface RunState {
|
||||||
/** Generated point crawl map */
|
/** Generated point crawl map */
|
||||||
map: PointCrawlMap;
|
map: PointCrawlMap;
|
||||||
/** Player HP and gold */
|
/** Player HP and gold */
|
||||||
player: PlayerState;
|
player: PlayerState;
|
||||||
/** Grid inventory with placed items */
|
/** Grid inventory with placed items */
|
||||||
inventory: GridInventory<GameItemMeta>;
|
inventory: GridInventory<GameItemMeta>;
|
||||||
/** Current node ID where the player is located */
|
/** Current node ID where the player is located */
|
||||||
currentNodeId: string;
|
currentNodeId: string;
|
||||||
/** State of the encounter at the current node */
|
/** State of the encounter at the current node */
|
||||||
currentEncounter: EncounterState;
|
currentEncounter: EncounterState;
|
||||||
/** Set of node IDs whose encounters have been resolved */
|
/** Set of node IDs whose encounters have been resolved */
|
||||||
resolvedNodeIds: Set<string>;
|
resolvedNodeIds: Set<string>;
|
||||||
/** Internal counter for generating unique item instance IDs */
|
/** Internal counter for generating unique item instance IDs */
|
||||||
_idCounter: { value: number };
|
_idCounter: { value: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of a mutation operation on the run state.
|
* Result of a mutation operation on the run state.
|
||||||
*/
|
*/
|
||||||
export type RunMutationResult = { success: true } | { success: false; reason: string };
|
export type RunMutationResult =
|
||||||
|
| { success: true }
|
||||||
|
| { success: false; reason: string };
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,14 @@ import type {
|
||||||
CombatEntity,
|
CombatEntity,
|
||||||
CombatState,
|
CombatState,
|
||||||
EffectTable,
|
EffectTable,
|
||||||
|
IRunContext,
|
||||||
PlayerEntity,
|
PlayerEntity,
|
||||||
EnemyEntity,
|
EnemyEntity,
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/types";
|
} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
import type { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
import type {
|
||||||
|
EffectData,
|
||||||
|
ItemData,
|
||||||
|
} from "@/samples/slay-the-spire-like/system/types";
|
||||||
import type {
|
import type {
|
||||||
CellKey,
|
CellKey,
|
||||||
GridInventory,
|
GridInventory,
|
||||||
|
|
@ -30,6 +34,31 @@ import type { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress
|
||||||
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";
|
||||||
|
|
||||||
|
function createRunContext(
|
||||||
|
items: Map<string, InventoryItem<GameItemMeta>>,
|
||||||
|
): IRunContext {
|
||||||
|
return {
|
||||||
|
getItemData(id: string): ItemData | null {
|
||||||
|
const item = items.get(id);
|
||||||
|
return item?.meta?.itemData ?? null;
|
||||||
|
},
|
||||||
|
getNeighborItems(_id: string): Iterable<string> {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
getConsumedUses(id: string): number {
|
||||||
|
const item = items.get(id);
|
||||||
|
return item?.meta?.consumedUses ?? 0;
|
||||||
|
},
|
||||||
|
setConsumedUsesAsync(id: string, uses: number): Promise<void> {
|
||||||
|
const item = items.get(id);
|
||||||
|
if (item?.meta) {
|
||||||
|
item.meta.consumedUses = uses;
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function createEffect(
|
function createEffect(
|
||||||
id: string,
|
id: string,
|
||||||
lifecycle: EffectData["lifecycle"],
|
lifecycle: EffectData["lifecycle"],
|
||||||
|
|
@ -65,9 +94,7 @@ function createItem(
|
||||||
id: itemId,
|
id: itemId,
|
||||||
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||||
transform: {
|
transform: {
|
||||||
x: 0,
|
offset: { x: 0, y: 0 },
|
||||||
y: 0,
|
|
||||||
rotation: 0,
|
|
||||||
flipX: false,
|
flipX: false,
|
||||||
flipY: false,
|
flipY: false,
|
||||||
} as unknown as Transform2D,
|
} as unknown as Transform2D,
|
||||||
|
|
@ -82,7 +109,7 @@ function createItem(
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||||
depletion: costType === "uses" ? depletion : undefined,
|
consumedUses: costType === "uses" ? depletion : undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -94,13 +121,14 @@ function createInventory(
|
||||||
const occupied = new Set<CellKey>();
|
const occupied = new Set<CellKey>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
map.set(item.id, item);
|
map.set(item.id, item);
|
||||||
occupied.add(`${item.transform.x},${item.transform.y}`);
|
occupied.add(`${item.transform.offset.x},${item.transform.offset.y}`);
|
||||||
}
|
}
|
||||||
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCombatEntity(hp = 10, maxHp = 10): CombatEntity {
|
function createCombatEntity(hp = 10, maxHp = 10): CombatEntity {
|
||||||
return {
|
return {
|
||||||
|
id: "",
|
||||||
effects: {},
|
effects: {},
|
||||||
hp,
|
hp,
|
||||||
maxHp,
|
maxHp,
|
||||||
|
|
@ -130,9 +158,9 @@ function createEnemyEntity(id: string, hp = 10, maxHp = 10): EnemyEntity {
|
||||||
return {
|
return {
|
||||||
...createCombatEntity(hp, maxHp),
|
...createCombatEntity(hp, maxHp),
|
||||||
id,
|
id,
|
||||||
enemy: { id, name: id, description: "" },
|
enemy: { id, name: id, description: "", intents: null! },
|
||||||
intents: {},
|
intents: {},
|
||||||
currentIntentId: "",
|
currentIntent: null!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,12 +171,6 @@ function createCombatState(
|
||||||
return {
|
return {
|
||||||
player: createPlayerEntity(playerHp),
|
player: createPlayerEntity(playerHp),
|
||||||
enemies,
|
enemies,
|
||||||
inventory: {
|
|
||||||
width: 6,
|
|
||||||
height: 4,
|
|
||||||
items: new Map(),
|
|
||||||
occupiedCells: new Set(),
|
|
||||||
},
|
|
||||||
phase: "playerTurn",
|
phase: "playerTurn",
|
||||||
turnNumber: 1,
|
turnNumber: 1,
|
||||||
result: null,
|
result: null,
|
||||||
|
|
@ -517,8 +539,9 @@ describe("combat/effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "energy", 2, "any", inventory);
|
const result = canPlayCard(player, "energy", 2, "any", run);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -527,8 +550,9 @@ describe("combat/effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 1;
|
player.energy = 1;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "energy", 2, "any", inventory);
|
const result = canPlayCard(player, "energy", 2, "any", run);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -537,8 +561,9 @@ describe("combat/effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "uses", 3, "potion-1", inventory);
|
const result = canPlayCard(player, "uses", 3, "potion-1", run);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
@ -547,8 +572,9 @@ describe("combat/effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem("potion-1", "potion-card", "uses", 3, 3);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 3);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "uses", 3, "potion-1", inventory);
|
const result = canPlayCard(player, "uses", 3, "potion-1", run);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -556,8 +582,9 @@ describe("combat/effects", () => {
|
||||||
it("should reject playing uses card when item not in inventory", () => {
|
it("should reject playing uses card when item not in inventory", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "uses", 1, "missing", inventory);
|
const result = canPlayCard(player, "uses", 1, "missing", run);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -566,51 +593,56 @@ describe("combat/effects", () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 0;
|
player.energy = 0;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
const result = canPlayCard(player, "none", 0, "any", inventory);
|
const result = canPlayCard(player, "none", 0, "any", run);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("payCardCost", () => {
|
describe("payCardCost", () => {
|
||||||
it("should deduct energy for energy cost card", () => {
|
it("should deduct energy for energy cost card", async () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
payCardCost(player, "energy", 2, "any", inventory);
|
await payCardCost(player, "energy", 2, "any", run);
|
||||||
|
|
||||||
expect(player.energy).toBe(1);
|
expect(player.energy).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should increment depletion for uses cost card", () => {
|
it("should increment depletion for uses cost card", async () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
const item = createItem("potion-1", "potion-card", "uses", 3, 1);
|
||||||
const inventory = createInventory([item]);
|
const inventory = createInventory([item]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
payCardCost(player, "uses", 3, "potion-1", inventory);
|
await payCardCost(player, "uses", 3, "potion-1", run);
|
||||||
|
|
||||||
expect(item.meta?.depletion).toBe(4);
|
expect(item.meta?.consumedUses).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should do nothing for none cost card", () => {
|
it("should do nothing for none cost card", async () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
player.energy = 3;
|
player.energy = 3;
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
payCardCost(player, "none", 0, "any", inventory);
|
await payCardCost(player, "none", 0, "any", run);
|
||||||
|
|
||||||
expect(player.energy).toBe(3);
|
expect(player.energy).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle missing item gracefully for uses cost", () => {
|
it("should handle missing item gracefully for uses cost", async () => {
|
||||||
const player = createPlayerEntity();
|
const player = createPlayerEntity();
|
||||||
const inventory = createInventory([]);
|
const inventory = createInventory([]);
|
||||||
|
const run = createRunContext(inventory.items);
|
||||||
|
|
||||||
expect(() =>
|
await expect(
|
||||||
payCardCost(player, "uses", 1, "missing", inventory),
|
payCardCost(player, "uses", 1, "missing", run),
|
||||||
).not.toThrow();
|
).resolves.not.toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { addTriggers } from "@/samples/slay-the-spire-like/data/desert/triggers"
|
||||||
import {
|
import {
|
||||||
CombatState,
|
CombatState,
|
||||||
EnemyEntity,
|
EnemyEntity,
|
||||||
|
IRunContext,
|
||||||
} from "@/samples/slay-the-spire-like/system/combat/types";
|
} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||||
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
import { EffectData } from "@/samples/slay-the-spire-like/system/types";
|
||||||
import {
|
import {
|
||||||
|
|
@ -138,7 +139,6 @@ function createCombatState(overrides: Partial<CombatState> = {}): CombatState {
|
||||||
itemEffects: {},
|
itemEffects: {},
|
||||||
},
|
},
|
||||||
enemies: [],
|
enemies: [],
|
||||||
inventory: createInventory([]),
|
|
||||||
phase: "playerTurn",
|
phase: "playerTurn",
|
||||||
turnNumber: 1,
|
turnNumber: 1,
|
||||||
result: null,
|
result: null,
|
||||||
|
|
@ -155,8 +155,18 @@ function createTestContext(state?: CombatState): IGameContext<CombatState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTriggers(): Triggers {
|
function getTriggers(): Triggers {
|
||||||
const triggers = createTriggers();
|
const run: IRunContext = {
|
||||||
addTriggers(triggers);
|
getConsumedUses() {
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
getItemData() {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
*getNeighborItems() {},
|
||||||
|
async setConsumedUsesAsync() {},
|
||||||
|
};
|
||||||
|
const triggers = createTriggers(run);
|
||||||
|
addTriggers(triggers, run);
|
||||||
return triggers;
|
return triggers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"lib": ["ES2020"],
|
"lib": ["ES2020"],
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"types": ["node"],
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
@ -18,6 +19,6 @@
|
||||||
"@/*": ["src/*"],
|
"@/*": ["src/*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"include": ["src/**/*", "tests/**/*"],
|
"include": ["src/**/*", "tests/**/*", "*.config.ts"],
|
||||||
"exclude": ["node_modules", "dist"],
|
"exclude": ["node_modules", "dist"],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { defineConfig } from 'tsup';
|
import { defineConfig } from "tsup";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from "url";
|
||||||
import { csvLoader } from 'inline-schema/csv-loader/rollup';
|
import { csvLoader } from "inline-schema/csv-loader/rollup";
|
||||||
|
|
||||||
const srcDir = fileURLToPath(new URL('./src', import.meta.url));
|
const srcDir = fileURLToPath(new URL("./src", import.meta.url));
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: ['src/index.ts'],
|
entry: ["src/index.ts"],
|
||||||
format: ['esm'],
|
format: ["esm"],
|
||||||
dts: true,
|
dts: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
external: ['@preact/signals-core', 'mutative', 'inline-schema'],
|
external: ["@preact/signals-core", "mutative", "inline-schema"],
|
||||||
plugins: [csvLoader()],
|
plugins: [csvLoader()],
|
||||||
esbuildOptions(options) {
|
esbuildOptions(options) {
|
||||||
options.alias = {
|
options.alias = {
|
||||||
'@': srcDir,
|
"@": srcDir,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue