2026-04-17 15:30:28 +08:00
|
|
|
import {
|
2026-04-20 10:28:58 +08:00
|
|
|
CombatEntity,
|
|
|
|
|
CombatGameContext,
|
|
|
|
|
CombatState,
|
|
|
|
|
EffectTable,
|
|
|
|
|
PlayerEntity,
|
|
|
|
|
} from "./types";
|
|
|
|
|
import {
|
|
|
|
|
CardData,
|
|
|
|
|
CardEffectTarget,
|
|
|
|
|
CardTargetType,
|
|
|
|
|
EffectData,
|
|
|
|
|
EffectTarget,
|
2026-04-17 15:30:28 +08:00
|
|
|
} from "@/samples/slay-the-spire-like/system/types";
|
2026-04-20 10:28:58 +08:00
|
|
|
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];
|
2026-04-16 14:00:49 +08:00
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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;
|
2026-04-16 14:00:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
export function addEntityEffect(
|
|
|
|
|
entity: CombatEntity,
|
|
|
|
|
effect: EffectData,
|
|
|
|
|
stacks: number,
|
|
|
|
|
) {
|
|
|
|
|
addEffect(entity.effects, effect, stacks);
|
2026-04-16 14:00:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
export function addItemEffect(
|
|
|
|
|
entity: PlayerEntity,
|
|
|
|
|
itemKey: string,
|
|
|
|
|
effect: EffectData,
|
|
|
|
|
stacks: number,
|
|
|
|
|
) {
|
|
|
|
|
entity.itemEffects[itemKey] = entity.itemEffects[itemKey] || {};
|
|
|
|
|
addEffect(entity.itemEffects[itemKey], effect, stacks);
|
2026-04-16 14:00:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-04-16 14:00:49 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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));
|
|
|
|
|
}
|
2026-04-17 11:57:07 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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);
|
2026-04-16 14:00:49 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
}
|
2026-04-17 10:00:14 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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);
|
2026-04-17 11:57:07 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
}
|
2026-04-17 11:57:07 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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);
|
2026-04-17 11:57:07 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
}
|
2026-04-17 11:57:07 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:06:09 +08:00
|
|
|
export function* getAliveEnemies(state: CombatState) {
|
2026-04-20 10:28:58 +08:00
|
|
|
for (let enemy of state.enemies) {
|
|
|
|
|
if (enemy.isAlive) {
|
|
|
|
|
yield enemy;
|
2026-04-17 11:06:09 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
}
|
2026-04-17 11:06:09 +08:00
|
|
|
}
|
2026-04-17 12:58:12 +08:00
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
export function* getEffectTargets(
|
|
|
|
|
target: CardEffectTarget | EffectTarget,
|
|
|
|
|
game: CombatGameContext,
|
|
|
|
|
targetId?: string,
|
|
|
|
|
) {
|
|
|
|
|
if (target === "all" || target === "team") {
|
|
|
|
|
for (const enemy of getAliveEnemies(game.value)) {
|
|
|
|
|
yield enemy;
|
2026-04-17 15:30:28 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
} 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];
|
|
|
|
|
}
|
2026-04-17 15:30:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
export function getCombatEntity(state: CombatState, entityKey: string) {
|
|
|
|
|
return entityKey === "player"
|
|
|
|
|
? state.player
|
|
|
|
|
: state.enemies.find((e) => e.id === entityKey);
|
2026-04-17 13:57:25 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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.consumedUses ?? 0;
|
|
|
|
|
return depletion < costCount;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
2026-04-17 13:57:25 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 10:28:58 +08:00
|
|
|
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.consumedUses = (item.meta.consumedUses ?? 0) + costCount;
|
2026-04-17 13:57:25 +08:00
|
|
|
}
|
2026-04-20 10:28:58 +08:00
|
|
|
}
|
2026-04-17 13:57:25 +08:00
|
|
|
}
|