refactor: reorg
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
import { effectDesertData, cardDesertData } from "../data";
|
||||
import { createStatusCard } from "../deck/factory";
|
||||
import type { PlayerDeck, GameCard } from "../deck/types";
|
||||
import type {
|
||||
BuffTable,
|
||||
CombatEffectEntry,
|
||||
CombatState,
|
||||
EffectData,
|
||||
EffectTarget,
|
||||
EffectTiming,
|
||||
EnemyState,
|
||||
ItemBuff,
|
||||
PlayerCombatState,
|
||||
} from "./types";
|
||||
import {
|
||||
drawCardsToHand,
|
||||
addFatigueCards,
|
||||
discardCard,
|
||||
exhaustCard,
|
||||
getEffectData,
|
||||
discardHand,
|
||||
} from "./state";
|
||||
|
||||
export type DamageResult = {
|
||||
damageDealt: number;
|
||||
blockedByDefend: number;
|
||||
targetDied: boolean;
|
||||
};
|
||||
|
||||
export function applyDamage(
|
||||
state: CombatState,
|
||||
targetKey: "player" | string,
|
||||
amount: number,
|
||||
sourceKey?: "player" | string,
|
||||
): DamageResult {
|
||||
if (amount <= 0) return { damageDealt: 0, blockedByDefend: 0, targetDied: false };
|
||||
|
||||
let actualDamage = amount;
|
||||
let blockedByDefend = 0;
|
||||
|
||||
if (targetKey === "player") {
|
||||
const defendStacks = state.player.buffs["defend"] ?? 0;
|
||||
if (defendStacks > 0) {
|
||||
blockedByDefend = Math.min(defendStacks, actualDamage);
|
||||
actualDamage -= blockedByDefend;
|
||||
state.player.buffs["defend"] = defendStacks - blockedByDefend;
|
||||
if (state.player.buffs["defend"] === 0) {
|
||||
delete state.player.buffs["defend"];
|
||||
}
|
||||
}
|
||||
|
||||
const damageReduce = state.player.buffs["damageReduce"] ?? 0;
|
||||
if (damageReduce > 0 && actualDamage > 0) {
|
||||
actualDamage = Math.max(0, actualDamage - damageReduce);
|
||||
}
|
||||
|
||||
if (actualDamage > 0) {
|
||||
state.player.hp = Math.max(0, state.player.hp - actualDamage);
|
||||
state.player.damageTakenThisTurn += actualDamage;
|
||||
state.player.damagedThisTurn = true;
|
||||
}
|
||||
|
||||
if (blockedByDefend > 0 && defendStacks - blockedByDefend <= 0) {
|
||||
for (const enemyId of state.enemyOrder) {
|
||||
const enemy = state.enemies[enemyId];
|
||||
if (enemy.isAlive && enemy.buffs["defend"] !== undefined) {
|
||||
// Not relevant for player, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
damageDealt: actualDamage,
|
||||
blockedByDefend,
|
||||
targetDied: state.player.hp <= 0,
|
||||
};
|
||||
}
|
||||
|
||||
const enemy = state.enemies[targetKey];
|
||||
if (!enemy || !enemy.isAlive) {
|
||||
return { damageDealt: 0, blockedByDefend: 0, targetDied: false };
|
||||
}
|
||||
|
||||
const defendStacks = enemy.buffs["defend"] ?? 0;
|
||||
if (defendStacks > 0) {
|
||||
blockedByDefend = Math.min(defendStacks, actualDamage);
|
||||
actualDamage -= blockedByDefend;
|
||||
enemy.buffs["defend"] = defendStacks - blockedByDefend;
|
||||
if (enemy.buffs["defend"] === 0) {
|
||||
delete enemy.buffs["defend"];
|
||||
}
|
||||
|
||||
if (defendStacks > 0 && defendStacks - blockedByDefend <= 0) {
|
||||
enemy.hadDefendBroken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (actualDamage > 0) {
|
||||
enemy.hp = Math.max(0, enemy.hp - actualDamage);
|
||||
if (enemy.hp <= 0) {
|
||||
enemy.isAlive = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
damageDealt: actualDamage,
|
||||
blockedByDefend,
|
||||
targetDied: !enemy.isAlive,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyDefend(
|
||||
targetBuffs: BuffTable,
|
||||
amount: number,
|
||||
): void {
|
||||
if (amount <= 0) return;
|
||||
targetBuffs["defend"] = (targetBuffs["defend"] ?? 0) + amount;
|
||||
}
|
||||
|
||||
export function applyBuff(
|
||||
buffs: BuffTable,
|
||||
effectId: string,
|
||||
timing: EffectTiming,
|
||||
stacks: number,
|
||||
): void {
|
||||
if (stacks <= 0) return;
|
||||
buffs[effectId] = (buffs[effectId] ?? 0) + stacks;
|
||||
}
|
||||
|
||||
export function removeBuff(buffs: BuffTable, effectId: string, stacks?: number): number {
|
||||
const current = buffs[effectId] ?? 0;
|
||||
if (stacks === undefined || stacks >= current) {
|
||||
delete buffs[effectId];
|
||||
return current;
|
||||
}
|
||||
buffs[effectId] = current - stacks;
|
||||
return stacks;
|
||||
}
|
||||
|
||||
export function updateBuffs(buffs: BuffTable): void {
|
||||
const toDelete: string[] = [];
|
||||
const toDecrement: string[] = [];
|
||||
|
||||
for (const [effectId] of Object.entries(buffs)) {
|
||||
const effectData = getEffectData(effectId);
|
||||
if (!effectData) continue;
|
||||
|
||||
switch (effectData.timing) {
|
||||
case "temporary":
|
||||
toDelete.push(effectId);
|
||||
break;
|
||||
case "lingering":
|
||||
toDecrement.push(effectId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of toDelete) {
|
||||
delete buffs[id];
|
||||
}
|
||||
|
||||
for (const id of toDecrement) {
|
||||
buffs[id] = (buffs[id] ?? 0) - 1;
|
||||
if (buffs[id] <= 0) {
|
||||
delete buffs[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ResolveEffectContext = {
|
||||
state: CombatState;
|
||||
rng: { nextInt: (n: number) => number };
|
||||
};
|
||||
|
||||
export function resolveEffect(
|
||||
ctx: ResolveEffectContext,
|
||||
target: EffectTarget,
|
||||
effect: EffectData,
|
||||
stacks: number,
|
||||
sourceKey?: "player" | string,
|
||||
sourceCardId?: string,
|
||||
): void {
|
||||
const { state } = ctx;
|
||||
const timing = effect.timing;
|
||||
|
||||
switch (timing) {
|
||||
case "instant":
|
||||
resolveInstantEffect(ctx, target, effect, stacks, sourceKey, sourceCardId);
|
||||
break;
|
||||
case "posture":
|
||||
applyBuffToTarget(state, target, effect.id, stacks, sourceKey);
|
||||
break;
|
||||
case "temporary":
|
||||
case "lingering":
|
||||
case "permanent":
|
||||
applyBuffToTarget(state, target, effect.id, stacks, sourceKey);
|
||||
break;
|
||||
case "card":
|
||||
addStatusCardToDiscard(state, effect.id, stacks);
|
||||
break;
|
||||
case "cardDraw":
|
||||
addStatusCardToDrawPile(state, effect.id, stacks);
|
||||
break;
|
||||
case "cardHand":
|
||||
addStatusCardToHand(state, effect.id, stacks);
|
||||
break;
|
||||
case "item":
|
||||
case "itemUntilPlayed":
|
||||
applyItemBuff(state, effect.id, timing, stacks, sourceCardId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function applyBuffToTarget(
|
||||
state: CombatState,
|
||||
target: EffectTarget,
|
||||
effectId: string,
|
||||
stacks: number,
|
||||
sourceKey?: "player" | string,
|
||||
): void {
|
||||
if (target === "self") {
|
||||
if (sourceKey === "player") {
|
||||
applyBuff(state.player.buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks);
|
||||
} else if (sourceKey && state.enemies[sourceKey]) {
|
||||
applyBuff(state.enemies[sourceKey].buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks);
|
||||
}
|
||||
} else if (target === "player" || target === "team") {
|
||||
applyBuff(state.player.buffs, effectId, getEffectData(effectId)?.timing ?? "permanent", stacks);
|
||||
} else if (target === "target" || target === "all" || target === "random") {
|
||||
// For attack/defend effects, these are handled by resolveInstantEffect
|
||||
}
|
||||
}
|
||||
|
||||
function resolveInstantEffect(
|
||||
ctx: ResolveEffectContext,
|
||||
target: EffectTarget,
|
||||
effect: EffectData,
|
||||
stacks: number,
|
||||
sourceKey?: "player" | string,
|
||||
sourceCardId?: string,
|
||||
): void {
|
||||
const { state, rng } = ctx;
|
||||
|
||||
switch (effect.id) {
|
||||
case "attack": {
|
||||
const damageAmount = stacks;
|
||||
if (target === "all") {
|
||||
for (const enemyId of state.enemyOrder) {
|
||||
const enemy = state.enemies[enemyId];
|
||||
if (enemy.isAlive) {
|
||||
applyDamage(state, enemyId, damageAmount, sourceKey);
|
||||
}
|
||||
}
|
||||
} else if (target === "random") {
|
||||
const aliveEnemies = state.enemyOrder.filter(id => state.enemies[id].isAlive);
|
||||
if (aliveEnemies.length > 0) {
|
||||
const targetId = aliveEnemies[rng.nextInt(aliveEnemies.length)];
|
||||
applyDamage(state, targetId, damageAmount, sourceKey);
|
||||
}
|
||||
} else if (target === "target") {
|
||||
if (sourceKey && sourceKey !== "player" && state.enemies[sourceKey]?.isAlive) {
|
||||
applyDamage(state, "player", damageAmount, sourceKey);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "defend": {
|
||||
if (target === "self" && sourceKey === "player") {
|
||||
applyDefend(state.player.buffs, stacks);
|
||||
} else if (target === "self" && sourceKey && state.enemies[sourceKey]) {
|
||||
applyDefend(state.enemies[sourceKey].buffs, stacks);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "draw": {
|
||||
drawCardsToHand(state.player.deck, stacks);
|
||||
break;
|
||||
}
|
||||
case "gainEnergy": {
|
||||
state.player.energy += stacks;
|
||||
break;
|
||||
}
|
||||
case "removeWound": {
|
||||
removeWoundCards(state.player.deck, stacks);
|
||||
break;
|
||||
}
|
||||
case "tailSting": {
|
||||
applyDamage(state, "player", stacks, sourceKey);
|
||||
break;
|
||||
}
|
||||
case "rollDamage": {
|
||||
const rollStacks = sourceKey && sourceKey !== "player"
|
||||
? state.enemies[sourceKey]?.buffs["roll"] ?? 0
|
||||
: 0;
|
||||
if (rollStacks >= 10) {
|
||||
const damageFromRoll = Math.floor(rollStacks / 10) * 10;
|
||||
applyDamage(state, "player", Math.floor(damageFromRoll / 10), sourceKey);
|
||||
removeBuff(state.enemies[sourceKey!].buffs, "roll", damageFromRoll);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "crossbow": {
|
||||
break;
|
||||
}
|
||||
case "discard": {
|
||||
break;
|
||||
}
|
||||
case "summonMummy":
|
||||
case "summonSandwormLarva":
|
||||
case "reviveMummy": {
|
||||
break;
|
||||
}
|
||||
case "drawChoice":
|
||||
case "transformRandom": {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function addStatusCardToDiscard(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cardId = `status-${effectId}-${Date.now()}-${i}`;
|
||||
const card = createStatusCard(cardId, cardDef.name, cardDef.desc);
|
||||
state.player.deck.cards[card.id] = card;
|
||||
state.player.deck.discardPile.push(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
function addStatusCardToDrawPile(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cardId = `status-${effectId}-${Date.now()}-${i}`;
|
||||
const card = createStatusCard(cardId, cardDef.name, cardDef.desc);
|
||||
state.player.deck.cards[card.id] = card;
|
||||
state.player.deck.drawPile.push(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
function addStatusCardToHand(state: CombatState, effectId: string, count: number): void {
|
||||
const cardDef = cardDesertData.find(c => c.id === effectId);
|
||||
if (!cardDef) return;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const cardId = `status-${effectId}-${Date.now()}-${i}`;
|
||||
const card = createStatusCard(cardId, cardDef.name, cardDef.desc);
|
||||
state.player.deck.cards[card.id] = card;
|
||||
state.player.deck.hand.push(card.id);
|
||||
}
|
||||
}
|
||||
|
||||
function applyItemBuff(
|
||||
state: CombatState,
|
||||
effectId: string,
|
||||
timing: EffectTiming,
|
||||
stacks: number,
|
||||
sourceCardId?: string,
|
||||
): void {
|
||||
if (!sourceCardId) return;
|
||||
|
||||
const card = state.player.deck.cards[sourceCardId];
|
||||
if (!card || !card.sourceItemId) return;
|
||||
|
||||
const itemBuff: ItemBuff = {
|
||||
effectId,
|
||||
stacks,
|
||||
timing,
|
||||
sourceItemId: card.sourceItemId,
|
||||
targetItemId: card.sourceItemId,
|
||||
};
|
||||
state.player.itemBuffs.push(itemBuff);
|
||||
}
|
||||
|
||||
function removeWoundCards(deck: PlayerDeck, count: number): void {
|
||||
let removed = 0;
|
||||
|
||||
for (let i = deck.drawPile.length - 1; i >= 0 && removed < count; i--) {
|
||||
const card = deck.cards[deck.drawPile[i]];
|
||||
if (card && card.itemData === null && card.displayName === "伤口") {
|
||||
delete deck.cards[deck.drawPile[i]];
|
||||
deck.drawPile.splice(i, 1);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = deck.discardPile.length - 1; i >= 0 && removed < count; i--) {
|
||||
const card = deck.cards[deck.discardPile[i]];
|
||||
if (card && card.itemData === null && card.displayName === "伤口") {
|
||||
delete deck.cards[deck.discardPile[i]];
|
||||
deck.discardPile.splice(i, 1);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCardEffects(
|
||||
ctx: ResolveEffectContext,
|
||||
cardId: string,
|
||||
targetEnemyId?: string,
|
||||
): void {
|
||||
const { state } = ctx;
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (!card || !card.itemData) return;
|
||||
|
||||
const sourceKey: "player" | string = "player";
|
||||
|
||||
const effects = card.itemData.onPlay as unknown as CombatEffectEntry[];
|
||||
for (const entry of effects) {
|
||||
const [target, effect, stacks] = entry;
|
||||
|
||||
if (target === "target") {
|
||||
if (targetEnemyId && state.enemies[targetEnemyId]?.isAlive) {
|
||||
if (effect.id === "attack") {
|
||||
const actualDamage = getModifiedAttackDamage(state, cardId, stacks);
|
||||
applyDamage(state, targetEnemyId, actualDamage, "player");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolveEffect(ctx, target, effect, stacks, sourceKey, cardId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getModifiedAttackDamage(
|
||||
state: CombatState,
|
||||
cardId: string,
|
||||
baseDamage: number,
|
||||
): number {
|
||||
let damage = baseDamage;
|
||||
|
||||
const attackBuff = state.player.itemBuffs
|
||||
.filter((b) => b.effectId === "attackBuff" || b.effectId === "attackBuffUntilPlay")
|
||||
.filter((b) => {
|
||||
const card = state.player.deck.cards[cardId];
|
||||
return card && card.sourceItemId === b.targetItemId;
|
||||
})
|
||||
.reduce((sum, b) => sum + b.stacks, 0);
|
||||
damage += attackBuff;
|
||||
|
||||
return Math.max(0, damage);
|
||||
}
|
||||
|
||||
export function getModifiedDefendAmount(
|
||||
state: CombatState,
|
||||
cardId: string,
|
||||
baseDefend: number,
|
||||
): number {
|
||||
let defend = baseDefend;
|
||||
|
||||
const defendBuff = state.player.itemBuffs
|
||||
.filter((b) => b.effectId === "defendBuff" || b.effectId === "defendBuffUntilPlay")
|
||||
.filter((b) => {
|
||||
const card = state.player.deck.cards[cardId];
|
||||
return card && card.sourceItemId === b.targetItemId;
|
||||
})
|
||||
.reduce((sum, b) => sum + b.stacks, 0);
|
||||
defend += defendBuff;
|
||||
|
||||
return Math.max(0, defend);
|
||||
}
|
||||
|
||||
export function canPlayCard(
|
||||
state: CombatState,
|
||||
cardId: string,
|
||||
): { canPlay: boolean; reason?: string } {
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (!card) return { canPlay: false, reason: "卡牌不存在" };
|
||||
|
||||
if (!card.itemData) return { canPlay: false, reason: "状态牌不可打出" };
|
||||
|
||||
const handIdx = state.player.deck.hand.indexOf(cardId);
|
||||
if (handIdx < 0) return { canPlay: false, reason: "卡牌不在手牌中" };
|
||||
|
||||
if (card.itemData.costType === "energy") {
|
||||
if (state.player.energy < card.itemData.costCount) {
|
||||
return { canPlay: false, reason: "能量不足" };
|
||||
}
|
||||
}
|
||||
|
||||
return { canPlay: true };
|
||||
}
|
||||
|
||||
export function playCard(
|
||||
ctx: ResolveEffectContext,
|
||||
cardId: string,
|
||||
targetEnemyId?: string,
|
||||
): { success: boolean; reason?: string } {
|
||||
const { state } = ctx;
|
||||
const check = canPlayCard(state, cardId);
|
||||
if (!check.canPlay) return { success: false, reason: check.reason };
|
||||
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (!card || !card.itemData) return { success: false, reason: "卡牌无效" };
|
||||
|
||||
if (card.itemData.costType === "energy") {
|
||||
state.player.energy -= card.itemData.costCount;
|
||||
}
|
||||
|
||||
resolveCardEffects(ctx, cardId, targetEnemyId);
|
||||
|
||||
if (card.itemData.costType === "uses") {
|
||||
exhaustCard(state.player.deck, cardId);
|
||||
} else {
|
||||
discardCard(state.player.deck, cardId);
|
||||
}
|
||||
|
||||
expireItemBuffsOnCardPlayed(state, cardId);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function expireItemBuffsOnCardPlayed(state: CombatState, cardId: string): void {
|
||||
const card = state.player.deck.cards[cardId];
|
||||
if (!card || !card.sourceItemId) return;
|
||||
|
||||
state.player.itemBuffs = state.player.itemBuffs.filter((buff) => {
|
||||
if (buff.timing === "itemUntilPlayed" && buff.sourceItemId === card.sourceItemId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function areAllEnemiesDead(state: CombatState): boolean {
|
||||
return state.enemyOrder.every(id => !state.enemies[id].isAlive);
|
||||
}
|
||||
|
||||
export function isPlayerDead(state: CombatState): boolean {
|
||||
return state.player.hp <= 0;
|
||||
}
|
||||
Reference in New Issue
Block a user