Files
i-want-to-heal-2/src/actionArena.ts
T

365 lines
9.7 KiB
TypeScript

import {
ARENA_PRIEST_SPELL_BAR,
SPELLS,
type SpellSlot,
} from './actionBoss/actionCombatSimulation'
import { SPELL_MANA_COSTS } from './actionCombatCore'
export type ArenaTeamId = 'player' | 'opponent'
export type ArenaPhase = 'setup' | 'fighting' | 'upgrade' | 'complete'
export type ArenaUpgradeId =
| 'all-target'
| 'all-cost'
| 'all-cooldown'
| `slot-${SpellSlot}-cost`
| `slot-${SpellSlot}-cooldown`
| 'slot-1-renew'
| 'slot-1-shield'
| 'slot-2-shield'
| 'slot-2-duration'
| 'slot-3-half-shield'
| 'slot-3-renew'
| 'slot-4-renew'
| 'slot-5-renew'
| 'slot-5-shield'
| 'stored-momentum'
| 'wide-radiance'
| 'dense-shields'
| 'atonement'
export type ArenaUpgrade = {
id: ArenaUpgradeId
label: string
scope: string
description: string
cost: number
}
export type ArenaMatchState = {
phase: ArenaPhase
round: number
playerName: string
opponentName: string
playerWins: number
opponentWins: number
playerCredits: number
opponentCredits: number
playerUpgrades: ArenaUpgradeId[]
opponentUpgrades: ArenaUpgradeId[]
history: string[]
}
const FALLBACK_SPELL_NAMES: Partial<Record<SpellSlot, string>> = {
1: 'Smite',
2: 'Power Word: Fortitude',
3: 'Shadow Word: Pain',
4: 'Power Word: Shield',
5: 'Renew',
6: 'Mind Blast',
7: 'Heal',
8: 'Mind Flay',
9: 'Flash Heal',
}
function getSlotCostUpgradeId(slot: SpellSlot): ArenaUpgradeId {
return `slot-${slot}-cost`
}
function getSlotCooldownUpgradeId(slot: SpellSlot): ArenaUpgradeId {
return `slot-${slot}-cooldown`
}
function createSlotEconomyUpgrades(slot: SpellSlot): ArenaUpgrade[] {
const name = getArenaSpellName(slot)
return [
{
id: getSlotCostUpgradeId(slot),
label: `${name}: -25% Mana Cost`,
scope: `Slot ${slot}`,
description: `${name} costs 25% less mana.`,
cost: 1,
},
{
id: getSlotCooldownUpgradeId(slot),
label: `${name}: -25% Cooldown`,
scope: `Slot ${slot}`,
description: `${name} cooldown recovers 25% faster.`,
cost: 1,
},
]
}
const ARENA_UPGRADES: ArenaUpgrade[] = [
{
id: 'all-target',
label: 'Every Slot: +1 Target',
scope: 'Every Slot',
description: 'Friendly ClaudeCraft priest spells reach one extra ally.',
cost: 2,
},
{
id: 'all-cost',
label: 'Every Slot: -25% Cost',
scope: 'Every Slot',
description: 'All spell energy costs are reduced by 25%.',
cost: 1,
},
{
id: 'all-cooldown',
label: 'Every Slot: -25% Cooldown',
scope: 'Every Slot',
description: 'All spell cooldowns recover faster.',
cost: 1,
},
{
id: 'slot-1-renew',
label: `${getArenaSpellName(1)} Applies Renew`,
scope: 'Slot 1',
description: 'Slot 1 also applies Renew to its targets.',
cost: 2,
},
{
id: 'slot-1-shield',
label: `${getArenaSpellName(1)} Applies Shield`,
scope: 'Slot 1',
description: 'Slot 1 also grants a shield.',
cost: 2,
},
...createSlotEconomyUpgrades(1),
{
id: 'slot-2-shield',
label: `${getArenaSpellName(2)} Applies Shield`,
scope: 'Slot 2',
description: 'Slot 2 also grants a shield.',
cost: 2,
},
{
id: 'slot-2-duration',
label: `${getArenaSpellName(2)} Double Duration`,
scope: 'Slot 2',
description: 'Slot 2 Renew lasts twice as long.',
cost: 2,
},
...createSlotEconomyUpgrades(2),
{
id: 'slot-3-half-shield',
label: `${getArenaSpellName(3)} Applies 50% Shield`,
scope: 'Slot 3',
description: 'Slot 3 also grants a half-strength shield.',
cost: 2,
},
{
id: 'slot-3-renew',
label: `${getArenaSpellName(3)} Applies Renew`,
scope: 'Slot 3',
description: 'Slot 3 also applies Renew to healed targets.',
cost: 2,
},
...createSlotEconomyUpgrades(3),
{
id: 'slot-4-renew',
label: `${getArenaSpellName(4)} Applies Renew`,
scope: 'Slot 4',
description: 'Slot 4 also applies Renew.',
cost: 2,
},
...createSlotEconomyUpgrades(4),
{
id: 'slot-5-renew',
label: `${getArenaSpellName(5)} Applies Renew`,
scope: 'Slot 5',
description: 'Slot 5 also applies Renew.',
cost: 2,
},
{
id: 'slot-5-shield',
label: `${getArenaSpellName(5)} Applies Shield`,
scope: 'Slot 5',
description: 'Slot 5 also grants a shield.',
cost: 2,
},
...createSlotEconomyUpgrades(5),
...createSlotEconomyUpgrades(6),
...createSlotEconomyUpgrades(7),
...createSlotEconomyUpgrades(8),
...createSlotEconomyUpgrades(9),
...createSlotEconomyUpgrades(10),
{
id: 'stored-momentum',
label: 'Stored Momentum',
scope: 'Misc',
description: 'After 5 spell casts, the next cast is free.',
cost: 1,
},
{
id: 'atonement',
label: 'Atonement',
scope: 'Priest',
description: 'Smite and Shadow Word: Pain heal the lowest-health ally for 100% of damage dealt.',
cost: 3,
},
{
id: 'wide-radiance',
label: 'Stronger Heal',
scope: 'Misc',
description: 'Heal is 25% stronger.',
cost: 1,
},
{
id: 'dense-shields',
label: 'Dense Shields',
scope: 'Misc',
description: 'Shields absorb 25% more damage.',
cost: 1,
},
]
export function getArenaUpgrades() {
return ARENA_UPGRADES
}
export function getArenaSpellName(slot: SpellSlot) {
return SPELLS[slot]?.name || FALLBACK_SPELL_NAMES[slot] || `Slot ${slot}`
}
export function getArenaSpellCostMultiplier(upgrades: ArenaUpgradeId[], spell: SpellSlot) {
let multiplier = upgrades.includes('all-cost') ? 0.75 : 1
if (upgrades.includes(getSlotCostUpgradeId(spell))) multiplier *= 0.75
return multiplier
}
export function getArenaSpellCooldownMultiplier(upgrades: ArenaUpgradeId[], spell: SpellSlot) {
let multiplier = upgrades.includes('all-cooldown') ? 0.75 : 1
if (upgrades.includes(getSlotCooldownUpgradeId(spell))) multiplier *= 0.75
return multiplier
}
export function getArenaSpellLoadout() {
return ARENA_PRIEST_SPELL_BAR.map((slot) => ({
slot,
name: getArenaSpellName(slot),
cost: SPELL_MANA_COSTS[slot],
cooldown: SPELLS[slot]?.cooldown ?? 0,
}))
}
export function createArenaMatchState(playerName = 'Action Healer'): ArenaMatchState {
return {
phase: 'setup',
round: 1,
playerName,
opponentName: 'Rival Five',
playerWins: 0,
opponentWins: 0,
playerCredits: 0,
opponentCredits: 0,
playerUpgrades: [],
opponentUpgrades: [],
history: [],
}
}
export function resolveArenaRoundWinner(
match: ArenaMatchState,
winner: 'player' | 'opponent',
elapsedSeconds: number,
): ArenaMatchState {
const playerWon = winner === 'player'
const playerWins = match.playerWins + (playerWon ? 1 : 0)
const opponentWins = match.opponentWins + (playerWon ? 0 : 1)
const complete = playerWins >= 3 || opponentWins >= 3
const winnerName = playerWon ? match.playerName : match.opponentName
const playerPointAward = playerWon ? 3 : 4
const opponentPointAward = playerWon ? 4 : 3
const opponentDraft = complete
? { upgrades: match.opponentUpgrades, credits: match.opponentCredits + opponentPointAward, picked: null }
: pickOpponentUpgrade(match.opponentUpgrades, match.opponentCredits + opponentPointAward)
return {
...match,
phase: complete ? 'complete' : 'upgrade',
round: complete ? match.round : match.round + 1,
playerWins,
opponentWins,
playerCredits: match.playerCredits + playerPointAward,
opponentCredits: opponentDraft.credits,
opponentUpgrades: opponentDraft.upgrades,
history: [
`${winnerName} won round ${match.round} in ${elapsedSeconds.toFixed(1)}s.`,
...(opponentDraft.picked ? [`${match.opponentName} drafted ${getUpgradeLabel(opponentDraft.picked)}.`] : []),
...match.history,
].slice(0, 6),
}
}
export function buyArenaUpgrade(match: ArenaMatchState, upgradeId: ArenaUpgradeId): ArenaMatchState {
const upgrade = ARENA_UPGRADES.find((item) => item.id === upgradeId)
if (!upgrade) return match
if (match.playerUpgrades.includes(upgradeId)) {
return {
...match,
playerCredits: match.playerCredits + upgrade.cost,
playerUpgrades: match.playerUpgrades.filter((id) => id !== upgradeId),
history: [`${match.playerName} removed ${upgrade.label}.`, ...match.history].slice(0, 6),
}
}
if (match.playerCredits < upgrade.cost) return match
return {
...match,
playerCredits: match.playerCredits - upgrade.cost,
playerUpgrades: [...match.playerUpgrades, upgradeId],
history: [`${match.playerName} drafted ${upgrade.label}.`, ...match.history].slice(0, 6),
}
}
export function startNextArenaRound(match: ArenaMatchState): ArenaMatchState {
return {
...match,
phase: 'fighting',
}
}
export function getArenaAvailableUpgrades(match: ArenaMatchState) {
return ARENA_UPGRADES.map((upgrade) => ({
...upgrade,
owned: match.playerUpgrades.includes(upgrade.id),
affordable: match.playerCredits >= upgrade.cost,
}))
}
function pickOpponentUpgrade(upgrades: ArenaUpgradeId[], credits: number) {
const priority: ArenaUpgradeId[] = [
'all-cooldown',
'wide-radiance',
'all-cost',
'slot-1-cost',
'slot-1-cooldown',
'slot-3-cost',
'slot-3-cooldown',
'slot-4-renew',
'dense-shields',
'all-target',
'slot-3-half-shield',
'stored-momentum',
]
const picked = priority.find((id) => {
const upgrade = ARENA_UPGRADES.find((item) => item.id === id)
return upgrade && !upgrades.includes(id) && credits >= upgrade.cost
}) ?? null
if (!picked) return { upgrades, credits, picked }
const cost = ARENA_UPGRADES.find((upgrade) => upgrade.id === picked)?.cost ?? 0
return {
upgrades: [...upgrades, picked],
credits: credits - cost,
picked,
}
}
function getUpgradeLabel(id: ArenaUpgradeId) {
return ARENA_UPGRADES.find((upgrade) => upgrade.id === id)?.label ?? id
}