import type { PartyMember, Spell } from '../game' import type { Ability } from '../profile' import { createStackCounts, stackCount } from './stackCounts' export const DEFAULT_TICK_MS = 700 export function clamp(value: number, min: number, max: number) { return Math.min(max, Math.max(min, value)) } export function chooseRandom(items: T[], count: number) { const pool = [...items] const result: T[] = [] while (pool.length > 0 && result.length < count) { const index = Math.floor(Math.random() * pool.length) result.push(pool.splice(index, 1)[0]) } return result } export function effectiveMaxHealth(member: PartyMember) { return Math.max( 1, Math.round(member.maxHealth * (member.maxHealthPenaltyTicks && member.maxHealthPenaltyTicks > 0 ? 0.75 : 1)), ) } export function healAmount(member: PartyMember, amount: number, multiplier = 1) { return Math.round(amount * (member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1) * multiplier) } export function healMember(member: PartyMember, amount: number, multiplier = 1) { return clamp(member.health + healAmount(member, amount, multiplier), 0, effectiveMaxHealth(member)) } export function memberHotEffects(member: PartyMember) { if (member.hotEffects?.length) return member.hotEffects return member.hotTicks > 0 ? [{ id: 'legacy-renew', spellId: 'legacy-renew', label: 'Renew', ticks: member.hotTicks, power: 6 }] : [] } export function effectId(prefix: string) { return `${prefix}-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}` } export function addHotEffect(member: PartyMember, spell: Spell, ticks = 5) { const nextEffect = { id: effectId(spell.id), spellId: spell.id, label: spell.name, ticks, power: Math.max(1, Math.round(spell.power / 2)), } const currentEffects = memberHotEffects(member).filter((effect) => effect.spellId !== spell.id) return [...currentEffects, nextEffect] } export function addBounceHeal(member: PartyMember, spell: Spell) { return [ ...(member.bounceHeals ?? []), { id: effectId(spell.id), label: spell.name, charges: 4, power: spell.power, }, ] } export function tickHotEffects(effects: PartyMember['hotEffects']) { return (effects ?? []) .map((effect) => ({ ...effect, ticks: effect.ticks - 1 })) .filter((effect) => effect.ticks > 0) } export function formatEffectTime(ticks: number, tickMs = DEFAULT_TICK_MS) { const seconds = (ticks * tickMs) / 1000 return Number.isInteger(seconds) ? `${seconds}s` : `${seconds.toFixed(1)}s` } export function buffStacks(items: readonly T[], id: T) { return stackCount(createStackCounts(items), id) } export function createLogEntry(nextLogId: { current: number }, text: string, tone: 'system' | 'heal' | 'danger' | 'loot') { return { id: nextLogId.current++, text, tone } } export function toCombatSpell(ability: Ability, key: string, healingPower = 0): Spell { const kinds: Record = { direct_heal: 'direct', direct_hot: 'direct', heal_over_time: 'hot', party_heal: 'group', party_hot: 'group', party_absorb: 'group', absorb: 'shield', damage_reduction: 'damage_reduction', bounce_heal: 'bounce_heal', cleanse: 'cleanse', } return { id: String(ability.id), key, name: ability.name, description: ability.description, cost: ability.cost, cooldown: ability.cooldown, power: ability.power + healingPower, glyph: ability.glyph, kind: kinds[ability.spellType] ?? 'direct', effectType: ability.spellType, } } export function resetParty(partyTemplate: PartyMember[]) { return partyTemplate.map((member) => ({ ...member, health: member.maxHealth, shield: 0, hotTicks: 0, hotEffects: undefined, debuff: undefined, debuffTicks: undefined, poisonStacks: undefined, maxHealthPenaltyTicks: undefined, healingReductionTicks: undefined, damageReductionTicks: undefined, bounceHeals: undefined, })) }