Files
boardgame-core/src/samples/slay-the-spire-like/system/combat/effects.ts
T
hypercross 8ec95cbf81 refactor: rename 'user' target to 'source' in slay-the-spire-like
Updates the card effect and intent target types from 'user' to 'source'
within the desert data files and combat effect system to maintain
consistency. Also reformat command tests to use 2-space indentation.
2026-04-23 00:35:28 +08:00

172 lines
4.7 KiB
TypeScript

import {
CombatEntity,
CombatGameContext,
CombatState,
EffectTable,
IRunContext,
PlayerEntity,
} from "./types";
import {
CardData,
CardEffectTarget,
EffectData,
IntentEffectTarget,
} from "@/samples/slay-the-spire-like/system/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 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 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 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) {
for (let enemy of state.enemies) {
if (enemy.isAlive) {
yield enemy;
}
}
}
export function* getEffectTargets(
target: CardEffectTarget | IntentEffectTarget,
game: CombatGameContext,
targetId?: string,
sourceEntityKey: "player" | string = "player",
) {
if (target === "eachEnemy") {
for (const enemy of getAliveEnemies(game.value)) {
yield enemy;
}
} else if (target === "source") {
const entity = getCombatEntity(game.value, sourceEntityKey);
if (entity) yield entity;
} else if (target === "player") {
yield game.value.player;
} else if (target === "eachTarget") {
if (!targetId) return;
const entity = getCombatEntity(game.value, targetId);
if (entity) yield entity;
} else if (target === "randomEnemy") {
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 canPlayCard(
player: PlayerEntity,
costType: CardData["costType"],
costCount: number,
itemId: string,
run: IRunContext,
): boolean {
if (costType === "energy") {
return player.energy >= costCount;
}
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 async function payCardCost(
player: PlayerEntity,
costType: CardData["costType"],
costCount: number,
itemId: string,
run: IRunContext,
): Promise<void> {
if (costType === "energy") {
player.energy -= costCount;
} else if (costType === "uses") {
const consumed = run.getConsumedUses(itemId);
await run.setConsumedUsesAsync(itemId, consumed + costCount);
}
}