diff --git a/src/samples/slay-the-spire-like/system/combat/effects.ts b/src/samples/slay-the-spire-like/system/combat/effects.ts index 695318e..7aba3e1 100644 --- a/src/samples/slay-the-spire-like/system/combat/effects.ts +++ b/src/samples/slay-the-spire-like/system/combat/effects.ts @@ -1,134 +1,170 @@ -import {CombatEntity, CombatGameContext, CombatState, EffectTable, PlayerEntity} from "./types"; import { - CardData, - CardEffectTarget, - CardTargetType, - EffectData, - EffectTarget + CombatEntity, + CombatGameContext, + CombatState, + EffectTable, + PlayerEntity, +} from "./types"; +import { + CardData, + CardEffectTarget, + CardTargetType, + EffectData, + EffectTarget, } from "@/samples/slay-the-spire-like/system/types"; -import {GameItemMeta} from "@/samples/slay-the-spire-like/system/progress/types"; -import {GridInventory} from "@/samples/slay-the-spire-like/system/grid-inventory/types"; +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]; - - if(!current) current = {data: effect, stacks}; - else current.stacks += stacks; - - 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 addEffect( + effects: EffectTable, + effect: EffectData, + stacks: number, +) { + let current = effects[effect.id]; + + if (!current) current = { data: effect, stacks }; + else current.stacks += stacks; + + 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){ - addEffect(entity.effects, effect, stacks); +export function addEntityEffect( + entity: CombatEntity, + effect: EffectData, + stacks: number, +) { + addEffect(entity.effects, effect, stacks); } -export function addItemEffect(entity: PlayerEntity, itemKey: string, effect: EffectData, stacks: number){ - entity.itemEffects[itemKey] = entity.itemEffects[itemKey] || {}; - addEffect(entity.itemEffects[itemKey], effect, stacks); +export function addItemEffect( + entity: PlayerEntity, + itemKey: string, + effect: EffectData, + stacks: number, +) { + entity.itemEffects[itemKey] = entity.itemEffects[itemKey] || {}; + addEffect(entity.itemEffects[itemKey], effect, stacks); } -export function onEntityEffectUpkeep(entity: CombatEntity){ - for(const effect of Object.values(entity.effects)){ - const lifecycle = effect.data.lifecycle; - if(lifecycle === 'temporary') - addEntityEffect(entity, effect.data, -effect.stacks); - else if(lifecycle === 'lingering') - addEntityEffect(entity, effect.data, effect.stacks >= 0 ? -1 : 1); +export function onEntityEffectUpkeep(entity: CombatEntity) { + for (const effect of Object.values(entity.effects)) { + const lifecycle = effect.data.lifecycle; + if (lifecycle === "temporary") + addEntityEffect(entity, effect.data, -effect.stacks); + else if (lifecycle === "lingering") + 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){ - 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 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 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 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 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) { - for (let enemy of state.enemies) { - if (enemy.isAlive) { - yield enemy; - } + for (let enemy of state.enemies) { + if (enemy.isAlive) { + yield enemy; } + } } -export function* getEffectTargets(target: CardEffectTarget | EffectTarget, game: CombatGameContext, targetId?: string){ - if(target === 'all' || target === 'team'){ - for(const enemy of getAliveEnemies(game.value)){ - yield enemy; - } - } else if(target === 'self') { - yield game.value.player; - } else if(target === 'target'){ - if(!targetId) return; - const entity = getCombatEntity(game.value, targetId); - if(entity) yield entity; - } else if(target === 'random'){ - const aliveEnemies = [...getAliveEnemies(game.value)]; - if(aliveEnemies.length === 0) return; - const index = game.rng.nextInt(aliveEnemies.length); - yield aliveEnemies[index]; +export function* getEffectTargets( + target: CardEffectTarget | EffectTarget, + game: CombatGameContext, + targetId?: string, +) { + if (target === "all" || target === "team") { + for (const enemy of getAliveEnemies(game.value)) { + yield enemy; } + } else if (target === "self") { + yield game.value.player; + } else if (target === "target") { + if (!targetId) return; + const entity = getCombatEntity(game.value, targetId); + if (entity) yield entity; + } else if (target === "random") { + const aliveEnemies = [...getAliveEnemies(game.value)]; + if (aliveEnemies.length === 0) return; + const index = game.rng.nextInt(aliveEnemies.length); + yield aliveEnemies[index]; + } } -export function getCombatEntity(state: CombatState, entityKey: string){ - return entityKey === 'player' ? state.player : state.enemies.find(e => e.id === entityKey); +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): 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 canPlayCard( + player: PlayerEntity, + costType: CardData["costType"], + costCount: number, + itemId: string, + inventory: GridInventory, +): 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.consumedUses ?? 0; + return depletion < costCount; + } + return true; } -export function payCardCost(player: PlayerEntity, costType: CardData['costType'], costCount: number, itemId: string, inventory: GridInventory): 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; - } +export function payCardCost( + player: PlayerEntity, + costType: CardData["costType"], + costCount: number, + itemId: string, + inventory: GridInventory, +): void { + if (costType === "energy") { + player.energy -= costCount; + } else if (costType === "uses") { + const item = inventory.items.get(itemId); + if (item && item.meta) { + item.meta.consumedUses = (item.meta.consumedUses ?? 0) + costCount; } + } } diff --git a/src/samples/slay-the-spire-like/system/progress/types.ts b/src/samples/slay-the-spire-like/system/progress/types.ts index 89853a6..66ac845 100644 --- a/src/samples/slay-the-spire-like/system/progress/types.ts +++ b/src/samples/slay-the-spire-like/system/progress/types.ts @@ -1,32 +1,32 @@ -import type { PointCrawlMap } from '../map/types'; -import type { GridInventory, InventoryItem } from '../grid-inventory/types'; -import type { ParsedShape } from '../utils/parse-shape'; -import {ItemData} from "@/samples/slay-the-spire-like/system/types"; +import type { PointCrawlMap } from "../map/types"; +import type { GridInventory, InventoryItem } from "../grid-inventory/types"; +import type { ParsedShape } from "../utils/parse-shape"; +import { ItemData } from "@/samples/slay-the-spire-like/system/types"; /** * Result of an encounter (combat, event, etc.). */ export interface EncounterResult { - /** Gold earned from the encounter */ - goldEarned?: number; - /** HP lost during the encounter */ - hpLost?: number; - /** HP gained (e.g., from camp heal) */ - hpGained?: number; - /** Item IDs rewarded */ - itemRewards?: string[]; + /** Gold earned from the encounter */ + goldEarned?: number; + /** HP lost during the encounter */ + hpLost?: number; + /** HP gained (e.g., from camp heal) */ + hpGained?: number; + /** Item IDs rewarded */ + itemRewards?: string[]; } /** * Runtime state of an encounter at a specific node. */ export interface EncounterState { - /** The node ID where this encounter is located */ - nodeId: string; - /** Whether the encounter has been resolved */ - resolved: boolean; - /** Optional result data after resolution */ - result?: EncounterResult; + /** The node ID where this encounter is located */ + nodeId: string; + /** Whether the encounter has been resolved */ + resolved: boolean; + /** Optional result data after resolution */ + result?: EncounterResult; } /** @@ -34,12 +34,14 @@ export interface EncounterState { * Bridges CSV item data with the grid inventory system. */ export interface GameItemMeta { - /** Original CSV item data */ - itemData: ItemData; - /** Parsed shape for grid placement */ - shape: ParsedShape; - /** Consumed uses, if card cost type is uses**/ - depletion?: number; + /** Original CSV item data */ + itemData: ItemData; + /** Parsed shape for grid placement */ + shape: ParsedShape; + /** Consumed uses, if card cost type is uses**/ + consumedUses?: number; + /** Effects applied to the item */ + effects?: Record; } /** @@ -52,12 +54,12 @@ export type GameItem = InventoryItem; * Player runtime state. */ export interface PlayerState { - /** Maximum HP */ - maxHp: number; - /** Current HP */ - currentHp: number; - /** Current gold */ - gold: number; + /** Maximum HP */ + maxHp: number; + /** Current HP */ + currentHp: number; + /** Current gold */ + gold: number; } /** @@ -65,23 +67,25 @@ export interface PlayerState { * Designed to be used inside `MutableSignal.produce()` callbacks. */ export interface RunState { - /** Generated point crawl map */ - map: PointCrawlMap; - /** Player HP and gold */ - player: PlayerState; - /** Grid inventory with placed items */ - inventory: GridInventory; - /** Current node ID where the player is located */ - currentNodeId: string; - /** State of the encounter at the current node */ - currentEncounter: EncounterState; - /** Set of node IDs whose encounters have been resolved */ - resolvedNodeIds: Set; - /** Internal counter for generating unique item instance IDs */ - _idCounter: { value: number }; + /** Generated point crawl map */ + map: PointCrawlMap; + /** Player HP and gold */ + player: PlayerState; + /** Grid inventory with placed items */ + inventory: GridInventory; + /** Current node ID where the player is located */ + currentNodeId: string; + /** State of the encounter at the current node */ + currentEncounter: EncounterState; + /** Set of node IDs whose encounters have been resolved */ + resolvedNodeIds: Set; + /** Internal counter for generating unique item instance IDs */ + _idCounter: { value: number }; } /** * 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 }; diff --git a/tests/samples/slay-the-spire-like/combat/effects.test.ts b/tests/samples/slay-the-spire-like/combat/effects.test.ts index 4563fc1..445cae9 100644 --- a/tests/samples/slay-the-spire-like/combat/effects.test.ts +++ b/tests/samples/slay-the-spire-like/combat/effects.test.ts @@ -82,7 +82,7 @@ function createItem( description: "", }, shape: { id: "1x1", cells: [{ x: 0, y: 0 }] } as unknown as ParsedShape, - depletion: costType === "uses" ? depletion : undefined, + consumedUses: costType === "uses" ? depletion : undefined, }, }; } @@ -591,7 +591,7 @@ describe("combat/effects", () => { payCardCost(player, "uses", 3, "potion-1", inventory); - expect(item.meta?.depletion).toBe(4); + expect(item.meta?.consumedUses).toBe(4); }); it("should do nothing for none cost card", () => {