Android build v1.1.23
This commit is contained in:
@@ -17,6 +17,7 @@ import { castIwt2HealerAbility } from '../sim'
|
||||
import { PhaserArena } from '../render/PhaserArena'
|
||||
import {
|
||||
recordIwt2BossKillReward,
|
||||
type Iwt2BossDropAward,
|
||||
type Iwt2BossPetAward,
|
||||
type Iwt2Save,
|
||||
} from '../save/iwt2Repository'
|
||||
@@ -25,7 +26,8 @@ import { BossHud } from '../components/BossHud'
|
||||
import { PartyFrames } from '../components/PartyFrames'
|
||||
import { IWT2_CLASS_METADATA } from '../content/classes'
|
||||
import { IWT2_ABILITY_ACTIONS, IWT2_TARGET_ACTIONS } from '../content/controls'
|
||||
import { abilitiesForHealer, type Iwt2HealerAbility } from '../content/healerAbilities'
|
||||
import type { Iwt2Difficulty } from '../content/difficulties'
|
||||
import { abilitiesForHealer, type Iwt2HealerAbility, type Iwt2TriggeredAbilityEffect } from '../content/healerAbilities'
|
||||
import { IWT2_BOSS_METADATA, type Iwt2BossId } from '../content/bosses'
|
||||
import type {
|
||||
Iwt2RoguelikeContentType,
|
||||
@@ -33,25 +35,44 @@ import type {
|
||||
Iwt2RoguelikeSelfBuffId,
|
||||
Iwt2RoguelikeVariant,
|
||||
} from '../content/roguelike'
|
||||
import {
|
||||
createRoguelikePressureState,
|
||||
roguelikeIncomingDamageScale,
|
||||
} from '../sim/roguelikePressure'
|
||||
import { applyIwt2PveGearStats, applyIwt2PveGearToHealerAbilities } from '../sim/equipmentStats'
|
||||
import {
|
||||
IWT2_AEGIS_SCRIPT_DAMAGE_REDUCTION_BUFF_ID,
|
||||
IWT2_BARKSKIN_HOT_BONUS_BUFF_ID,
|
||||
IWT2_SUN_WARD_DAMAGE_REDUCTION_BUFF_ID,
|
||||
} from '../content/roguelike'
|
||||
|
||||
type ArenaStatus = 'playing' | 'paused' | 'victory' | 'defeat'
|
||||
type OverlayAction = 'primary' | 'menu'
|
||||
type OverlayAction = 'primary' | 'requeue' | 'menu'
|
||||
type OverlayNavEntry = {
|
||||
action: OverlayAction
|
||||
row: number
|
||||
}
|
||||
|
||||
const OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
|
||||
const DEFAULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
|
||||
{ action: 'primary', row: 0 },
|
||||
{ action: 'menu', row: 1 },
|
||||
]
|
||||
const PVP_RESULT_OVERLAY_NAV_ENTRIES: OverlayNavEntry[] = [
|
||||
{ action: 'primary', row: 0 },
|
||||
{ action: 'requeue', row: 1 },
|
||||
{ action: 'menu', row: 2 },
|
||||
]
|
||||
const EMPTY_ROGUELIKE_BUFFS: Iwt2RoguelikeSelfBuffId[] = []
|
||||
const IWT2_PVP_BOSS_HEALTH_MULTIPLIER = 0.7
|
||||
|
||||
type BossArenaScreenProps = {
|
||||
bossId: Iwt2BossId
|
||||
bossIds?: Iwt2BossId[]
|
||||
difficulty?: Iwt2Difficulty
|
||||
modeLabel?: string
|
||||
save: Iwt2Save
|
||||
onBack: () => void
|
||||
onPvpRequeue?: () => void
|
||||
onSaveUpdated: (save: Iwt2Save) => void
|
||||
roguelikeRun?: {
|
||||
buffs: Iwt2RoguelikeSelfBuffId[]
|
||||
@@ -63,18 +84,49 @@ type BossArenaScreenProps = {
|
||||
}
|
||||
}
|
||||
|
||||
export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
||||
export function BossArenaScreen({ bossId, bossIds, difficulty, modeLabel, save, onBack, onPvpRequeue, onSaveUpdated, roguelikeRun }: BossArenaScreenProps) {
|
||||
const bossMetadata = IWT2_BOSS_METADATA[bossId]
|
||||
const pvpRoguelike = roguelikeRun?.variant === 'pvp'
|
||||
const pveGearActive = roguelikeRun?.variant !== 'pvp'
|
||||
const bossHealthScale = roguelikeRun ? roguelikeBossHealthScale(roguelikeRun.stage) : 1
|
||||
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale))
|
||||
const difficultyHealthScale = difficulty?.healthMultiplier ?? 1
|
||||
const difficultyDamageScale = difficulty?.damageMultiplier ?? 1
|
||||
const roguelikeStage = roguelikeRun?.stage
|
||||
const roguelikeContentType = roguelikeRun?.contentType
|
||||
const roguelikeBuffs = roguelikeRun?.buffs ?? EMPTY_ROGUELIKE_BUFFS
|
||||
const roguelikeDamageScale = roguelikeRun
|
||||
? roguelikeIncomingDamageScale(roguelikeRun.stage, roguelikeRun.contentType)
|
||||
: 1
|
||||
const experienceMultiplier = difficulty?.experienceMultiplier ?? 1
|
||||
const difficultySlug = difficulty?.slug ?? 'initiate'
|
||||
const pvpBossHealthScale = pvpRoguelike ? IWT2_PVP_BOSS_HEALTH_MULTIPLIER : 1
|
||||
const combinedBossHealthScale = bossHealthScale * difficultyHealthScale * pvpBossHealthScale
|
||||
const combinedDamageScale = difficultyDamageScale * roguelikeDamageScale
|
||||
const [arenaState, setArenaState] = useState<Iwt2ArenaState>(() => createArenaState(
|
||||
bossId,
|
||||
bossIds,
|
||||
combinedBossHealthScale,
|
||||
combinedDamageScale,
|
||||
roguelikeBuffs,
|
||||
createPressureState(roguelikeStage, roguelikeContentType),
|
||||
pveGearActive ? save.gearProgress : undefined,
|
||||
))
|
||||
const [opponentArenaState, setOpponentArenaState] = useState<Iwt2ArenaState | null>(() => (
|
||||
pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale) : null
|
||||
pvpRoguelike
|
||||
? createInitialIwt2ArenaState(
|
||||
bossId,
|
||||
bossIds,
|
||||
combinedBossHealthScale,
|
||||
combinedDamageScale,
|
||||
createPressureState(roguelikeStage, roguelikeContentType),
|
||||
)
|
||||
: null
|
||||
))
|
||||
const [abilityCooldowns, setAbilityCooldowns] = useState<Record<string, number>>({})
|
||||
const [status, setStatus] = useState<ArenaStatus>('playing')
|
||||
const [selectedOverlayAction, setSelectedOverlayAction] = useState<OverlayAction>('primary')
|
||||
const [selectedPartyId, setSelectedPartyId] = useState<Iwt2EntityId>('player-healer')
|
||||
const [dropAwards, setDropAwards] = useState<Iwt2BossDropAward[]>([])
|
||||
const [petAwards, setPetAwards] = useState<Iwt2BossPetAward[]>([])
|
||||
const stateRef = useRef(arenaState)
|
||||
const opponentStateRef = useRef<Iwt2ArenaState | null>(opponentArenaState)
|
||||
@@ -121,8 +173,25 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
}, [save])
|
||||
|
||||
const resetArena = useCallback(() => {
|
||||
const next = createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale)
|
||||
const nextOpponentState = pvpRoguelike ? createInitialIwt2ArenaState(bossId, bossIds, bossHealthScale) : null
|
||||
const pressureState = createPressureState(roguelikeStage, roguelikeContentType)
|
||||
const next = createArenaState(
|
||||
bossId,
|
||||
bossIds,
|
||||
combinedBossHealthScale,
|
||||
combinedDamageScale,
|
||||
roguelikeBuffs,
|
||||
pressureState,
|
||||
pveGearActive ? save.gearProgress : undefined,
|
||||
)
|
||||
const nextOpponentState = pvpRoguelike
|
||||
? createInitialIwt2ArenaState(
|
||||
bossId,
|
||||
bossIds,
|
||||
combinedBossHealthScale,
|
||||
combinedDamageScale,
|
||||
pressureState,
|
||||
)
|
||||
: null
|
||||
recordedKillIdsRef.current = new Set()
|
||||
abilityCooldownsRef.current = {}
|
||||
lastHudSignatureRef.current = arenaHudSignature(next)
|
||||
@@ -131,9 +200,11 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
setArenaState(next)
|
||||
setOpponentArenaState(nextOpponentState)
|
||||
setAbilityCooldowns({})
|
||||
setDropAwards([])
|
||||
setPetAwards([])
|
||||
setSelectedOverlayAction('primary')
|
||||
setStatus('playing')
|
||||
}, [bossHealthScale, bossId, bossIds, pvpRoguelike])
|
||||
}, [bossId, bossIds, combinedBossHealthScale, combinedDamageScale, pveGearActive, pvpRoguelike, roguelikeBuffs, roguelikeContentType, roguelikeStage, save.gearProgress])
|
||||
|
||||
const showOverlay = useCallback((nextStatus: Exclude<ArenaStatus, 'playing'>) => {
|
||||
setSelectedOverlayAction('primary')
|
||||
@@ -141,12 +212,21 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
}, [])
|
||||
|
||||
const abilities = useMemo(
|
||||
() => applyRoguelikeModifiers(
|
||||
abilitiesForHealer(save.character.healerStyle),
|
||||
roguelikeRun?.buffs ?? [],
|
||||
roguelikeRun?.debuffs ?? [],
|
||||
),
|
||||
[roguelikeRun?.buffs, roguelikeRun?.debuffs, save.character.healerStyle],
|
||||
() => {
|
||||
const baseAbilities = abilitiesForHealer(
|
||||
save.character.healerStyle,
|
||||
pveGearActive ? save.gearProgress.healer.infusionAbilityId : null,
|
||||
)
|
||||
const gearAbilities = pveGearActive
|
||||
? applyIwt2PveGearToHealerAbilities(baseAbilities, save.gearProgress)
|
||||
: baseAbilities
|
||||
return applyRoguelikeModifiers(
|
||||
gearAbilities,
|
||||
roguelikeRun?.buffs ?? [],
|
||||
roguelikeRun?.debuffs ?? [],
|
||||
)
|
||||
},
|
||||
[pveGearActive, roguelikeRun?.buffs, roguelikeRun?.debuffs, save.character.healerStyle, save.gearProgress],
|
||||
)
|
||||
|
||||
const castAbility = useCallback((ability: Iwt2HealerAbility) => {
|
||||
@@ -168,8 +248,9 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
|
||||
const moveOverlaySelection = useCallback((action: string) => {
|
||||
setSelectedOverlayAction((current) => {
|
||||
const active = OVERLAY_NAV_ENTRIES.find((entry) => entry.action === current) ?? OVERLAY_NAV_ENTRIES[0]
|
||||
const candidates = OVERLAY_NAV_ENTRIES.filter((entry) => {
|
||||
const entries = overlayNavEntriesFor(statusRef.current, pvpRoguelike)
|
||||
const active = entries.find((entry) => entry.action === current) ?? entries[0]
|
||||
const candidates = entries.filter((entry) => {
|
||||
if (entry.action === current) return false
|
||||
if (action === 'navigateUp') return entry.row < active.row
|
||||
if (action === 'navigateDown') return entry.row > active.row
|
||||
@@ -179,9 +260,13 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
candidates.sort((a, b) => Math.abs(a.row - active.row) - Math.abs(b.row - active.row))
|
||||
return candidates[0]?.action ?? current
|
||||
})
|
||||
}, [])
|
||||
}, [pvpRoguelike])
|
||||
|
||||
const activateOverlayAction = useCallback((overlayAction = selectedOverlayActionRef.current) => {
|
||||
if (overlayAction === 'requeue') {
|
||||
onPvpRequeue?.()
|
||||
return
|
||||
}
|
||||
if (overlayAction === 'menu') {
|
||||
onBack()
|
||||
return
|
||||
@@ -195,7 +280,7 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
return
|
||||
}
|
||||
resetArena()
|
||||
}, [onBack, resetArena, roguelikeRun])
|
||||
}, [onBack, onPvpRequeue, resetArena, roguelikeRun])
|
||||
|
||||
useGameAction((action, device) => {
|
||||
if (device === 'controller' && statusRef.current !== 'playing') {
|
||||
@@ -273,21 +358,33 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
if (newlyDefeatedBosses.length > 0) {
|
||||
const nextRecordedIds = new Set(recordedKillIdsRef.current)
|
||||
let updatedSave = saveRef.current
|
||||
const newDropAwards: Iwt2BossDropAward[] = []
|
||||
const newPetAwards: Iwt2BossPetAward[] = []
|
||||
for (const defeatedBoss of newlyDefeatedBosses) {
|
||||
nextRecordedIds.add(defeatedBoss.bossId)
|
||||
const reward = recordIwt2BossKillReward(updatedSave, defeatedBoss.bossId)
|
||||
const reward = recordIwt2BossKillReward(updatedSave, defeatedBoss.bossId, {
|
||||
difficultySlug,
|
||||
experienceMultiplier,
|
||||
})
|
||||
updatedSave = reward.save
|
||||
newDropAwards.push(reward.dropAwarded)
|
||||
if (reward.petAwarded) newPetAwards.push(reward.petAwarded)
|
||||
}
|
||||
recordedKillIdsRef.current = nextRecordedIds
|
||||
saveRef.current = updatedSave
|
||||
if (newDropAwards.length > 0) setDropAwards((current) => [...current, ...newDropAwards])
|
||||
if (newPetAwards.length > 0) setPetAwards((current) => [...current, ...newPetAwards])
|
||||
onSaveUpdated(updatedSave)
|
||||
}
|
||||
|
||||
if (next.bosses.every((boss) => boss.health <= 0)) {
|
||||
showOverlay('victory')
|
||||
if (pvpRoguelike && roguelikeRun) {
|
||||
statusRef.current = 'victory'
|
||||
setStatus('victory')
|
||||
roguelikeRun.onVictory()
|
||||
} else {
|
||||
showOverlay('victory')
|
||||
}
|
||||
} else if (next.party.every((member) => member.health <= 0)) {
|
||||
showOverlay('defeat')
|
||||
}
|
||||
@@ -305,7 +402,7 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
setAbilityCooldowns(abilityCooldownsRef.current)
|
||||
}
|
||||
return next
|
||||
}, [onSaveUpdated, pvpRoguelike, showOverlay])
|
||||
}, [difficultySlug, experienceMultiplier, onSaveUpdated, pvpRoguelike, roguelikeRun, showOverlay])
|
||||
|
||||
const targetBindings = directPartyTargeting
|
||||
? IWT2_TARGET_ACTIONS.map((action) => activeBindings[action] ?? null)
|
||||
@@ -314,11 +411,14 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
const alivePartyCount = arenaState.party.filter((member) => member.health > 0).length
|
||||
const totalPartyDamage = arenaState.party.reduce((total, member) => total + member.damageDone, 0)
|
||||
const defeatedBossCount = arenaState.bosses.filter((boss) => boss.health <= 0).length
|
||||
const victoryExperience = Math.round(arenaState.bosses.length * 125 * experienceMultiplier)
|
||||
const bossTitle = formatBossEncounterTitle(arenaState.bosses)
|
||||
const overlayPrimaryLabel = status === 'paused'
|
||||
? 'Resume'
|
||||
: status === 'victory' && roguelikeRun
|
||||
? 'Choose Upgrade'
|
||||
: pvpRoguelike && status !== 'playing'
|
||||
? 'Rematch'
|
||||
: 'Restart'
|
||||
const overlayTitle = status === 'victory'
|
||||
? `${bossTitle} Down`
|
||||
@@ -463,9 +563,13 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
</div>
|
||||
{status === 'victory' && (
|
||||
<div className="iwt2-result-reward">
|
||||
<span>+{arenaState.bosses.length * 125} XP</span>
|
||||
<span>{arenaState.bosses.length} carves</span>
|
||||
<span>+{victoryExperience} XP</span>
|
||||
<span>Log +{arenaState.bosses.length}</span>
|
||||
{dropAwards.map((award) => (
|
||||
<span key={`${award.dropId}:${award.quantityAfter}`}>
|
||||
{award.dropName}{award.quantity > 1 ? ` x${award.quantity}` : ''}
|
||||
</span>
|
||||
))}
|
||||
{petAwards.map((award) => (
|
||||
<span key={`${award.petId}:${award.quantityAfter}`}>
|
||||
{award.petName}{award.duplicate ? ` x${award.quantityAfter}` : ''}
|
||||
@@ -478,14 +582,27 @@ export function BossArenaScreen({ bossId, bossIds, modeLabel, save, onBack, onSa
|
||||
className={`iwt2-result-button is-primary ${selectedOverlayAction === 'primary' ? 'game-selected' : ''}`}
|
||||
data-game-selected={selectedOverlayAction === 'primary' ? 'true' : undefined}
|
||||
onClick={() => activateOverlayAction('primary')}
|
||||
onPointerDown={() => setSelectedOverlayAction('primary')}
|
||||
type="button"
|
||||
>
|
||||
{overlayPrimaryLabel}
|
||||
</button>
|
||||
{pvpRoguelike && (
|
||||
<button
|
||||
className={`iwt2-result-button is-secondary ${selectedOverlayAction === 'requeue' ? 'game-selected' : ''}`}
|
||||
data-game-selected={selectedOverlayAction === 'requeue' ? 'true' : undefined}
|
||||
onClick={() => activateOverlayAction('requeue')}
|
||||
onPointerDown={() => setSelectedOverlayAction('requeue')}
|
||||
type="button"
|
||||
>
|
||||
Requeue
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`iwt2-result-button is-secondary ${selectedOverlayAction === 'menu' ? 'game-selected' : ''}`}
|
||||
data-game-selected={selectedOverlayAction === 'menu' ? 'true' : undefined}
|
||||
onClick={() => activateOverlayAction('menu')}
|
||||
onPointerDown={() => setSelectedOverlayAction('menu')}
|
||||
type="button"
|
||||
>
|
||||
Menu
|
||||
@@ -518,7 +635,12 @@ function formatArenaTime(seconds: number): string {
|
||||
}
|
||||
|
||||
function roguelikeBossHealthScale(stage: number): number {
|
||||
return 0.5 + Math.max(0, stage - 1) * 0.1
|
||||
return 1 + Math.max(0, stage - 1) * 0.1
|
||||
}
|
||||
|
||||
function overlayNavEntriesFor(status: ArenaStatus, pvpRoguelike: boolean): OverlayNavEntry[] {
|
||||
if (pvpRoguelike && (status === 'victory' || status === 'defeat')) return PVP_RESULT_OVERLAY_NAV_ENTRIES
|
||||
return DEFAULT_OVERLAY_NAV_ENTRIES
|
||||
}
|
||||
|
||||
function arenaHudSignature(state: Iwt2ArenaState): string {
|
||||
@@ -566,12 +688,66 @@ function tickCooldowns(cooldowns: Record<string, number>, dt: number) {
|
||||
return changed ? next : cooldowns
|
||||
}
|
||||
|
||||
function createArenaState(
|
||||
bossId: Iwt2BossId,
|
||||
bossIds: Iwt2BossId[] | undefined,
|
||||
bossHealthScale: number,
|
||||
partyDamageTakenScale: number,
|
||||
buffs: Iwt2RoguelikeSelfBuffId[],
|
||||
roguelikePressure: ReturnType<typeof createPressureState>,
|
||||
gearProgress?: Iwt2Save['gearProgress'],
|
||||
): Iwt2ArenaState {
|
||||
const baseState = createInitialIwt2ArenaState(
|
||||
bossId,
|
||||
bossIds,
|
||||
bossHealthScale,
|
||||
partyDamageTakenScale,
|
||||
roguelikePressure,
|
||||
)
|
||||
const state = gearProgress ? applyIwt2PveGearStats(baseState, gearProgress) : baseState
|
||||
const shieldedDamageTakenMultiplier = shieldedDamageTakenMultiplierForBuffs(buffs)
|
||||
const shieldedHotHealingMultiplier = buffs.includes(IWT2_BARKSKIN_HOT_BONUS_BUFF_ID) ? 1.25 : undefined
|
||||
if (shieldedDamageTakenMultiplier === undefined && shieldedHotHealingMultiplier === undefined) return state
|
||||
return {
|
||||
...state,
|
||||
party: state.party.map((member) => ({
|
||||
...member,
|
||||
shieldedDamageTakenMultiplier,
|
||||
shieldedHotHealingMultiplier,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function createPressureState(
|
||||
stage: number | undefined,
|
||||
contentType: Iwt2RoguelikeContentType | undefined,
|
||||
) {
|
||||
return stage && contentType ? createRoguelikePressureState(stage, contentType) : undefined
|
||||
}
|
||||
|
||||
function shieldedDamageTakenMultiplierForBuffs(buffs: Iwt2RoguelikeSelfBuffId[]) {
|
||||
const multipliers: number[] = []
|
||||
if (buffs.includes(IWT2_SUN_WARD_DAMAGE_REDUCTION_BUFF_ID)) multipliers.push(0.5)
|
||||
if (buffs.includes(IWT2_AEGIS_SCRIPT_DAMAGE_REDUCTION_BUFF_ID)) multipliers.push(0.7)
|
||||
return multipliers.length > 0 ? Math.min(...multipliers) : undefined
|
||||
}
|
||||
|
||||
function applyRoguelikeModifiers(
|
||||
abilities: Iwt2HealerAbility[],
|
||||
buffs: Iwt2RoguelikeSelfBuffId[],
|
||||
debuffs: Iwt2RoguelikeOpponentDebuffId[],
|
||||
): Iwt2HealerAbility[] {
|
||||
if (buffs.length === 0 && debuffs.length === 0) return abilities
|
||||
const abilityById = new Map(abilities.map((ability) => [ability.id, ability]))
|
||||
const renewEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-renew'))
|
||||
const sunWardEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-sun-ward'))
|
||||
const halfSunWardEffect = triggeredEffectFromAbility(abilityById.get('dawnweaver-sun-ward'), 0.5)
|
||||
const seedOfLifeEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-seed-of-life'))
|
||||
const barkskinEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-barkskin'))
|
||||
const halfBarkskinEffect = triggeredEffectFromAbility(abilityById.get('lifebinder-barkskin'), 0.5)
|
||||
const mendingRuneEffect = triggeredEffectFromAbility(abilityById.get('runesage-mending-rune'))
|
||||
const aegisScriptEffect = triggeredEffectFromAbility(abilityById.get('runesage-aegis-script'))
|
||||
const halfAegisScriptEffect = triggeredEffectFromAbility(abilityById.get('runesage-aegis-script'), 0.5)
|
||||
return abilities.map((ability) => {
|
||||
const slot = String(ability.slot)
|
||||
const costDown = countStacks(buffs, `slot${slot}-cost-down`)
|
||||
@@ -579,16 +755,118 @@ function applyRoguelikeModifiers(
|
||||
const cooldownDown = countStacks(buffs, `slot${slot}-cooldown-down`)
|
||||
const cooldownUp = countStacks(debuffs, `opp-slot${slot}-cooldown-up`)
|
||||
const extraTargets = countStacks(buffs, `slot${slot}-extra-target`)
|
||||
if (costDown === 0 && costUp === 0 && cooldownDown === 0 && cooldownUp === 0 && extraTargets === 0) return ability
|
||||
const triggeredEffects = iwt2TriggeredEffectsForAbility({
|
||||
ability,
|
||||
aegisScriptEffect,
|
||||
barkskinEffect,
|
||||
buffs,
|
||||
halfAegisScriptEffect,
|
||||
halfBarkskinEffect,
|
||||
halfSunWardEffect,
|
||||
mendingRuneEffect,
|
||||
renewEffect,
|
||||
seedOfLifeEffect,
|
||||
sunWardEffect,
|
||||
})
|
||||
if (costDown === 0 && costUp === 0 && cooldownDown === 0 && cooldownUp === 0 && extraTargets === 0 && triggeredEffects.length === 0) return ability
|
||||
return {
|
||||
...ability,
|
||||
cooldownSeconds: roundModifier(ability.cooldownSeconds * 0.75 ** cooldownDown * 1.25 ** cooldownUp),
|
||||
extraTargets: (ability.extraTargets ?? 0) + extraTargets,
|
||||
manaCost: Math.max(1, Math.ceil(ability.manaCost * 0.75 ** costDown * 1.25 ** costUp)),
|
||||
triggeredEffects: triggeredEffects.length > 0 ? triggeredEffects : ability.triggeredEffects,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function iwt2TriggeredEffectsForAbility({
|
||||
ability,
|
||||
aegisScriptEffect,
|
||||
barkskinEffect,
|
||||
buffs,
|
||||
halfAegisScriptEffect,
|
||||
halfBarkskinEffect,
|
||||
halfSunWardEffect,
|
||||
mendingRuneEffect,
|
||||
renewEffect,
|
||||
seedOfLifeEffect,
|
||||
sunWardEffect,
|
||||
}: {
|
||||
ability: Iwt2HealerAbility
|
||||
aegisScriptEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
barkskinEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
buffs: Iwt2RoguelikeSelfBuffId[]
|
||||
halfAegisScriptEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
halfBarkskinEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
halfSunWardEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
mendingRuneEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
renewEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
seedOfLifeEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
sunWardEffect: Iwt2TriggeredAbilityEffect | undefined
|
||||
}): Iwt2TriggeredAbilityEffect[] {
|
||||
const effects = [...ability.triggeredEffects ?? []]
|
||||
const addEffect = (effect: Iwt2TriggeredAbilityEffect | undefined) => {
|
||||
if (effect && !effects.some((existing) => existing.id === effect.id && existing.power === effect.power)) {
|
||||
effects.push(effect)
|
||||
}
|
||||
}
|
||||
if (ability.id === 'dawnweaver-mend') {
|
||||
if (buffs.includes('dawnweaver-mend-applies-renew')) addEffect(renewEffect)
|
||||
if (buffs.includes('dawnweaver-mend-applies-sun-ward')) addEffect(sunWardEffect)
|
||||
} else if (ability.id === 'dawnweaver-renew') {
|
||||
if (buffs.includes('dawnweaver-renew-applies-sun-ward')) addEffect(sunWardEffect)
|
||||
} else if (ability.id === 'dawnweaver-radiance') {
|
||||
if (buffs.includes('dawnweaver-radiance-applies-renew')) addEffect(renewEffect)
|
||||
if (buffs.includes('dawnweaver-radiance-applies-sun-ward')) addEffect(halfSunWardEffect)
|
||||
} else if (ability.id === 'dawnweaver-sun-ward') {
|
||||
if (buffs.includes('dawnweaver-sun-ward-applies-renew')) addEffect(renewEffect)
|
||||
} else if (ability.id === 'dawnweaver-purify') {
|
||||
if (buffs.includes('dawnweaver-purify-applies-renew')) addEffect(renewEffect)
|
||||
if (buffs.includes('dawnweaver-purify-applies-sun-ward')) addEffect(sunWardEffect)
|
||||
} else if (ability.id === 'lifebinder-verdant-touch') {
|
||||
if (buffs.includes('lifebinder-verdant-touch-applies-seed-of-life')) addEffect(seedOfLifeEffect)
|
||||
if (buffs.includes('lifebinder-verdant-touch-applies-barkskin')) addEffect(halfBarkskinEffect)
|
||||
} else if (ability.id === 'lifebinder-seed-of-life') {
|
||||
if (buffs.includes('lifebinder-seed-of-life-applies-barkskin')) addEffect(barkskinEffect)
|
||||
} else if (ability.id === 'lifebinder-wild-growth') {
|
||||
if (buffs.includes('lifebinder-wild-growth-applies-seed-of-life')) addEffect(seedOfLifeEffect)
|
||||
if (buffs.includes('lifebinder-wild-growth-applies-barkskin')) addEffect(halfBarkskinEffect)
|
||||
} else if (ability.id === 'lifebinder-barkskin') {
|
||||
if (buffs.includes('lifebinder-barkskin-applies-seed-of-life')) addEffect(seedOfLifeEffect)
|
||||
} else if (ability.id === 'lifebinder-purging-sap') {
|
||||
if (buffs.includes('lifebinder-purging-sap-applies-seed-of-life')) addEffect(seedOfLifeEffect)
|
||||
if (buffs.includes('lifebinder-purging-sap-applies-barkskin')) addEffect(barkskinEffect)
|
||||
} else if (ability.id === 'runesage-etched-mend') {
|
||||
if (buffs.includes('runesage-etched-mend-applies-mending-rune')) addEffect(mendingRuneEffect)
|
||||
if (buffs.includes('runesage-etched-mend-applies-aegis-script')) addEffect(halfAegisScriptEffect)
|
||||
} else if (ability.id === 'runesage-mending-rune') {
|
||||
if (buffs.includes('runesage-mending-rune-applies-aegis-script')) addEffect(aegisScriptEffect)
|
||||
} else if (ability.id === 'runesage-concordance') {
|
||||
if (buffs.includes('runesage-concordance-applies-mending-rune')) addEffect(mendingRuneEffect)
|
||||
if (buffs.includes('runesage-concordance-applies-aegis-script')) addEffect(halfAegisScriptEffect)
|
||||
} else if (ability.id === 'runesage-aegis-script') {
|
||||
if (buffs.includes('runesage-aegis-script-applies-mending-rune')) addEffect(mendingRuneEffect)
|
||||
} else if (ability.id === 'runesage-unravel') {
|
||||
if (buffs.includes('runesage-unravel-applies-mending-rune')) addEffect(mendingRuneEffect)
|
||||
if (buffs.includes('runesage-unravel-applies-aegis-script')) addEffect(aegisScriptEffect)
|
||||
}
|
||||
return effects
|
||||
}
|
||||
|
||||
function triggeredEffectFromAbility(
|
||||
ability: Iwt2HealerAbility | undefined,
|
||||
strength = 1,
|
||||
): Iwt2TriggeredAbilityEffect | undefined {
|
||||
if (!ability) return undefined
|
||||
return {
|
||||
effectType: ability.effectType,
|
||||
id: ability.id,
|
||||
kind: ability.kind,
|
||||
name: ability.name,
|
||||
power: Math.max(1, Math.round(ability.power * strength)),
|
||||
}
|
||||
}
|
||||
|
||||
function countStacks(items: readonly string[], id: string) {
|
||||
return items.filter((item) => item === id).length
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user