feat: cost implementation for card play
This commit is contained in:
parent
8155747cac
commit
fb66ec55c4
|
|
@ -1,7 +1,7 @@
|
|||
import {CombatEntity, EffectTable} from "./types";
|
||||
import {EffectData} from "@/samples/slay-the-spire-like/system/types";
|
||||
import {PlayerEntity} from "@/samples/slay-the-spire-like/system/combat/types";
|
||||
import {CombatState} from "./types";
|
||||
import {CombatEntity, CombatState, EffectTable, PlayerEntity} from "./types";
|
||||
import {CardData, EffectData} 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){
|
||||
let current = effects[effect.id];
|
||||
|
|
@ -83,3 +83,27 @@ export function* getAliveEnemies(state: CombatState) {
|
|||
export function getCombatEntity(state: CombatState, entityKey: string){
|
||||
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 {
|
||||
if (costType === 'energy') {
|
||||
return player.energy >= costCount;
|
||||
}
|
||||
if (costType === 'uses') {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (!item || !item.meta) return false;
|
||||
const depletion = item.meta.depletion ?? 0;
|
||||
return depletion < costCount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function payCardCost(player: PlayerEntity, costType: CardData['costType'], costCount: number, itemId: string, inventory: GridInventory<GameItemMeta>): void {
|
||||
if (costType === 'energy') {
|
||||
player.energy -= costCount;
|
||||
} else if (costType === 'uses') {
|
||||
const item = inventory.items.get(itemId);
|
||||
if (item && item.meta) {
|
||||
item.meta.depletion = (item.meta.depletion ?? 0) + costCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { createPromptDef } from "@/core/game";
|
||||
import {CombatGameContext} from "./types";
|
||||
import {canPlayCard} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||
|
||||
export const prompts = {
|
||||
mainAction: createPromptDef<[string, string?]>(
|
||||
|
|
@ -18,7 +19,12 @@ export async function promptMainAction(game: CombatGameContext){
|
|||
if(!exists) throw `卡牌"${cardId}"不在手牌中`;
|
||||
|
||||
const card = game.value.player.deck.cards[cardId];
|
||||
const {targetType} = card.cardData;
|
||||
const {cardData, itemId} = card;
|
||||
if(!canPlayCard(game.value.player, cardData.costType, cardData.costCount, itemId, game.value.inventory)){
|
||||
throw `无法支付卡牌"${cardId}"的费用`;
|
||||
}
|
||||
|
||||
const {targetType} = cardData;
|
||||
if(targetType === 'single'){
|
||||
if(!targetId) throw `请指定目标`;
|
||||
const target = game.value.enemies.find(e => e.id === targetId);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
addItemEffect,
|
||||
getAliveEnemies, onEntityPostureDamage,
|
||||
onEntityEffectUpkeep,
|
||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay
|
||||
onPlayerItemEffectUpkeep, onItemDiscard, onItemPlay, payCardCost
|
||||
} from "@/samples/slay-the-spire-like/system/combat/effects";
|
||||
import {promptMainAction} from "@/samples/slay-the-spire-like/system/combat/prompts";
|
||||
import {moveToRegion, shuffle} from "@/core/region";
|
||||
|
|
@ -60,8 +60,10 @@ function createTriggers(){
|
|||
onCardPlayed: createTrigger("onCardPlayed", async ctx => {
|
||||
await ctx.game.produceAsync(draft => {
|
||||
const {cards, regions} = draft.player.deck;
|
||||
moveToRegion(cards[ctx.cardId], regions.hand, regions.discardPile);
|
||||
onItemPlay(draft.player, cards[ctx.cardId].itemId);
|
||||
const card = cards[ctx.cardId];
|
||||
payCardCost(draft.player, card.cardData.costType, card.cardData.costCount, card.itemId, draft.inventory);
|
||||
moveToRegion(card, regions.hand, regions.discardPile);
|
||||
onItemPlay(draft.player, card.itemId);
|
||||
});
|
||||
}),
|
||||
onCardDiscarded: createTrigger("onCardDiscarded", async ctx => {
|
||||
|
|
@ -190,7 +192,6 @@ export function createStartWith(build: (triggers: Triggers) => void){
|
|||
const action = await promptMainAction(game);
|
||||
if (action.action === "end-turn") break;
|
||||
if (action.action === "play") {
|
||||
// TODO: energy/use consumption handling
|
||||
await triggers.onCardPlayed.execute(game, action);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,47 @@ import {
|
|||
onItemDiscard,
|
||||
getAliveEnemies,
|
||||
getCombatEntity,
|
||||
canPlayCard,
|
||||
payCardCost,
|
||||
} from '@/samples/slay-the-spire-like/system/combat/effects';
|
||||
import type { CombatEntity, CombatState, EffectTable, PlayerEntity, EnemyEntity } from '@/samples/slay-the-spire-like/system/combat/types';
|
||||
import type { EffectData } from '@/samples/slay-the-spire-like/system/types';
|
||||
import type { GridInventory, InventoryItem } from '@/samples/slay-the-spire-like/system/grid-inventory/types';
|
||||
import type { GameItemMeta } from '@/samples/slay-the-spire-like/system/progress/types';
|
||||
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';
|
||||
|
||||
function createEffect(id: string, lifecycle: EffectData['lifecycle']): EffectData {
|
||||
return { id, name: id, description: '', lifecycle };
|
||||
}
|
||||
|
||||
function createCard(id: string, costType: 'energy' | 'uses' | 'none', costCount: number) {
|
||||
return { id, name: id, desc: '', type: 'item' as const, costType, costCount, targetType: 'none' as const, effects: [] as const };
|
||||
}
|
||||
|
||||
function createItem(itemId: string, cardId: string, costType: 'energy' | 'uses' | 'none', costCount: number, depletion = 0): InventoryItem<GameItemMeta> {
|
||||
return {
|
||||
id: itemId,
|
||||
shape: { id: '1x1', cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||
transform: { x: 0, y: 0, rotation: 0, flipX: false, flipY: false } as unknown as Transform2D,
|
||||
meta: {
|
||||
itemData: { id: itemId, type: 'weapon', name: itemId, shape: '1x1', card: createCard(cardId, costType, costCount), price: 0, description: '' },
|
||||
shape: { id: '1x1', cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape,
|
||||
depletion: costType === 'uses' ? depletion : undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createInventory(items: InventoryItem<GameItemMeta>[]): GridInventory<GameItemMeta> {
|
||||
const map = new Map<string, InventoryItem<GameItemMeta>>();
|
||||
const occupied = new Set<string>();
|
||||
for (const item of items) {
|
||||
map.set(item.id, item);
|
||||
occupied.add(`${item.transform.x},${item.transform.y}`);
|
||||
}
|
||||
return { width: 6, height: 4, items: map, occupiedCells: occupied };
|
||||
}
|
||||
|
||||
function createCombatEntity(hp = 10, maxHp = 10): CombatEntity {
|
||||
return {
|
||||
effects: {},
|
||||
|
|
@ -414,4 +447,104 @@ describe('combat/effects', () => {
|
|||
expect(entity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('canPlayCard', () => {
|
||||
it('should allow playing energy card when player has enough energy', () => {
|
||||
const player = createPlayerEntity();
|
||||
player.energy = 3;
|
||||
const inventory = createInventory([]);
|
||||
|
||||
const result = canPlayCard(player, 'energy', 2, 'any', inventory);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject playing energy card when player lacks energy', () => {
|
||||
const player = createPlayerEntity();
|
||||
player.energy = 1;
|
||||
const inventory = createInventory([]);
|
||||
|
||||
const result = canPlayCard(player, 'energy', 2, 'any', inventory);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow playing uses card when item has remaining uses', () => {
|
||||
const player = createPlayerEntity();
|
||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 1);
|
||||
const inventory = createInventory([item]);
|
||||
|
||||
const result = canPlayCard(player, 'uses', 3, 'potion-1', inventory);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject playing uses card when item is depleted', () => {
|
||||
const player = createPlayerEntity();
|
||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 3);
|
||||
const inventory = createInventory([item]);
|
||||
|
||||
const result = canPlayCard(player, 'uses', 3, 'potion-1', inventory);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject playing uses card when item not in inventory', () => {
|
||||
const player = createPlayerEntity();
|
||||
const inventory = createInventory([]);
|
||||
|
||||
const result = canPlayCard(player, 'uses', 1, 'missing', inventory);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should always allow playing none cost card', () => {
|
||||
const player = createPlayerEntity();
|
||||
player.energy = 0;
|
||||
const inventory = createInventory([]);
|
||||
|
||||
const result = canPlayCard(player, 'none', 0, 'any', inventory);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('payCardCost', () => {
|
||||
it('should deduct energy for energy cost card', () => {
|
||||
const player = createPlayerEntity();
|
||||
player.energy = 3;
|
||||
const inventory = createInventory([]);
|
||||
|
||||
payCardCost(player, 'energy', 2, 'any', inventory);
|
||||
|
||||
expect(player.energy).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment depletion for uses cost card', () => {
|
||||
const player = createPlayerEntity();
|
||||
const item = createItem('potion-1', 'potion-card', 'uses', 3, 1);
|
||||
const inventory = createInventory([item]);
|
||||
|
||||
payCardCost(player, 'uses', 3, 'potion-1', inventory);
|
||||
|
||||
expect(item.meta?.depletion).toBe(4);
|
||||
});
|
||||
|
||||
it('should do nothing for none cost card', () => {
|
||||
const player = createPlayerEntity();
|
||||
player.energy = 3;
|
||||
const inventory = createInventory([]);
|
||||
|
||||
payCardCost(player, 'none', 0, 'any', inventory);
|
||||
|
||||
expect(player.energy).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle missing item gracefully for uses cost', () => {
|
||||
const player = createPlayerEntity();
|
||||
const inventory = createInventory([]);
|
||||
|
||||
expect(() => payCardCost(player, 'uses', 1, 'missing', inventory)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue