refactor(slay-the-spire-like): rename depletion to consumedUses
Rename `depletion` to `consumedUses` in `GameItemMeta` to better reflect its purpose. Update combat effect logic and tests to use the new field name. Also apply consistent formatting and import organization to combat effect modules.
This commit is contained in:
parent
08c6a67d16
commit
9bed2ca13e
|
|
@ -1,134 +1,170 @@
|
||||||
import {CombatEntity, CombatGameContext, CombatState, EffectTable, PlayerEntity} from "./types";
|
|
||||||
import {
|
import {
|
||||||
CardData,
|
CombatEntity,
|
||||||
CardEffectTarget,
|
CombatGameContext,
|
||||||
CardTargetType,
|
CombatState,
|
||||||
EffectData,
|
EffectTable,
|
||||||
EffectTarget
|
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 { GameItemMeta } from "@/samples/slay-the-spire-like/system/progress/types";
|
||||||
import {GridInventory} from "@/samples/slay-the-spire-like/system/grid-inventory/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);
|
inventory: GridInventory<GameItemMeta>,
|
||||||
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 = 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<GameItemMeta>): void {
|
export 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) {
|
inventory: GridInventory<GameItemMeta>,
|
||||||
item.meta.depletion = (item.meta.depletion ?? 0) + costCount;
|
): 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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -591,7 +591,7 @@ describe("combat/effects", () => {
|
||||||
|
|
||||||
payCardCost(player, "uses", 3, "potion-1", inventory);
|
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", () => {
|
it("should do nothing for none cost card", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue