import type { PartyMember, Spell } from '../game' import { effectiveMaxHealth } from './rules' export type CpuHealBehavior = { directHealThreshold: number groupHealThreshold: number hotThreshold: number shieldThreshold: number } export type CpuTurnBehavior = CpuHealBehavior & { actionEveryTicks: number mistakeChance: number } export function shouldCpuAct({ elapsedTicks, behavior, }: { elapsedTicks: number behavior: CpuTurnBehavior }) { return elapsedTicks % behavior.actionEveryTicks === 0 && Math.random() >= behavior.mistakeChance } export function chooseCpuHealActions({ party, spells, behavior, preferSlots = false, }: { party: PartyMember[] spells: Spell[] behavior: CpuHealBehavior preferSlots?: boolean }) { let livingCount = 0 let healthRatioTotal = 0 let lowest: PartyMember | null = null let tank: PartyMember | null = null let cleanseTarget: PartyMember | null = null let renewTarget: PartyMember | null = null let shieldTarget: PartyMember | null = null let woundedCount = 0 for (const member of party) { if (member.health <= 0) continue livingCount += 1 const ratio = member.health / effectiveMaxHealth(member) healthRatioTotal += ratio if (!lowest || ratio < lowest.health / effectiveMaxHealth(lowest)) lowest = member if (!tank && member.role === 'Tank') tank = member if (!cleanseTarget && (member.debuff || (member.poisonStacks ?? 0) > 0)) cleanseTarget = member if (!renewTarget && member.hotTicks <= 1 && ratio < behavior.hotThreshold) renewTarget = member if (!shieldTarget && member.role === 'Tank' && member.shield <= 5 && ratio < behavior.shieldThreshold) { shieldTarget = member } if (ratio < behavior.directHealThreshold) woundedCount += 1 } if (!lowest || livingCount === 0) return [] const averageHealth = healthRatioTotal / livingCount const spellByKind = (kind: Spell['kind']) => spells.find((candidate) => candidate.kind === kind) const spellBySlot = (slot: string) => spells.find((candidate) => candidate.key === slot) const direct = preferSlots ? spellBySlot('1') : spellByKind('direct') const hot = preferSlots ? spellBySlot('2') : spellByKind('hot') const group = preferSlots ? spellBySlot('3') : spellByKind('group') const shield = preferSlots ? spellBySlot('4') : spellByKind('shield') const cleanse = preferSlots ? spellBySlot('5') : spellByKind('cleanse') const ordered: Array<{ spell: Spell | undefined; targetId: string | null }> = [ { spell: cleanseTarget ? cleanse : undefined, targetId: cleanseTarget?.id ?? null }, { spell: averageHealth < behavior.groupHealThreshold ? group : undefined, targetId: lowest.id }, { spell: shieldTarget ? shield : undefined, targetId: shieldTarget?.id ?? null }, { spell: woundedCount > 0 ? direct : undefined, targetId: lowest.id }, { spell: renewTarget ? hot : undefined, targetId: renewTarget?.id ?? null }, { spell: tank ? direct : undefined, targetId: tank?.id ?? null }, ] return ordered .filter((action): action is { spell: Spell; targetId: string } => Boolean(action.spell && action.targetId)) } export function chooseCpuHealAction(options: Parameters[0]) { return chooseCpuHealActions(options)[0] ?? null } export function runCpuHealTurn({ side, elapsedTicks, spells, behavior, preferSlots = false, applySpell, }: { side: TSide & { party: PartyMember[] } elapsedTicks: number spells: Spell[] behavior: CpuTurnBehavior preferSlots?: boolean applySpell: (side: TSide, spell: Spell, targetId: string) => void }) { if (!shouldCpuAct({ elapsedTicks, behavior })) return false const action = chooseCpuHealAction({ party: side.party, spells, behavior, preferSlots, }) if (!action) return false applySpell(side, action.spell, action.targetId) return true }