I Want To Heal 2 build v1.0.5 code
This commit is contained in:
@@ -0,0 +1,760 @@
|
||||
import { getEmptyClaudeCraftAbilityModifier, type TalentModifiers } from './claudeCraftTalents'
|
||||
import { getArenaAbility, getArenaClassVitals, type ArenaClassId } from './actionClassKits'
|
||||
|
||||
export type PartyRole = 'healer' | 'tank' | 'melee' | 'ranged'
|
||||
export type SpellSlot = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
|
||||
|
||||
export type SpellDefinition = {
|
||||
slot: SpellSlot
|
||||
abilityId: ClaudeCraftHealerAbilityId
|
||||
name: string
|
||||
cooldown: number
|
||||
castTime: number
|
||||
}
|
||||
|
||||
export type ActionHealerClassId = 'priest' | 'paladin' | 'shaman' | 'druid'
|
||||
|
||||
export type ClaudeCraftHealerAbilityId =
|
||||
| 'smite'
|
||||
| 'lesser_heal'
|
||||
| 'power_word_fortitude'
|
||||
| 'shadow_word_pain'
|
||||
| 'power_word_shield'
|
||||
| 'renew'
|
||||
| 'mind_blast'
|
||||
| 'heal'
|
||||
| 'mind_flay'
|
||||
| 'flash_heal'
|
||||
| 'judgement'
|
||||
| 'holy_light'
|
||||
| 'flash_of_light'
|
||||
| 'blessing_of_might'
|
||||
| 'consecration'
|
||||
| 'divine_protection'
|
||||
| 'exorcism'
|
||||
| 'lay_on_hands'
|
||||
| 'seal_of_righteousness'
|
||||
| 'lightning_bolt'
|
||||
| 'healing_wave'
|
||||
| 'lightning_shield'
|
||||
| 'earth_shock'
|
||||
| 'flame_shock'
|
||||
| 'frost_shock'
|
||||
| 'healing_touch'
|
||||
| 'rejuvenation'
|
||||
| 'regrowth'
|
||||
| 'barkskin'
|
||||
| 'wrath'
|
||||
| 'moonfire'
|
||||
| 'insect_swarm'
|
||||
| 'starfire'
|
||||
| 'faerie_fire'
|
||||
|
||||
export type ClaudeCraftPriestAbilityId = Extract<
|
||||
ClaudeCraftHealerAbilityId,
|
||||
| 'smite'
|
||||
| 'lesser_heal'
|
||||
| 'power_word_fortitude'
|
||||
| 'shadow_word_pain'
|
||||
| 'power_word_shield'
|
||||
| 'renew'
|
||||
| 'mind_blast'
|
||||
| 'heal'
|
||||
| 'mind_flay'
|
||||
| 'flash_heal'
|
||||
>
|
||||
|
||||
export type ActionSpellTarget = 'Enemy target' | 'Friendly target'
|
||||
export type ActionSpellSchool = 'Holy' | 'Shadow' | 'Nature' | 'Fire' | 'Frost' | 'Arcane' | 'Physical'
|
||||
|
||||
export type ActionSpellbook = {
|
||||
classId: ActionHealerClassId
|
||||
iconFolder: string
|
||||
spells: Record<SpellSlot, SpellDefinition>
|
||||
manaCosts: Record<SpellSlot, number>
|
||||
ranks: Record<SpellSlot, number>
|
||||
schools: Record<SpellSlot, ActionSpellSchool>
|
||||
targets: Record<SpellSlot, ActionSpellTarget>
|
||||
descriptions: Record<SpellSlot, string>
|
||||
bar: SpellSlot[]
|
||||
}
|
||||
|
||||
export type CastState = {
|
||||
spell: SpellSlot
|
||||
targetId: string
|
||||
remaining: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type HealOverTimeEffectId = 'renew' | 'rejuvenation' | 'regrowth'
|
||||
|
||||
export type HealOverTimeEffect = {
|
||||
id: HealOverTimeEffectId
|
||||
label: string
|
||||
remaining: number
|
||||
tickHeal: number
|
||||
tickSeconds: number
|
||||
tickTimer: number
|
||||
}
|
||||
|
||||
export type HealOverTimeFrame = Pick<HealOverTimeEffect, 'id' | 'label' | 'remaining'>
|
||||
|
||||
export type SpellResourceState = {
|
||||
mana: number
|
||||
maxMana: number
|
||||
storedMomentumCasts: number
|
||||
storedMomentumReady: boolean
|
||||
}
|
||||
|
||||
export type ActionRangePoint = {
|
||||
x: number
|
||||
y: number
|
||||
radius?: number
|
||||
}
|
||||
|
||||
export type ActionSpellReachBlocker = 'range' | 'lineOfSight'
|
||||
|
||||
export type ActionSpellReachResult = {
|
||||
distance: number
|
||||
effectiveRange: number
|
||||
ok: true
|
||||
} | {
|
||||
distance: number
|
||||
effectiveRange: number
|
||||
ok: false
|
||||
reason: ActionSpellReachBlocker
|
||||
}
|
||||
|
||||
export type ShieldedHealthState = {
|
||||
hp: number
|
||||
maxHp: number
|
||||
shield: number
|
||||
invulnerableTimer?: number
|
||||
}
|
||||
|
||||
export type RenewState = {
|
||||
healOverTimes?: HealOverTimeEffect[]
|
||||
renewTimer: number
|
||||
renewTickTimer: number
|
||||
}
|
||||
|
||||
export const ACTION_SQUARE_SIZE = 48
|
||||
export const HEAL_RANGE = ACTION_SQUARE_SIZE * 6
|
||||
export const RANGED_ATTACK_RANGE = ACTION_SQUARE_SIZE * 6
|
||||
|
||||
export const ACTION_MOVEMENT_RULES = {
|
||||
baseSpeed: 245,
|
||||
} as const
|
||||
|
||||
export const ACTION_CONTROL_RULES = {
|
||||
stunSeconds: 0.92,
|
||||
invulnerableSeconds: 0.7,
|
||||
} as const
|
||||
|
||||
export const HEALING_SPELL_RULES = {
|
||||
mendHeal: 34,
|
||||
renewSeconds: 15,
|
||||
renewTickSeconds: 3,
|
||||
renewHeal: 28,
|
||||
radianceHeal: 250,
|
||||
sunWardShield: 145,
|
||||
purifyHeal: 131,
|
||||
dungeonShieldCap: 60,
|
||||
arenaShieldCapRatio: 0.72,
|
||||
} as const
|
||||
|
||||
export const SPELLS: Record<SpellSlot, SpellDefinition> = {
|
||||
1: { slot: 1, abilityId: 'smite', name: 'Smite', cooldown: 0, castTime: 2.5 },
|
||||
2: { slot: 2, abilityId: 'lesser_heal', name: 'Lesser Heal', cooldown: 0, castTime: 2 },
|
||||
3: { slot: 3, abilityId: 'power_word_fortitude', name: 'Power Word: Fortitude', cooldown: 0, castTime: 0 },
|
||||
4: { slot: 4, abilityId: 'shadow_word_pain', name: 'Shadow Word: Pain', cooldown: 0, castTime: 0 },
|
||||
5: { slot: 5, abilityId: 'power_word_shield', name: 'Power Word: Shield', cooldown: 6, castTime: 0 },
|
||||
6: { slot: 6, abilityId: 'renew', name: 'Renew', cooldown: 0, castTime: 0 },
|
||||
7: { slot: 7, abilityId: 'mind_blast', name: 'Mind Blast', cooldown: 8, castTime: 1.5 },
|
||||
8: { slot: 8, abilityId: 'heal', name: 'Heal', cooldown: 0, castTime: 2.5 },
|
||||
9: { slot: 9, abilityId: 'mind_flay', name: 'Mind Flay', cooldown: 0, castTime: 0 },
|
||||
10: { slot: 10, abilityId: 'flash_heal', name: 'Flash Heal', cooldown: 0, castTime: 1.5 },
|
||||
}
|
||||
|
||||
export const SPELL_MANA_COSTS: Record<SpellSlot, number> = {
|
||||
1: 70,
|
||||
2: 65,
|
||||
3: 80,
|
||||
4: 55,
|
||||
5: 100,
|
||||
6: 75,
|
||||
7: 95,
|
||||
8: 130,
|
||||
9: 45,
|
||||
10: 75,
|
||||
}
|
||||
|
||||
const ALL_SPELL_SLOTS: SpellSlot[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
export const CLAUDECRAFT_PRIEST_ACTION_BAR: SpellSlot[] = ALL_SPELL_SLOTS
|
||||
export const DUNGEON_PRIEST_SPELL_BAR: SpellSlot[] = CLAUDECRAFT_PRIEST_ACTION_BAR
|
||||
export const ARENA_PRIEST_SPELL_BAR: SpellSlot[] = CLAUDECRAFT_PRIEST_ACTION_BAR
|
||||
export const SPELL_GLOBAL_COOLDOWN_SECONDS = 1.5
|
||||
export const SPELL_RESOURCE_MAX = 900
|
||||
export const SPELL_RESOURCE_REGEN = 10
|
||||
|
||||
export const PRIEST_SPELL_ICONS: Record<SpellSlot, ClaudeCraftPriestAbilityId> = {
|
||||
1: 'smite',
|
||||
2: 'lesser_heal',
|
||||
3: 'power_word_fortitude',
|
||||
4: 'shadow_word_pain',
|
||||
5: 'power_word_shield',
|
||||
6: 'renew',
|
||||
7: 'mind_blast',
|
||||
8: 'heal',
|
||||
9: 'mind_flay',
|
||||
10: 'flash_heal',
|
||||
}
|
||||
|
||||
export const PRIEST_SPELL_RANKS: Record<SpellSlot, number> = {
|
||||
1: 4,
|
||||
2: 3,
|
||||
3: 3,
|
||||
4: 3,
|
||||
5: 3,
|
||||
6: 3,
|
||||
7: 3,
|
||||
8: 2,
|
||||
9: 1,
|
||||
10: 1,
|
||||
}
|
||||
|
||||
export const PRIEST_SPELL_SCHOOLS: Record<SpellSlot, 'Holy' | 'Shadow'> = {
|
||||
1: 'Holy',
|
||||
2: 'Holy',
|
||||
3: 'Holy',
|
||||
4: 'Shadow',
|
||||
5: 'Holy',
|
||||
6: 'Holy',
|
||||
7: 'Shadow',
|
||||
8: 'Holy',
|
||||
9: 'Shadow',
|
||||
10: 'Holy',
|
||||
}
|
||||
|
||||
export const PRIEST_SPELL_TARGETS: Record<SpellSlot, 'Enemy target' | 'Friendly target'> = {
|
||||
1: 'Enemy target',
|
||||
2: 'Friendly target',
|
||||
3: 'Friendly target',
|
||||
4: 'Enemy target',
|
||||
5: 'Friendly target',
|
||||
6: 'Friendly target',
|
||||
7: 'Enemy target',
|
||||
8: 'Friendly target',
|
||||
9: 'Enemy target',
|
||||
10: 'Friendly target',
|
||||
}
|
||||
|
||||
export const PRIEST_SPELL_DESCRIPTIONS: Record<SpellSlot, string> = {
|
||||
1: 'Smites the enemy for 64 to 78 Holy damage.',
|
||||
2: 'Heals a friendly target for 110 to 132.',
|
||||
3: 'Increases the target Stamina by 12 for 30 min.',
|
||||
4: 'A word of darkness causes 84 Shadow damage over 18 sec.',
|
||||
5: 'Shields the target, absorbing 145 damage for 30 sec.',
|
||||
6: 'Heals the target for 140 over 15 sec.',
|
||||
7: 'Blasts the target mind for 86 to 94 Shadow damage.',
|
||||
8: 'A slow but powerful prayer that heals a friendly target for 230 to 270.',
|
||||
9: 'Assaults the target mind, causing 12 Shadow damage each second for 3 sec.',
|
||||
10: 'A fast prayer that heals a friendly target for 120 to 142.',
|
||||
}
|
||||
|
||||
export const ACTION_HEALER_SPELLBOOKS: Record<ActionHealerClassId, ActionSpellbook> = {
|
||||
priest: createSpellbook('priest', 'priest', SPELLS, SPELL_MANA_COSTS, PRIEST_SPELL_RANKS, PRIEST_SPELL_SCHOOLS, PRIEST_SPELL_TARGETS, PRIEST_SPELL_DESCRIPTIONS),
|
||||
paladin: createSpellbook(
|
||||
'paladin',
|
||||
'paladin',
|
||||
{
|
||||
1: spell(1, 'judgement', 'Judgement', 8, 0),
|
||||
2: spell(2, 'holy_light', 'Holy Light', 0, 2.5),
|
||||
3: spell(3, 'flash_of_light', 'Flash of Light', 0, 1.5),
|
||||
4: spell(4, 'blessing_of_might', 'Blessing of Might', 0, 0),
|
||||
5: spell(5, 'consecration', 'Consecration', 8, 0),
|
||||
6: spell(6, 'divine_protection', 'Divine Protection', 30, 0),
|
||||
7: spell(7, 'exorcism', 'Exorcism', 15, 0),
|
||||
8: spell(8, 'lay_on_hands', 'Lay on Hands', 60, 0),
|
||||
9: spell(9, 'seal_of_righteousness', 'Seal of Righteousness', 0, 0),
|
||||
10: spell(10, 'flash_of_light', 'Flash of Light', 0, 1.5),
|
||||
},
|
||||
costs(70, 130, 75, 80, 105, 100, 95, 220, 45, 75),
|
||||
ranks(4, 3, 3, 3, 3, 2, 2, 1, 2, 3),
|
||||
schools('Holy', 'Holy', 'Holy', 'Holy', 'Holy', 'Holy', 'Holy', 'Holy', 'Holy', 'Holy'),
|
||||
targets('Enemy target', 'Friendly target', 'Friendly target', 'Friendly target', 'Enemy target', 'Friendly target', 'Enemy target', 'Friendly target', 'Enemy target', 'Friendly target'),
|
||||
descriptions(
|
||||
'Judges the enemy for Holy damage.',
|
||||
'Heals a friendly target with a large burst of Holy light.',
|
||||
'A fast, efficient Holy heal.',
|
||||
'Blesses an ally, represented here as a small protective ward.',
|
||||
'Consecrates nearby ground under the enemy for Holy damage.',
|
||||
'Wraps the target in divine protection.',
|
||||
'Strikes an enemy with Holy force.',
|
||||
'A long-cooldown emergency heal.',
|
||||
'Seals your weapon with Holy damage against the enemy.',
|
||||
'A fast, efficient Holy heal.',
|
||||
),
|
||||
),
|
||||
shaman: createSpellbook(
|
||||
'shaman',
|
||||
'shaman',
|
||||
{
|
||||
1: spell(1, 'lightning_bolt', 'Lightning Bolt', 0, 2.5),
|
||||
2: spell(2, 'healing_wave', 'Healing Wave', 0, 2.5),
|
||||
3: spell(3, 'lightning_shield', 'Lightning Shield', 10, 0),
|
||||
4: spell(4, 'earth_shock', 'Earth Shock', 6, 0),
|
||||
5: spell(5, 'flame_shock', 'Flame Shock', 6, 0),
|
||||
6: spell(6, 'frost_shock', 'Frost Shock', 6, 0),
|
||||
7: spell(7, 'healing_wave', 'Healing Wave', 0, 2.5),
|
||||
8: spell(8, 'lightning_shield', 'Lightning Shield', 10, 0),
|
||||
9: spell(9, 'lightning_bolt', 'Lightning Bolt', 0, 2.5),
|
||||
10: spell(10, 'healing_wave', 'Healing Wave', 0, 2.5),
|
||||
},
|
||||
costs(70, 125, 90, 75, 80, 80, 125, 90, 70, 125),
|
||||
ranks(4, 3, 3, 3, 3, 3, 3, 3, 4, 3),
|
||||
schools('Nature', 'Nature', 'Nature', 'Nature', 'Fire', 'Frost', 'Nature', 'Nature', 'Nature', 'Nature'),
|
||||
targets('Enemy target', 'Friendly target', 'Friendly target', 'Enemy target', 'Enemy target', 'Enemy target', 'Friendly target', 'Friendly target', 'Enemy target', 'Friendly target'),
|
||||
descriptions(
|
||||
'Hurls lightning at the enemy.',
|
||||
'Heals a friendly target with a steady wave.',
|
||||
'Surrounds an ally with a lightning shield.',
|
||||
'Shocks an enemy with earth power.',
|
||||
'Burns an enemy with flame shock damage over time.',
|
||||
'Shocks an enemy with frost.',
|
||||
'Heals a friendly target with a steady wave.',
|
||||
'Surrounds an ally with a lightning shield.',
|
||||
'Hurls lightning at the enemy.',
|
||||
'Heals a friendly target with a steady wave.',
|
||||
),
|
||||
),
|
||||
druid: createSpellbook(
|
||||
'druid',
|
||||
'druid',
|
||||
{
|
||||
1: spell(1, 'wrath', 'Wrath', 0, 2),
|
||||
2: spell(2, 'rejuvenation', 'Rejuvenation', 0, 0),
|
||||
3: spell(3, 'regrowth', 'Regrowth', 0, 2),
|
||||
4: spell(4, 'healing_touch', 'Healing Touch', 0, 2.5),
|
||||
5: spell(5, 'barkskin', 'Barkskin', 12, 0),
|
||||
6: spell(6, 'moonfire', 'Moonfire', 0, 0),
|
||||
7: spell(7, 'insect_swarm', 'Insect Swarm', 0, 0),
|
||||
8: spell(8, 'starfire', 'Starfire', 0, 3),
|
||||
9: spell(9, 'faerie_fire', 'Faerie Fire', 6, 0),
|
||||
10: spell(10, 'regrowth', 'Regrowth', 0, 2),
|
||||
},
|
||||
costs(70, 75, 105, 130, 100, 55, 65, 120, 50, 105),
|
||||
ranks(4, 3, 3, 3, 2, 3, 2, 2, 2, 3),
|
||||
schools('Nature', 'Nature', 'Nature', 'Nature', 'Nature', 'Arcane', 'Nature', 'Arcane', 'Nature', 'Nature'),
|
||||
targets('Enemy target', 'Friendly target', 'Friendly target', 'Friendly target', 'Friendly target', 'Enemy target', 'Enemy target', 'Enemy target', 'Enemy target', 'Friendly target'),
|
||||
descriptions(
|
||||
'Calls nature damage down on the enemy.',
|
||||
'Heals the target over time.',
|
||||
'Heals immediately and leaves a rejuvenating bloom.',
|
||||
'A slow, powerful nature heal.',
|
||||
'Protects the target with barkskin.',
|
||||
'Burns the enemy with arcane lunar damage over time.',
|
||||
'Sends insects to wear down the enemy.',
|
||||
'A slow, heavy arcane strike.',
|
||||
'Weakens the enemy with faerie fire.',
|
||||
'Heals immediately and leaves a rejuvenating bloom.',
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
function spell(slot: SpellSlot, abilityId: ClaudeCraftHealerAbilityId, name: string, cooldown: number, castTime: number): SpellDefinition {
|
||||
return { abilityId, castTime, cooldown, name, slot }
|
||||
}
|
||||
|
||||
function createSpellbook(
|
||||
classId: ActionHealerClassId,
|
||||
iconFolder: string,
|
||||
spells: Record<SpellSlot, SpellDefinition>,
|
||||
manaCosts: Record<SpellSlot, number>,
|
||||
ranks: Record<SpellSlot, number>,
|
||||
schools: Record<SpellSlot, ActionSpellSchool>,
|
||||
targets: Record<SpellSlot, ActionSpellTarget>,
|
||||
descriptions: Record<SpellSlot, string>,
|
||||
): ActionSpellbook {
|
||||
return {
|
||||
bar: ALL_SPELL_SLOTS,
|
||||
classId,
|
||||
descriptions,
|
||||
iconFolder,
|
||||
manaCosts,
|
||||
ranks,
|
||||
schools,
|
||||
spells,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
|
||||
function costs(
|
||||
one: number,
|
||||
two: number,
|
||||
three: number,
|
||||
four: number,
|
||||
five: number,
|
||||
six: number,
|
||||
seven: number,
|
||||
eight: number,
|
||||
nine: number,
|
||||
ten: number,
|
||||
): Record<SpellSlot, number> {
|
||||
return { 1: one, 2: two, 3: three, 4: four, 5: five, 6: six, 7: seven, 8: eight, 9: nine, 10: ten }
|
||||
}
|
||||
|
||||
function ranks(
|
||||
one: number,
|
||||
two: number,
|
||||
three: number,
|
||||
four: number,
|
||||
five: number,
|
||||
six: number,
|
||||
seven: number,
|
||||
eight: number,
|
||||
nine: number,
|
||||
ten: number,
|
||||
): Record<SpellSlot, number> {
|
||||
return { 1: one, 2: two, 3: three, 4: four, 5: five, 6: six, 7: seven, 8: eight, 9: nine, 10: ten }
|
||||
}
|
||||
|
||||
function schools(
|
||||
one: ActionSpellSchool,
|
||||
two: ActionSpellSchool,
|
||||
three: ActionSpellSchool,
|
||||
four: ActionSpellSchool,
|
||||
five: ActionSpellSchool,
|
||||
six: ActionSpellSchool,
|
||||
seven: ActionSpellSchool,
|
||||
eight: ActionSpellSchool,
|
||||
nine: ActionSpellSchool,
|
||||
ten: ActionSpellSchool,
|
||||
): Record<SpellSlot, ActionSpellSchool> {
|
||||
return { 1: one, 2: two, 3: three, 4: four, 5: five, 6: six, 7: seven, 8: eight, 9: nine, 10: ten }
|
||||
}
|
||||
|
||||
function targets(
|
||||
one: ActionSpellTarget,
|
||||
two: ActionSpellTarget,
|
||||
three: ActionSpellTarget,
|
||||
four: ActionSpellTarget,
|
||||
five: ActionSpellTarget,
|
||||
six: ActionSpellTarget,
|
||||
seven: ActionSpellTarget,
|
||||
eight: ActionSpellTarget,
|
||||
nine: ActionSpellTarget,
|
||||
ten: ActionSpellTarget,
|
||||
): Record<SpellSlot, ActionSpellTarget> {
|
||||
return { 1: one, 2: two, 3: three, 4: four, 5: five, 6: six, 7: seven, 8: eight, 9: nine, 10: ten }
|
||||
}
|
||||
|
||||
function descriptions(
|
||||
one: string,
|
||||
two: string,
|
||||
three: string,
|
||||
four: string,
|
||||
five: string,
|
||||
six: string,
|
||||
seven: string,
|
||||
eight: string,
|
||||
nine: string,
|
||||
ten: string,
|
||||
): Record<SpellSlot, string> {
|
||||
return { 1: one, 2: two, 3: three, 4: four, 5: five, 6: six, 7: seven, 8: eight, 9: nine, 10: ten }
|
||||
}
|
||||
|
||||
function normalizeHealerClassId(classId: string | undefined): ActionHealerClassId {
|
||||
if (classId === 'paladin' || classId === 'shaman' || classId === 'druid') return classId
|
||||
return 'priest'
|
||||
}
|
||||
|
||||
export function createSpellCooldowns(): Record<SpellSlot, number> {
|
||||
return {
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
10: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export function createSpellResourceState(classId?: string): SpellResourceState {
|
||||
const vitals = classId ? getArenaClassVitals(classId as ArenaClassId) : null
|
||||
const maxMana = vitals?.maxMana ?? SPELL_RESOURCE_MAX
|
||||
return {
|
||||
mana: maxMana,
|
||||
maxMana,
|
||||
storedMomentumCasts: 0,
|
||||
storedMomentumReady: false,
|
||||
}
|
||||
}
|
||||
|
||||
export function tickSpellCooldowns(cooldowns: Record<SpellSlot, number>, deltaSeconds: number) {
|
||||
for (const slot of ALL_SPELL_SLOTS) {
|
||||
cooldowns[slot] = Math.max(0, cooldowns[slot] - deltaSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
export function tickSpellResource(resource: SpellResourceState, deltaSeconds: number) {
|
||||
resource.mana = Math.min(resource.maxMana, resource.mana + SPELL_RESOURCE_REGEN * deltaSeconds)
|
||||
}
|
||||
|
||||
export function getSpellManaCost(spell: SpellSlot, options?: { costMultiplier?: number, freeCast?: boolean }) {
|
||||
if (options?.freeCast) return 0
|
||||
return Math.max(1, Math.round(SPELL_MANA_COSTS[spell] * (options?.costMultiplier ?? 1)))
|
||||
}
|
||||
|
||||
export function getActionSpellbook(classId: string | undefined): ActionSpellbook {
|
||||
return ACTION_HEALER_SPELLBOOKS[normalizeHealerClassId(classId)]
|
||||
}
|
||||
|
||||
export function getActionSpellDefinition(classId: string | undefined, spell: SpellSlot, talentMods?: TalentModifiers) {
|
||||
const definition = getActionSpellbook(classId).spells[spell]
|
||||
const modifier = getActionTalentAbilityModifier(talentMods, definition.abilityId)
|
||||
return {
|
||||
...definition,
|
||||
castTime: Math.max(0, roundTenths(definition.castTime * (1 + modifier.castPct))),
|
||||
cooldown: Math.max(0, roundTenths(definition.cooldown * (1 + modifier.cooldownPct))),
|
||||
}
|
||||
}
|
||||
|
||||
export function getActionSpellRange(classId: string | undefined, spell: SpellSlot, talentMods?: TalentModifiers) {
|
||||
return getArenaAbility(getActionSpellDefinition(classId, spell, talentMods).abilityId).range
|
||||
}
|
||||
|
||||
export function getActionSpellReach(options: {
|
||||
caster: ActionRangePoint
|
||||
target: ActionRangePoint
|
||||
classId: string | undefined
|
||||
spell: SpellSlot
|
||||
talentMods?: TalentModifiers
|
||||
includeTargetRadius?: boolean
|
||||
hasLineOfSight?: (from: ActionRangePoint, to: ActionRangePoint) => boolean
|
||||
}): ActionSpellReachResult {
|
||||
const baseRange = getActionSpellRange(options.classId, options.spell, options.talentMods)
|
||||
const effectiveRange = baseRange + (options.includeTargetRadius ? options.target.radius ?? 0 : 0)
|
||||
const distance = Math.hypot(options.caster.x - options.target.x, options.caster.y - options.target.y)
|
||||
if (distance > effectiveRange) return { ok: false, reason: 'range', distance, effectiveRange }
|
||||
if (options.hasLineOfSight && !options.hasLineOfSight(options.caster, options.target)) {
|
||||
return { ok: false, reason: 'lineOfSight', distance, effectiveRange }
|
||||
}
|
||||
return { ok: true, distance, effectiveRange }
|
||||
}
|
||||
|
||||
export function getActionSpellManaCost(classId: string | undefined, spell: SpellSlot, options?: { costMultiplier?: number, freeCast?: boolean, talentMods?: TalentModifiers }) {
|
||||
if (options?.freeCast) return 0
|
||||
const definition = getActionSpellbook(classId).spells[spell]
|
||||
const modifier = getActionTalentAbilityModifier(options?.talentMods, definition.abilityId)
|
||||
return Math.max(1, Math.round(getActionSpellbook(classId).manaCosts[spell] * (1 + modifier.costPct) * (options?.costMultiplier ?? 1)))
|
||||
}
|
||||
|
||||
export function getActionTalentAdjustedAmount(classId: string | undefined, spell: SpellSlot, baseAmount: number, kind: 'damage' | 'heal' | 'shield', talentMods?: TalentModifiers) {
|
||||
const definition = getActionSpellbook(classId).spells[spell]
|
||||
const abilityModifier = getActionTalentAbilityModifier(talentMods, definition.abilityId)
|
||||
const globalMultiplier = kind === 'heal'
|
||||
? talentMods?.global.healPct ?? 0
|
||||
: kind === 'damage'
|
||||
? talentMods?.global.spellDmgPct ?? 0
|
||||
: 0
|
||||
return Math.max(0, Math.round((baseAmount + abilityModifier.flatDmg) * (1 + abilityModifier.dmgPct + globalMultiplier)))
|
||||
}
|
||||
|
||||
export function getActionSpellTarget(classId: string | undefined, spell: SpellSlot) {
|
||||
return getActionSpellbook(classId).targets[spell]
|
||||
}
|
||||
|
||||
export function getActionSpellIconUrl(classId: string | undefined, spell: SpellSlot) {
|
||||
const book = getActionSpellbook(classId)
|
||||
return `/ui/skills/${book.iconFolder}/${book.spells[spell].abilityId}.png`
|
||||
}
|
||||
|
||||
export function getActionSpellTooltip(classId: string | undefined, spell: SpellSlot) {
|
||||
const book = getActionSpellbook(classId)
|
||||
return {
|
||||
description: book.descriptions[spell],
|
||||
rank: book.ranks[spell],
|
||||
school: book.schools[spell],
|
||||
target: book.targets[spell],
|
||||
}
|
||||
}
|
||||
|
||||
export function getDungeonSpellManaCost(spell: SpellSlot, options?: { freeCast?: boolean }) {
|
||||
if (options?.freeCast) return 0
|
||||
return getSpellManaCost(spell, options)
|
||||
}
|
||||
|
||||
export function spendSpellMana(resource: SpellResourceState, spell: SpellSlot, options?: { costMultiplier?: number }) {
|
||||
const cost = getSpellManaCost(spell, {
|
||||
costMultiplier: options?.costMultiplier,
|
||||
freeCast: resource.storedMomentumReady,
|
||||
})
|
||||
if (!resource.storedMomentumReady && resource.mana < cost) {
|
||||
return { ok: false, cost }
|
||||
}
|
||||
|
||||
if (resource.storedMomentumReady) resource.storedMomentumReady = false
|
||||
else resource.mana = Math.max(0, resource.mana - cost)
|
||||
return { ok: true, cost }
|
||||
}
|
||||
|
||||
export function spendActionSpellMana(resource: SpellResourceState, classId: string | undefined, spell: SpellSlot, options?: { costMultiplier?: number, talentMods?: TalentModifiers }) {
|
||||
const cost = getActionSpellManaCost(classId, spell, {
|
||||
costMultiplier: options?.costMultiplier,
|
||||
freeCast: resource.storedMomentumReady,
|
||||
talentMods: options?.talentMods,
|
||||
})
|
||||
if (!resource.storedMomentumReady && resource.mana < cost) {
|
||||
return { ok: false, cost }
|
||||
}
|
||||
|
||||
if (resource.storedMomentumReady) resource.storedMomentumReady = false
|
||||
else resource.mana = Math.max(0, resource.mana - cost)
|
||||
return { ok: true, cost }
|
||||
}
|
||||
|
||||
function getActionTalentAbilityModifier(talentMods: TalentModifiers | undefined, abilityId: ClaudeCraftHealerAbilityId) {
|
||||
return talentMods?.abilities[abilityId] ?? getEmptyClaudeCraftAbilityModifier()
|
||||
}
|
||||
|
||||
function roundTenths(value: number) {
|
||||
return Math.round(value * 10) / 10
|
||||
}
|
||||
|
||||
export function spendDungeonSpellMana(resource: SpellResourceState, spell: SpellSlot) {
|
||||
const cost = getDungeonSpellManaCost(spell, { freeCast: resource.storedMomentumReady })
|
||||
if (!resource.storedMomentumReady && resource.mana < cost) {
|
||||
return { ok: false, cost }
|
||||
}
|
||||
|
||||
if (resource.storedMomentumReady) resource.storedMomentumReady = false
|
||||
else resource.mana = Math.max(0, resource.mana - cost)
|
||||
return { ok: true, cost }
|
||||
}
|
||||
|
||||
export function recordSpellCast(resource: SpellResourceState, options?: { storedMomentum?: boolean }) {
|
||||
if (!options?.storedMomentum) return
|
||||
resource.storedMomentumCasts += 1
|
||||
if (resource.storedMomentumCasts >= 5) {
|
||||
resource.storedMomentumCasts = 0
|
||||
resource.storedMomentumReady = true
|
||||
}
|
||||
}
|
||||
|
||||
export function applyShieldedDamage(target: ShieldedHealthState, amount: number) {
|
||||
if ((target.invulnerableTimer ?? 0) > 0) return 0
|
||||
const absorbed = Math.min(target.shield, amount)
|
||||
target.shield -= absorbed
|
||||
const damage = amount - absorbed
|
||||
target.hp = Math.max(0, target.hp - damage)
|
||||
return damage
|
||||
}
|
||||
|
||||
export function addShield(target: ShieldedHealthState, amount: number, options?: { cap?: number, capRatio?: number }) {
|
||||
const cap = options?.cap ?? target.maxHp * (options?.capRatio ?? HEALING_SPELL_RULES.arenaShieldCapRatio)
|
||||
target.shield = Math.min(cap, target.shield + amount)
|
||||
}
|
||||
|
||||
export function dampenHealing(amount: number, dampeningPercent = 0) {
|
||||
return amount * Math.max(0, 1 - dampeningPercent / 100)
|
||||
}
|
||||
|
||||
export function applyRenewTimers(target: RenewState, durationSeconds: number = HEALING_SPELL_RULES.renewSeconds) {
|
||||
applyHealOverTimeTimers(target, 'renew', { durationSeconds })
|
||||
}
|
||||
|
||||
export function applyHealOverTimeTimers(
|
||||
target: RenewState,
|
||||
id: HealOverTimeEffectId,
|
||||
options: {
|
||||
durationSeconds?: number
|
||||
tickHeal?: number
|
||||
} = {},
|
||||
) {
|
||||
const durationSeconds = options.durationSeconds ?? HEALING_SPELL_RULES.renewSeconds
|
||||
const tickSeconds = HEALING_SPELL_RULES.renewTickSeconds
|
||||
const healOverTimes = target.healOverTimes ?? []
|
||||
const existing = healOverTimes.find((effect) => effect.id === id)
|
||||
if (existing) {
|
||||
existing.remaining = Math.max(existing.remaining, durationSeconds)
|
||||
existing.tickHeal = options.tickHeal ?? existing.tickHeal
|
||||
existing.tickTimer = tickSeconds
|
||||
} else {
|
||||
healOverTimes.push({
|
||||
id,
|
||||
label: HEAL_OVER_TIME_LABELS[id],
|
||||
remaining: durationSeconds,
|
||||
tickHeal: options.tickHeal ?? HEALING_SPELL_RULES.renewHeal,
|
||||
tickSeconds,
|
||||
tickTimer: tickSeconds,
|
||||
})
|
||||
}
|
||||
target.healOverTimes = healOverTimes
|
||||
syncRenewCompatibilityTimers(target)
|
||||
}
|
||||
|
||||
export function tickHealOverTimes(target: RenewState, deltaSeconds: number, onTick: (effect: HealOverTimeEffect) => void) {
|
||||
const healOverTimes = target.healOverTimes
|
||||
if (!healOverTimes || healOverTimes.length === 0) {
|
||||
tickLegacyRenew(target, deltaSeconds, onTick)
|
||||
return
|
||||
}
|
||||
|
||||
for (const effect of healOverTimes) {
|
||||
effect.remaining = Math.max(0, effect.remaining - deltaSeconds)
|
||||
effect.tickTimer -= deltaSeconds
|
||||
while (effect.remaining > 0 && effect.tickTimer <= 0) {
|
||||
onTick(effect)
|
||||
effect.tickTimer += effect.tickSeconds
|
||||
}
|
||||
}
|
||||
target.healOverTimes = healOverTimes.filter((effect) => effect.remaining > 0)
|
||||
syncRenewCompatibilityTimers(target)
|
||||
}
|
||||
|
||||
export function getActiveHealOverTimeFrames(target: RenewState): HealOverTimeFrame[] {
|
||||
const healOverTimes = target.healOverTimes?.filter((effect) => effect.remaining > 0) ?? []
|
||||
if (healOverTimes.length > 0) {
|
||||
return healOverTimes.map(({ id, label, remaining }) => ({ id, label, remaining }))
|
||||
}
|
||||
return target.renewTimer > 0 ? [{ id: 'renew', label: HEAL_OVER_TIME_LABELS.renew, remaining: target.renewTimer }] : []
|
||||
}
|
||||
|
||||
export function getHealOverTimeEffectId(abilityId: string): HealOverTimeEffectId | null {
|
||||
if (abilityId === 'renew' || abilityId === 'rejuvenation' || abilityId === 'regrowth') return abilityId
|
||||
return null
|
||||
}
|
||||
|
||||
function tickLegacyRenew(target: RenewState, deltaSeconds: number, onTick: (effect: HealOverTimeEffect) => void) {
|
||||
if (target.renewTimer <= 0) return
|
||||
target.renewTimer = Math.max(0, target.renewTimer - deltaSeconds)
|
||||
target.renewTickTimer -= deltaSeconds
|
||||
if (target.renewTickTimer <= 0) {
|
||||
onTick({
|
||||
id: 'renew',
|
||||
label: HEAL_OVER_TIME_LABELS.renew,
|
||||
remaining: target.renewTimer,
|
||||
tickHeal: HEALING_SPELL_RULES.renewHeal,
|
||||
tickSeconds: HEALING_SPELL_RULES.renewTickSeconds,
|
||||
tickTimer: target.renewTickTimer,
|
||||
})
|
||||
target.renewTickTimer += HEALING_SPELL_RULES.renewTickSeconds
|
||||
}
|
||||
}
|
||||
|
||||
function syncRenewCompatibilityTimers(target: RenewState) {
|
||||
const renew = target.healOverTimes?.find((effect) => effect.id === 'renew')
|
||||
target.renewTimer = renew?.remaining ?? 0
|
||||
target.renewTickTimer = renew?.tickTimer ?? 0
|
||||
}
|
||||
|
||||
const HEAL_OVER_TIME_LABELS: Record<HealOverTimeEffectId, string> = {
|
||||
regrowth: 'Regrowth',
|
||||
rejuvenation: 'Rejuvenation',
|
||||
renew: 'Renew',
|
||||
}
|
||||
Reference in New Issue
Block a user