Android build v1.0.35

This commit is contained in:
Warren H
2026-06-20 16:10:35 -04:00
parent 1281be69d8
commit 2300973164
15 changed files with 877 additions and 293 deletions
+104 -37
View File
@@ -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>