Android build v1.0.35
This commit is contained in:
+104
-37
@@ -105,18 +105,18 @@ function effectiveMaxHealth(member: PartyMember) {
|
||||
return Math.max(1, Math.round(member.maxHealth * (member.maxHealthPenaltyTicks && member.maxHealthPenaltyTicks > 0 ? 0.75 : 1)))
|
||||
}
|
||||
|
||||
function healAmount(member: PartyMember, amount: number) {
|
||||
return Math.round(amount * (member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1))
|
||||
function healAmount(member: PartyMember, amount: number, multiplier = 1) {
|
||||
return Math.round(amount * (member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1) * multiplier)
|
||||
}
|
||||
|
||||
function healMember(member: PartyMember, amount: number) {
|
||||
return clamp(member.health + healAmount(member, amount), 0, effectiveMaxHealth(member))
|
||||
function healMember(member: PartyMember, amount: number, multiplier = 1) {
|
||||
return clamp(member.health + healAmount(member, amount, multiplier), 0, effectiveMaxHealth(member))
|
||||
}
|
||||
|
||||
function memberHotEffects(member: PartyMember) {
|
||||
if (member.hotEffects?.length) return member.hotEffects
|
||||
return member.hotTicks > 0
|
||||
? [{ id: 'legacy-renew', label: 'Renew', ticks: member.hotTicks, power: 6 }]
|
||||
? [{ id: 'legacy-renew', spellId: 'legacy-renew', label: 'Renew', ticks: member.hotTicks, power: 6 }]
|
||||
: []
|
||||
}
|
||||
|
||||
@@ -125,15 +125,15 @@ function effectId(prefix: string) {
|
||||
}
|
||||
|
||||
function addHotEffect(member: PartyMember, spell: Spell, ticks = 5) {
|
||||
return [
|
||||
...memberHotEffects(member),
|
||||
{
|
||||
id: effectId(spell.id),
|
||||
label: spell.name,
|
||||
ticks,
|
||||
power: Math.max(1, Math.round(spell.power / 2)),
|
||||
},
|
||||
]
|
||||
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]
|
||||
}
|
||||
|
||||
function addBounceHeal(member: PartyMember, spell: Spell) {
|
||||
@@ -418,6 +418,7 @@ export function CombatScreen({
|
||||
const [encounterIndex, setEncounterIndex] = useState(initialEncounterIndex)
|
||||
const [status, setStatus] = useState<'playing' | 'won' | 'lost' | 'part-complete' | 'upgrade-choice'>('playing')
|
||||
const [paused, setPaused] = useState(false)
|
||||
const [speedMultiplier, setSpeedMultiplier] = useState<1 | 2>(1)
|
||||
const [targetGroup, setTargetGroup] = useState<0 | 1 | 2>(0)
|
||||
const [log, setLog] = useState<CombatLogEntry[]>([
|
||||
{ id: 1, text: `${dungeon.name} begins.`, tone: 'system' },
|
||||
@@ -445,6 +446,7 @@ export function CombatScreen({
|
||||
const lastCombatTickAtRef = useRef(performance.now())
|
||||
const statusRef = useRef(status)
|
||||
const pausedRef = useRef(paused)
|
||||
const speedMultiplierRef = useRef<1 | 2>(speedMultiplier)
|
||||
const { party, resource, enemyHealth, cooldowns, freeCastReady } = combatState
|
||||
const encounter = encounters[encounterIndex]
|
||||
const encounterMaxHealth = encounter.maxHealth * enemyCount
|
||||
@@ -463,11 +465,21 @@ export function CombatScreen({
|
||||
const playerHealer = party.find((member) => member.id === 'mira')
|
||||
const playerIsAlive = Boolean(playerHealer && playerHealer.health > 0)
|
||||
const upgradesEveryEncounter = roguelikeUpgradeTiming === 'encounter'
|
||||
const activeSetEffects = useMemo(
|
||||
() => isRoguelike
|
||||
? new Set<string>()
|
||||
: new Set(profile.setBonuses.filter((bonus) => bonus.active).map((bonus) => bonus.effectType)),
|
||||
[isRoguelike, profile.setBonuses],
|
||||
const activeEffects = useMemo(
|
||||
() => {
|
||||
const effects = new Set<string>(
|
||||
gameClass.talents
|
||||
.filter((talent) => talent.rank > 0)
|
||||
.map((talent) => talent.effectType),
|
||||
)
|
||||
if (!isRoguelike) {
|
||||
profile.setBonuses
|
||||
.filter((bonus) => bonus.active)
|
||||
.forEach((bonus) => effects.add(bonus.effectType))
|
||||
}
|
||||
return effects
|
||||
},
|
||||
[gameClass.talents, isRoguelike, profile.setBonuses],
|
||||
)
|
||||
const {
|
||||
bindings,
|
||||
@@ -481,6 +493,7 @@ export function CombatScreen({
|
||||
|
||||
statusRef.current = status
|
||||
pausedRef.current = paused
|
||||
speedMultiplierRef.current = speedMultiplier
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now()
|
||||
@@ -619,6 +632,14 @@ export function CombatScreen({
|
||||
const extraTarget = (blockedIds: string[]) => current.party
|
||||
.filter((member) => member.health > 0 && !blockedIds.includes(member.id))
|
||||
.sort((left, right) => (left.health / left.maxHealth) - (right.health / right.maxHealth))[0]
|
||||
const effectSpell = (name: string) => {
|
||||
const ability = gameClass.spells.find((candidate) => candidate.name === name)
|
||||
return ability ? toCombatSpell(ability, `effect-${ability.id}`, healingPower) : null
|
||||
}
|
||||
const renewEffect = effectSpell('Renew')
|
||||
const shieldEffect = effectSpell('Sun Ward')
|
||||
const healingMultiplier = (member: PartyMember) =>
|
||||
activeEffects.has('shielded_healing_bonus') && member.shield > 0 ? 1.2 : 1
|
||||
const directTargets = new Set([targetId])
|
||||
const hotTargets = new Set<string>()
|
||||
const shieldTargets = new Set<string>()
|
||||
@@ -630,15 +651,15 @@ export function CombatScreen({
|
||||
)
|
||||
if (spell.kind === 'hot' || spell.effectType === 'direct_hot') hotTargets.add(targetId)
|
||||
if (spell.kind === 'shield') shieldTargets.add(targetId)
|
||||
if (spell.name === 'Mend' && activeSetEffects.has('mend_extra_target')) {
|
||||
if (spell.name === 'Mend' && activeEffects.has('mend_extra_target')) {
|
||||
const extra = extraTarget([targetId])
|
||||
if (extra) directTargets.add(extra.id)
|
||||
}
|
||||
if (spell.name === 'Renew' && activeSetEffects.has('renew_extra_target')) {
|
||||
if (spell.name === 'Renew' && activeEffects.has('renew_extra_target')) {
|
||||
const extra = extraTarget([targetId])
|
||||
if (extra) hotTargets.add(extra.id)
|
||||
}
|
||||
if (spell.name === 'Mend' && activeSetEffects.has('mend_applies_renew')) {
|
||||
if (spell.name === 'Mend' && activeEffects.has('mend_applies_renew')) {
|
||||
hotTargets.add(targetId)
|
||||
}
|
||||
for (let index = 0; index < extraTargets; index += 1) {
|
||||
@@ -673,9 +694,20 @@ export function CombatScreen({
|
||||
}
|
||||
}
|
||||
const power = Math.round(spell.power * (1.25 ** upgradeStackCount(roguelikeUpgrades, 'group-heal-boost')))
|
||||
const nextHealth = healMember(member, power)
|
||||
const nextHealth = healMember(member, power, healingMultiplier(member))
|
||||
addFloatingHeal(member.id, Math.max(0, nextHealth - member.health))
|
||||
return { ...member, health: nextHealth }
|
||||
const nextShield = spell.name === 'Radiance' && activeEffects.has('radiance_applies_shield')
|
||||
? Math.max(member.shield, Math.round((shieldEffect?.power ?? spell.power) * 0.3))
|
||||
: member.shield
|
||||
return {
|
||||
...member,
|
||||
health: nextHealth,
|
||||
shield: nextShield,
|
||||
hotTicks: spell.name === 'Radiance' && activeEffects.has('radiance_applies_renew') ? 0 : member.hotTicks,
|
||||
hotEffects: spell.name === 'Radiance' && activeEffects.has('radiance_applies_renew') && renewEffect
|
||||
? addHotEffect(member, renewEffect, 3)
|
||||
: member.hotEffects,
|
||||
}
|
||||
}
|
||||
if (
|
||||
!directTargets.has(member.id)
|
||||
@@ -685,7 +717,14 @@ export function CombatScreen({
|
||||
) return member
|
||||
if (spell.kind === 'shield') {
|
||||
const power = Math.round(spell.power * (1.25 ** upgradeStackCount(roguelikeUpgrades, 'shield-boost')))
|
||||
return { ...member, shield: Math.max(member.shield, power) }
|
||||
return {
|
||||
...member,
|
||||
hotTicks: activeEffects.has('shield_applies_renew') && renewEffect ? 0 : member.hotTicks,
|
||||
hotEffects: activeEffects.has('shield_applies_renew') && renewEffect
|
||||
? addHotEffect(member, renewEffect)
|
||||
: member.hotEffects,
|
||||
shield: Math.max(member.shield, power),
|
||||
}
|
||||
}
|
||||
if (spell.kind === 'damage_reduction') {
|
||||
return { ...member, damageReductionTicks: 12 }
|
||||
@@ -696,7 +735,7 @@ export function CombatScreen({
|
||||
if (spell.kind === 'cleanse') {
|
||||
return {
|
||||
...member,
|
||||
health: healMember(member, spell.power),
|
||||
health: healMember(member, spell.power, healingMultiplier(member)),
|
||||
debuff: undefined,
|
||||
debuffTicks: undefined,
|
||||
poisonStacks: undefined,
|
||||
@@ -705,14 +744,23 @@ export function CombatScreen({
|
||||
}
|
||||
}
|
||||
const nextHealth = directTargets.has(member.id)
|
||||
? healMember(member, spell.power)
|
||||
? healMember(member, spell.power, healingMultiplier(member))
|
||||
: member.health
|
||||
if (nextHealth > member.health) addFloatingHeal(member.id, nextHealth - member.health)
|
||||
const nextShield = spell.name === 'Mend' && directTargets.has(member.id) && activeEffects.has('mend_applies_shield')
|
||||
? Math.max(member.shield, Math.round((shieldEffect?.power ?? spell.power) * 0.5))
|
||||
: member.shield
|
||||
const appliedHotSpell = spell.name === 'Mend' && activeEffects.has('mend_applies_renew') && renewEffect
|
||||
? renewEffect
|
||||
: spell
|
||||
return {
|
||||
...member,
|
||||
health: nextHealth,
|
||||
shield: nextShield,
|
||||
hotTicks: 0,
|
||||
hotEffects: hotTargets.has(member.id) ? addHotEffect(member, spell) : member.hotEffects,
|
||||
hotEffects: hotTargets.has(member.id)
|
||||
? addHotEffect(member, appliedHotSpell)
|
||||
: member.hotEffects,
|
||||
}
|
||||
})
|
||||
const freeCastStacks = upgradeStackCount(roguelikeUpgrades, 'fifth-cast-free')
|
||||
@@ -730,20 +778,26 @@ export function CombatScreen({
|
||||
&& !current.freeCastReady
|
||||
&& current.castsTowardFree + 1 >= 5
|
||||
resourceSpentRef.current += effectiveCost
|
||||
const nextCooldowns = {
|
||||
...current.cooldowns,
|
||||
}
|
||||
if (spell.name === 'Mend' && activeEffects.has('mend_reduces_radiance_cooldown')) {
|
||||
const radiance = spells.find((candidate) => candidate.name === 'Radiance')
|
||||
if (radiance) nextCooldowns[radiance.id] = Math.max(0, (nextCooldowns[radiance.id] ?? 0) - 2)
|
||||
}
|
||||
nextCooldowns[spell.id] = spell.cooldown * cooldownMultiplier(spell, roguelikeUpgrades)
|
||||
|
||||
setCombat({
|
||||
...current,
|
||||
party: nextParty,
|
||||
resource: current.resource - effectiveCost,
|
||||
cooldowns: {
|
||||
...current.cooldowns,
|
||||
[spell.id]: spell.cooldown * cooldownMultiplier(spell, roguelikeUpgrades),
|
||||
},
|
||||
cooldowns: nextCooldowns,
|
||||
castsTowardFree: nextCastsTowardFree,
|
||||
freeCastReady: gainedFreeCast || nextFreeCastReady,
|
||||
})
|
||||
addLog(`${spell.name} cast on ${spell.kind === 'group' ? 'the party' : selected.name}${effectiveCost === 0 ? ' for free' : ''}.`, 'heal')
|
||||
},
|
||||
[activeSetEffects, addFloatingHeal, addLog, roguelikeUpgrades, setCombat, status],
|
||||
[activeEffects, addFloatingHeal, addLog, gameClass.spells, healingPower, roguelikeUpgrades, setCombat, spells, status],
|
||||
)
|
||||
|
||||
const finishRun = useCallback(
|
||||
@@ -909,6 +963,10 @@ export function CombatScreen({
|
||||
}, [addLog, difficulty, encounterIndex, encounters, enemyCount, maxResource, roguelikeMode, roguelikePool, roguelikeStage, setCombat])
|
||||
|
||||
useGameAction((action, device) => {
|
||||
if (action === 'toggleSpeed') {
|
||||
if (status === 'playing') setSpeedMultiplier((value) => (value === 1 ? 2 : 1))
|
||||
return
|
||||
}
|
||||
if (action === 'pause' || (action === 'back' && device === 'pc')) {
|
||||
if (status === 'playing') setPaused((value) => !value)
|
||||
return
|
||||
@@ -1021,13 +1079,17 @@ export function CombatScreen({
|
||||
if ((member.damageReductionTicks ?? 0) > 0) {
|
||||
damage = Math.round(damage * 0.5)
|
||||
}
|
||||
if (member.shield > 0 && activeEffects.has('shielded_damage_reduction')) {
|
||||
damage = Math.round(damage * 0.8)
|
||||
}
|
||||
const absorbed = Math.min(member.shield, damage)
|
||||
const hotEffects = memberHotEffects(member)
|
||||
let healing = hotEffects.reduce((total, effect) => total + healAmount(member, effect.power), 0)
|
||||
const healingMultiplier = member.shield > 0 && activeEffects.has('shielded_healing_bonus') ? 1.2 : 1
|
||||
let healing = hotEffects.reduce((total, effect) => total + healAmount(member, effect.power, healingMultiplier), 0)
|
||||
let nextBounceHeals = [...(member.bounceHeals ?? [])]
|
||||
if (damage > 0 && nextBounceHeals.length > 0) {
|
||||
nextBounceHeals = nextBounceHeals.flatMap((effect) => {
|
||||
healing += healAmount(member, effect.power)
|
||||
healing += healAmount(member, effect.power, healingMultiplier)
|
||||
const nextCharges = effect.charges - 1
|
||||
if (nextCharges <= 0) return []
|
||||
const jumpTargets = current.party.filter((candidate) => candidate.health > 0 && candidate.id !== member.id)
|
||||
@@ -1192,6 +1254,7 @@ export function CombatScreen({
|
||||
})
|
||||
addLog(`${encounter.enemyName} defeated. ${nextEncounter.enemyName} approaches.`, 'system')
|
||||
}, [
|
||||
activeEffects,
|
||||
addLog,
|
||||
addFloatingHeal,
|
||||
difficulty.damageMultiplier,
|
||||
@@ -1239,9 +1302,10 @@ export function CombatScreen({
|
||||
|| pausedRef.current
|
||||
) return
|
||||
const now = performance.now()
|
||||
const dueTicks = Math.min(4, Math.floor((now - lastCombatTickAtRef.current) / TICK_MS))
|
||||
const tickMs = TICK_MS / speedMultiplierRef.current
|
||||
const dueTicks = Math.min(8, Math.floor((now - lastCombatTickAtRef.current) / tickMs))
|
||||
if (dueTicks <= 0) return
|
||||
lastCombatTickAtRef.current += dueTicks * TICK_MS
|
||||
lastCombatTickAtRef.current += dueTicks * tickMs
|
||||
for (let index = 0; index < dueTicks; index += 1) {
|
||||
if (statusRef.current !== 'playing' || pausedRef.current) return
|
||||
runCombatTickRef.current()
|
||||
@@ -1310,6 +1374,7 @@ export function CombatScreen({
|
||||
directPartyTargeting,
|
||||
paused,
|
||||
targetGroup,
|
||||
speedMultiplier,
|
||||
}), [
|
||||
bindings,
|
||||
controllerIconStyle,
|
||||
@@ -1340,6 +1405,7 @@ export function CombatScreen({
|
||||
spells,
|
||||
freeCastReady,
|
||||
roguelikeUpgrades,
|
||||
speedMultiplier,
|
||||
status,
|
||||
targetGroup,
|
||||
])
|
||||
@@ -1403,6 +1469,7 @@ export function CombatScreen({
|
||||
? `${gameClass.resourceName} ${Math.floor(resource)} / ${maxResource}`
|
||||
: `${profile.character.name} is defeated`}
|
||||
</span>
|
||||
{speedMultiplier === 2 && <strong className="speed-badge">2x speed</strong>}
|
||||
<div className="bar mana-bar"><span style={{ width: `${(resource / maxResource) * 100}%` }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user