365 lines
9.7 KiB
TypeScript
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
|
|
}
|