Files
i-want-to-heal/src/combat/rules.ts
T
2026-06-29 22:35:49 -04:00

133 lines
4.0 KiB
TypeScript

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<T>(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<T extends string>(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<string, Spell['kind']> = {
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,
}))
}