refactor(slay-the-spire-like): use IRunContext for combat logic
Decouple combat systems from the inventory and progress state by introducing `IRunContext`. This replaces direct access to `GridInventory` and `GameItemMeta` with abstract methods for retrieving item data, neighbors, and managing consumed uses.
This commit is contained in:
parent
9bed2ca13e
commit
5019bc6324
|
|
@ -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];
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
CombatGameContext,
|
CombatGameContext,
|
||||||
CombatState,
|
CombatState,
|
||||||
EffectTable,
|
EffectTable,
|
||||||
|
IRunContext,
|
||||||
PlayerEntity,
|
PlayerEntity,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,8 +13,6 @@ import {
|
||||||
EffectData,
|
EffectData,
|
||||||
EffectTarget,
|
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(
|
export function addEffect(
|
||||||
effects: EffectTable,
|
effects: EffectTable,
|
||||||
|
|
@ -138,33 +137,32 @@ export function canPlayCard(
|
||||||
costType: CardData["costType"],
|
costType: CardData["costType"],
|
||||||
costCount: number,
|
costCount: number,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
inventory: GridInventory<GameItemMeta>,
|
run: IRunContext,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (costType === "energy") {
|
if (costType === "energy") {
|
||||||
return player.energy >= costCount;
|
return player.energy >= costCount;
|
||||||
}
|
}
|
||||||
if (costType === "uses") {
|
if (costType === "uses") {
|
||||||
const item = inventory.items.get(itemId);
|
const item = run.getItemData(itemId);
|
||||||
if (!item || !item.meta) return false;
|
if (!item) return false;
|
||||||
const depletion = item.meta.consumedUses ?? 0;
|
const maxUses = item?.card.costType === "energy" ? item.card.costCount : 0;
|
||||||
return depletion < costCount;
|
const consumed = run.getConsumedUses(itemId);
|
||||||
|
return consumed + costCount <= maxUses;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function payCardCost(
|
export async function payCardCost(
|
||||||
player: PlayerEntity,
|
player: PlayerEntity,
|
||||||
costType: CardData["costType"],
|
costType: CardData["costType"],
|
||||||
costCount: number,
|
costCount: number,
|
||||||
itemId: string,
|
itemId: string,
|
||||||
inventory: GridInventory<GameItemMeta>,
|
run: IRunContext,
|
||||||
): void {
|
): Promise<void> {
|
||||||
if (costType === "energy") {
|
if (costType === "energy") {
|
||||||
player.energy -= costCount;
|
player.energy -= costCount;
|
||||||
} else if (costType === "uses") {
|
} else if (costType === "uses") {
|
||||||
const item = inventory.items.get(itemId);
|
const consumed = run.getConsumedUses(itemId);
|
||||||
if (item && item.meta) {
|
await run.setConsumedUsesAsync(itemId, consumed + costCount);
|
||||||
item.meta.consumedUses = (item.meta.consumedUses ?? 0) + 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(
|
||||||
|
game: CombatGameContext,
|
||||||
|
run: IRunContext,
|
||||||
|
) {
|
||||||
return await game.prompt(prompts.mainAction, (cardId, targetId) => {
|
return await game.prompt(prompts.mainAction, (cardId, targetId) => {
|
||||||
if(cardId === 'end-turn') return {
|
if (cardId === "end-turn")
|
||||||
action: 'end-turn' as 'end-turn'
|
return {
|
||||||
|
action: "end-turn" as "end-turn",
|
||||||
};
|
};
|
||||||
|
|
||||||
const exists = game.value.player.deck.regions.hand.childIds.includes(cardId);
|
const exists =
|
||||||
|
game.value.player.deck.regions.hand.childIds.includes(cardId);
|
||||||
if (!exists) throw `卡牌"${cardId}"不在手牌中`;
|
if (!exists) throw `卡牌"${cardId}"不在手牌中`;
|
||||||
|
|
||||||
const card = game.value.player.deck.cards[cardId];
|
const card = game.value.player.deck.cards[cardId];
|
||||||
const { cardData, itemId } = card;
|
const { cardData, itemId } = card;
|
||||||
if(!canPlayCard(game.value.player, cardData.costType, cardData.costCount, itemId, game.value.inventory)){
|
if (
|
||||||
|
!canPlayCard(
|
||||||
|
game.value.player,
|
||||||
|
cardData.costType,
|
||||||
|
cardData.costCount,
|
||||||
|
itemId,
|
||||||
|
run,
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw `无法支付卡牌"${cardId}"的费用`;
|
throw `无法支付卡牌"${cardId}"的费用`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { targetType } = cardData;
|
const { targetType } = cardData;
|
||||||
if(targetType === 'single'){
|
if (targetType === "single") {
|
||||||
if (!targetId) throw `请指定目标`;
|
if (!targetId) throw `请指定目标`;
|
||||||
const target = game.value.enemies.find(e => e.id === targetId);
|
const target = game.value.enemies.find((e) => e.id === targetId);
|
||||||
if (!target) throw `目标"${targetId}"不存在`;
|
if (!target) throw `目标"${targetId}"不存在`;
|
||||||
if (!target.isAlive) throw `目标"${targetId}"已死亡`;
|
if (!target.isAlive) throw `目标"${targetId}"已死亡`;
|
||||||
}else if(targetType === 'none'){
|
} else if (targetType === "none") {
|
||||||
if (targetId) throw `目标"${targetId}"无效`;
|
if (targetId) throw `目标"${targetId}"无效`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: 'play' as 'play',
|
action: "play" as "play",
|
||||||
cardId,
|
cardId,
|
||||||
targetId
|
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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue