Compare commits

...

1 Commits

Author SHA1 Message Date
Warren H abdf4cc654 Android build v1.0.56 2026-06-21 21:00:17 -04:00
6 changed files with 88 additions and 103 deletions
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal" applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 74 versionCode 75
versionName "1.0.55" versionName "1.0.56"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+12 -3
View File
@@ -2307,7 +2307,10 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
const bossesCleared = Number(runMetrics?.bossesCleared ?? Math.floor(encountersCleared / 3)) const bossesCleared = Number(runMetrics?.bossesCleared ?? Math.floor(encountersCleared / 3))
const experienceMode = runMetrics?.experienceMode === 'pvp-boss-quarter-level' const experienceMode = runMetrics?.experienceMode === 'pvp-boss-quarter-level'
? 'pvp-boss-quarter-level' ? 'pvp-boss-quarter-level'
: runMetrics?.experienceMode === 'pvp-fight-twelfth-level'
? 'pvp-fight-twelfth-level'
: 'default' : 'default'
const fightsCleared = Number(runMetrics?.fightsCleared ?? encountersCleared)
const resourceSpent = Number(runMetrics?.resourceSpent) const resourceSpent = Number(runMetrics?.resourceSpent)
const durationSeconds = Number(runMetrics?.durationSeconds) const durationSeconds = Number(runMetrics?.durationSeconds)
if (!Number.isInteger(dungeonId) || dungeonId < 1) { if (!Number.isInteger(dungeonId) || dungeonId < 1) {
@@ -2322,6 +2325,9 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
if (!Number.isInteger(bossesCleared) || bossesCleared < 0 || bossesCleared > 100000) { if (!Number.isInteger(bossesCleared) || bossesCleared < 0 || bossesCleared > 100000) {
throw new Error('The roguelike boss total is invalid.') throw new Error('The roguelike boss total is invalid.')
} }
if (!Number.isInteger(fightsCleared) || fightsCleared < 0 || fightsCleared > 100000) {
throw new Error('The roguelike fight total is invalid.')
}
if (!Number.isInteger(resourceSpent) || resourceSpent < 0 || resourceSpent > 100000) { if (!Number.isInteger(resourceSpent) || resourceSpent < 0 || resourceSpent > 100000) {
throw new Error('The run resource total is invalid.') throw new Error('The run resource total is invalid.')
} }
@@ -2374,14 +2380,15 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
`).get(maxLevel).experienceRequired `).get(maxLevel).experienceRequired
let newExperience = character.experience let newExperience = character.experience
let newLevel = character.level let newLevel = character.level
if (experienceMode === 'pvp-boss-quarter-level') { if (experienceMode === 'pvp-boss-quarter-level' || experienceMode === 'pvp-fight-twelfth-level') {
const catchUpTargetLevel = database.prepare(` const catchUpTargetLevel = database.prepare(`
SELECT COALESCE(MAX(level), 0) AS level SELECT COALESCE(MAX(level), 0) AS level
FROM characters FROM characters
WHERE account_id = ? WHERE account_id = ?
AND id != ? AND id != ?
`).get(accountId, characterId).level `).get(accountId, characterId).level
for (let bossIndex = 0; bossIndex < bossesCleared && newExperience < maxExperience; bossIndex += 1) { const rewardUnits = experienceMode === 'pvp-boss-quarter-level' ? bossesCleared : fightsCleared
for (let rewardIndex = 0; rewardIndex < rewardUnits && newExperience < maxExperience; rewardIndex += 1) {
const currentLevelFloor = database.prepare(` const currentLevelFloor = database.prepare(`
SELECT experience_required AS experienceRequired SELECT experience_required AS experienceRequired
FROM level_progression FROM level_progression
@@ -2395,7 +2402,9 @@ function completeRoguelike(database, characterId, accountId, runMetrics) {
WHERE level = ? WHERE level = ?
`).get(newLevel + 1).experienceRequired `).get(newLevel + 1).experienceRequired
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor) const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
const rewardRate = catchUpTargetLevel > newLevel ? 0.5 : 0.25 const rewardRate = experienceMode === 'pvp-boss-quarter-level'
? (catchUpTargetLevel > newLevel ? 0.5 : 0.25)
: (catchUpTargetLevel > newLevel ? 1 / 6 : 1 / 12)
newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * rewardRate)) newExperience = Math.min(maxExperience, newExperience + Math.round(levelBand * rewardRate))
newLevel = database.prepare(` newLevel = database.prepare(`
SELECT MAX(level) AS level SELECT MAX(level) AS level
+33 -92
View File
@@ -57,24 +57,17 @@ type PvpEncounter = DungeonEncounter & {
} }
type SlotKey = '1' | '2' | '3' | '4' | '5' | '6' type SlotKey = '1' | '2' | '3' | '4' | '5' | '6'
type DraftSlotKey = Exclude<SlotKey, '6'>
type AbilityLabelMode = 'ability' | 'slot' type AbilityLabelMode = 'ability' | 'slot'
type SelfBuffId = type SelfBuffId =
| `slot${SlotKey}-extra-target` | `slot${DraftSlotKey}-extra-target`
| `slot${SlotKey}-cost-down` | `slot${DraftSlotKey}-cost-down`
| `slot${SlotKey}-cooldown-down` | `slot${DraftSlotKey}-cooldown-down`
| 'fifth-cast-free'
| 'group-heal-boost'
| 'shield-boost'
type OpponentDebuffId = type OpponentDebuffId =
| `opp-slot${SlotKey}-cost-up` | `opp-slot${DraftSlotKey}-cost-up`
| `opp-slot${SlotKey}-cooldown-up` | `opp-slot${DraftSlotKey}-cooldown-up`
| 'opp-takes-more-damage'
| 'opp-healing-reduced'
| 'opp-resource-regen-down'
| 'opp-cleanse-cooldown-up'
| 'opp-purge-random-buff'
type Choice<T extends string> = { type Choice<T extends string> = {
id: T id: T
@@ -184,7 +177,7 @@ function slotLabel(slot: SlotKey, spells: Spell[], labelMode: AbilityLabelMode)
} }
function buildSelfBuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<SelfBuffId>> { function buildSelfBuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<SelfBuffId>> {
const slotChoices = (['1', '2', '3', '4', '5', '6'] as SlotKey[]).flatMap((slot) => { return (['1', '2', '3', '4', '5'] as DraftSlotKey[]).flatMap((slot) => {
const label = slotLabel(slot, spells, labelMode) const label = slotLabel(slot, spells, labelMode)
return [ return [
{ {
@@ -204,16 +197,10 @@ function buildSelfBuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Arr
}, },
] ]
}) })
return [
...slotChoices,
{ id: 'fifth-cast-free', name: 'Stored Momentum', description: 'After 5 casts, the next cast is free.' },
{ id: 'group-heal-boost', name: 'Wide Radiance', description: 'Party healing is 25% stronger.' },
{ id: 'shield-boost', name: 'Dense Shields', description: 'Shield absorbs are 25% stronger.' },
]
} }
function buildOpponentDebuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<OpponentDebuffId>> { function buildOpponentDebuffChoices(spells: Spell[], labelMode: AbilityLabelMode): Array<Choice<OpponentDebuffId>> {
const slotChoices = (['1', '2', '3', '4', '5', '6'] as SlotKey[]).flatMap((slot) => { return (['1', '2', '3', '4', '5'] as DraftSlotKey[]).flatMap((slot) => {
const label = slotLabel(slot, spells, labelMode) const label = slotLabel(slot, spells, labelMode)
return [ return [
{ {
@@ -228,14 +215,6 @@ function buildOpponentDebuffChoices(spells: Spell[], labelMode: AbilityLabelMode
}, },
] ]
}) })
return [
...slotChoices,
{ id: 'opp-takes-more-damage', name: 'Expose Weakness', description: 'Opponent takes 10% more damage.' },
{ id: 'opp-healing-reduced', name: 'Blunted Recovery', description: 'Opponent healing is 15% weaker.' },
{ id: 'opp-resource-regen-down', name: 'Mana Squeeze', description: 'Opponent resource regeneration is reduced by 25%.' },
{ id: 'opp-cleanse-cooldown-up', name: 'Lingering Toxins', description: 'Opponent cleanse cooldown is 25% longer.' },
{ id: 'opp-purge-random-buff', name: 'Strip Momentum', description: 'Remove 1 random buff from the opponent immediately.' },
]
} }
function toCombatSpell(ability: Ability, key: string): Spell { function toCombatSpell(ability: Ability, key: string): Spell {
@@ -263,17 +242,10 @@ function effectiveMaxHealth(member: PartyMember) {
return Math.max(1, Math.round(member.maxHealth * (member.maxHealthPenaltyTicks && member.maxHealthPenaltyTicks > 0 ? 0.75 : 1))) return Math.max(1, Math.round(member.maxHealth * (member.maxHealthPenaltyTicks && member.maxHealthPenaltyTicks > 0 ? 0.75 : 1)))
} }
function incomingDamageMultiplier(debuffs: OpponentDebuffId[]) {
return 1.1 ** buffStacks(debuffs, 'opp-takes-more-damage')
}
function outgoingHealMultiplier(debuffs: OpponentDebuffId[]) {
return 0.85 ** buffStacks(debuffs, 'opp-healing-reduced')
}
function healAmount(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) { function healAmount(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) {
const healingReduction = member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1 const healingReduction = member.healingReductionTicks && member.healingReductionTicks > 0 ? 0.75 : 1
return Math.round(amount * healingReduction * outgoingHealMultiplier(debuffs) * multiplier) void debuffs
return Math.round(amount * healingReduction * multiplier)
} }
function healMember(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) { function healMember(member: PartyMember, amount: number, debuffs: OpponentDebuffId[], multiplier = 1) {
@@ -284,8 +256,7 @@ function cooldownMultiplier(spell: Spell, buffs: SelfBuffId[], debuffs: Opponent
const slot = spell.key as SlotKey const slot = spell.key as SlotKey
const downStacks = buffStacks(buffs, `slot${slot}-cooldown-down` as SelfBuffId) const downStacks = buffStacks(buffs, `slot${slot}-cooldown-down` as SelfBuffId)
const upStacks = buffStacks(debuffs, `opp-slot${slot}-cooldown-up` as OpponentDebuffId) const upStacks = buffStacks(debuffs, `opp-slot${slot}-cooldown-up` as OpponentDebuffId)
const cleansePenalty = spell.kind === 'cleanse' ? buffStacks(debuffs, 'opp-cleanse-cooldown-up') : 0 return (0.75 ** downStacks) * (1.25 ** upStacks)
return (0.75 ** downStacks) * (1.25 ** (upStacks + cleansePenalty))
} }
function spellResourceCost(spell: Spell, buffs: SelfBuffId[], debuffs: OpponentDebuffId[], freeCastReady: boolean) { function spellResourceCost(spell: Spell, buffs: SelfBuffId[], debuffs: OpponentDebuffId[], freeCastReady: boolean) {
@@ -293,7 +264,8 @@ function spellResourceCost(spell: Spell, buffs: SelfBuffId[], debuffs: OpponentD
const downStacks = buffStacks(buffs, `slot${slot}-cost-down` as SelfBuffId) const downStacks = buffStacks(buffs, `slot${slot}-cost-down` as SelfBuffId)
const upStacks = buffStacks(debuffs, `opp-slot${slot}-cost-up` as OpponentDebuffId) const upStacks = buffStacks(debuffs, `opp-slot${slot}-cost-up` as OpponentDebuffId)
const adjustedCost = Math.ceil(spell.cost * (0.75 ** downStacks) * (1.25 ** upStacks)) const adjustedCost = Math.ceil(spell.cost * (0.75 ** downStacks) * (1.25 ** upStacks))
return freeCastReady && buffStacks(buffs, 'fifth-cast-free') > 0 ? 0 : adjustedCost void freeCastReady
return adjustedCost
} }
function buildEncounterSegment(pool: DungeonEncounter[], stage: number, kind: PvpContentType): PvpEncounter[] { function buildEncounterSegment(pool: DungeonEncounter[], stage: number, kind: PvpContentType): PvpEncounter[] {
@@ -351,9 +323,6 @@ function starterSide(partyTemplate: PartyMember[], maxResource: number): SideSta
} }
function scoreSelfBuff(buff: Choice<SelfBuffId>, spells: Spell[]) { function scoreSelfBuff(buff: Choice<SelfBuffId>, spells: Spell[]) {
if (buff.id === 'fifth-cast-free') return 8
if (buff.id === 'group-heal-boost') return 8
if (buff.id === 'shield-boost') return 6
const slot = buff.id.match(/slot([1-6])/i)?.[1] as SlotKey | undefined const slot = buff.id.match(/slot([1-6])/i)?.[1] as SlotKey | undefined
const spell = spells.find((candidate) => candidate.key === slot) const spell = spells.find((candidate) => candidate.key === slot)
if (!spell) return 5 if (!spell) return 5
@@ -367,11 +336,7 @@ function scoreSelfBuff(buff: Choice<SelfBuffId>, spells: Spell[]) {
} }
function scoreDebuff(debuff: Choice<OpponentDebuffId>, opponentBuffCount: number) { function scoreDebuff(debuff: Choice<OpponentDebuffId>, opponentBuffCount: number) {
if (debuff.id === 'opp-takes-more-damage') return 9 void opponentBuffCount
if (debuff.id === 'opp-healing-reduced') return 8
if (debuff.id === 'opp-resource-regen-down') return 7
if (debuff.id === 'opp-cleanse-cooldown-up') return 5
if (debuff.id === 'opp-purge-random-buff') return opponentBuffCount > 0 ? 8 : 2
if (debuff.id.endsWith('cost-up')) return 7 if (debuff.id.endsWith('cost-up')) return 7
return 6 return 6
} }
@@ -388,13 +353,6 @@ function selectCpuChoice<T extends string>(
return ranked[0] return ranked[0]
} }
function removeRandomBuff(side: SideState) {
if (side.buffs.length === 0) return side
const nextBuffs = [...side.buffs]
nextBuffs.splice(Math.floor(Math.random() * nextBuffs.length), 1)
return { ...side, buffs: nextBuffs }
}
function createLogEntry(nextLogId: { current: number }, text: string, tone: CombatLogEntry['tone']) { function createLogEntry(nextLogId: { current: number }, text: string, tone: CombatLogEntry['tone']) {
return { id: nextLogId.current++, text, tone } return { id: nextLogId.current++, text, tone }
} }
@@ -571,10 +529,11 @@ export function PvPRoguelikeScreen({
encounterPoolRef.current = encounterPool encounterPoolRef.current = encounterPool
}, [encounterPool]) }, [encounterPool])
const awardBossReward = useCallback((encounterIndexValue: number) => { const awardEncounterReward = useCallback((encounterIndexValue: number) => {
if (bossRewardClaimedRef.current.has(encounterIndexValue)) return if (bossRewardClaimedRef.current.has(encounterIndexValue)) return
bossRewardClaimedRef.current.add(encounterIndexValue) bossRewardClaimedRef.current.add(encounterIndexValue)
const rewardEncounter = encounters[encounterIndexValue] const rewardEncounter = encounters[encounterIndexValue]
const isBossReward = Boolean(rewardEncounter?.isBoss)
completeRoguelike( completeRoguelike(
rewardDungeon.id, rewardDungeon.id,
rewardDifficulty.id, rewardDifficulty.id,
@@ -582,10 +541,11 @@ export function PvPRoguelikeScreen({
0, 0,
Math.max(1, Math.round((elapsedTicks * TICK_MS) / 1000)), Math.max(1, Math.round((elapsedTicks * TICK_MS) / 1000)),
{ {
bossesCleared: 1, bossesCleared: isBossReward ? 1 : 0,
experienceMode: 'pvp-boss-quarter-level', fightsCleared: 1,
lootSourceEncounterId: rewardEncounter?.sourceEncounterId, experienceMode: isBossReward ? 'pvp-boss-quarter-level' : 'pvp-fight-twelfth-level',
roguelikeStage: stage, lootSourceEncounterId: isBossReward ? rewardEncounter?.sourceEncounterId : undefined,
roguelikeStage: isBossReward ? stage : undefined,
}, },
) )
.then((result) => { .then((result) => {
@@ -594,7 +554,7 @@ export function PvPRoguelikeScreen({
const unlockedById = new Map(current.unlockedAbilities.map((ability) => [ability.id, ability])) const unlockedById = new Map(current.unlockedAbilities.map((ability) => [ability.id, ability]))
result.unlockedAbilities.forEach((ability) => unlockedById.set(ability.id, ability)) result.unlockedAbilities.forEach((ability) => unlockedById.set(ability.id, ability))
return { return {
bossesKilled: current.bossesKilled + 1, bossesKilled: current.bossesKilled + (isBossReward ? 1 : 0),
experienceGained: current.experienceGained + result.experienceGained, experienceGained: current.experienceGained + result.experienceGained,
previousLevel: current.previousLevel ?? result.previousLevel, previousLevel: current.previousLevel ?? result.previousLevel,
newLevel: result.newLevel, newLevel: result.newLevel,
@@ -605,6 +565,9 @@ export function PvPRoguelikeScreen({
} }
}) })
onProfileUpdated(result.profile) onProfileUpdated(result.profile)
if (!isBossReward && result.experienceGained > 0) {
addLog(`+${result.experienceGained} XP awarded.`, 'loot')
}
if (result.bonusItem) { if (result.bonusItem) {
addLog( addLog(
`${result.bonusItem.name} x${result.bonusItem.quantity} awarded.`, `${result.bonusItem.name} x${result.bonusItem.quantity} awarded.`,
@@ -949,7 +912,7 @@ export function PvPRoguelikeScreen({
if (member.health <= 0) return member if (member.health <= 0) return member
if (spell.kind === 'group') { if (spell.kind === 'group') {
if (!groupTargets.has(member.id)) return member if (!groupTargets.has(member.id)) return member
const groupPower = Math.round(spell.power * (1.25 ** buffStacks(buffs, 'group-heal-boost'))) const groupPower = spell.power
const nextHealth = healMember(member, groupPower, debuffs, healingMultiplier(member)) const nextHealth = healMember(member, groupPower, debuffs, healingMultiplier(member))
addFloatingHeal(sideName, member.id, Math.max(0, nextHealth - member.health)) addFloatingHeal(sideName, member.id, Math.max(0, nextHealth - member.health))
const nextShield = hasSpellEffect('radiance_applies_shield') const nextShield = hasSpellEffect('radiance_applies_shield')
@@ -966,7 +929,7 @@ export function PvPRoguelikeScreen({
} }
if (!directTargets.has(member.id) && !hotTargets.has(member.id) && !shieldTargets.has(member.id)) return member if (!directTargets.has(member.id) && !hotTargets.has(member.id) && !shieldTargets.has(member.id)) return member
if (spell.kind === 'shield') { if (spell.kind === 'shield') {
const shieldPower = Math.round(spell.power * (1.25 ** buffStacks(buffs, 'shield-boost'))) const shieldPower = spell.power
return { return {
...member, ...member,
shield: Math.max(member.shield, shieldPower), shield: Math.max(member.shield, shieldPower),
@@ -1000,16 +963,6 @@ export function PvPRoguelikeScreen({
hotTicks: hotTargets.has(member.id) ? 5 : member.hotTicks, hotTicks: hotTargets.has(member.id) ? 5 : member.hotTicks,
} }
}) })
const freeCastStacks = buffStacks(buffs, 'fifth-cast-free')
const nextFreeCastReady = freeCastStacks > 0 && current.freeCastReady ? false : current.freeCastReady
const nextCastsTowardFree = freeCastStacks > 0
? current.freeCastReady
? 0
: current.castsTowardFree + 1 >= 5
? 0
: current.castsTowardFree + 1
: current.castsTowardFree
const gainedFreeCast = freeCastStacks > 0 && !current.freeCastReady && current.castsTowardFree + 1 >= 5
const nextCooldowns = { const nextCooldowns = {
...current.cooldowns, ...current.cooldowns,
} }
@@ -1022,8 +975,8 @@ export function PvPRoguelikeScreen({
party: nextParty, party: nextParty,
resource: current.resource - effectiveCost, resource: current.resource - effectiveCost,
cooldowns: nextCooldowns, cooldowns: nextCooldowns,
castsTowardFree: nextCastsTowardFree, castsTowardFree: current.castsTowardFree,
freeCastReady: gainedFreeCast || nextFreeCastReady, freeCastReady: false,
} }
setCurrent(nextState) setCurrent(nextState)
return true return true
@@ -1134,7 +1087,6 @@ export function PvPRoguelikeScreen({
const appliesMaxHealthCut = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 13 === 0 && mechanics.includes('max-health-cut') const appliesMaxHealthCut = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 13 === 0 && mechanics.includes('max-health-cut')
const appliesHealingReduction = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 9 === 0 && mechanics.includes('healing-reduction') const appliesHealingReduction = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 9 === 0 && mechanics.includes('healing-reduction')
const appliesPoison = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 12 === 0 && mechanics.includes('ramping-poison') const appliesPoison = encounterValue.isBoss && elapsedTicks > 0 && elapsedTicks % 12 === 0 && mechanics.includes('ramping-poison')
const damageMultiplier = incomingDamageMultiplier(side.debuffs)
const hasSpellEffect = (effectType: string) => sideName === 'player' && activeSpellEffects.has(effectType) const hasSpellEffect = (effectType: string) => sideName === 'player' && activeSpellEffects.has(effectType)
const tankPressure = tankPressureTargets(side.party) const tankPressure = tankPressureTargets(side.party)
const tankPressureIds = new Set(tankPressure.targets.map((member) => member.id)) const tankPressureIds = new Set(tankPressure.targets.map((member) => member.id))
@@ -1150,7 +1102,6 @@ export function PvPRoguelikeScreen({
? Math.max(1, (member.poisonStacks ?? 0) + 1) ? Math.max(1, (member.poisonStacks ?? 0) + 1)
: member.poisonStacks ?? 0 : member.poisonStacks ?? 0
if (nextPoisonStacks > 0) damage += 3 + nextPoisonStacks * 3 if (nextPoisonStacks > 0) damage += 3 + nextPoisonStacks * 3
damage = Math.round(damage * damageMultiplier)
if (member.shield > 0 && hasSpellEffect('shielded_damage_reduction')) { if (member.shield > 0 && hasSpellEffect('shielded_damage_reduction')) {
damage = Math.round(damage * 0.8) damage = Math.round(damage * 0.8)
} }
@@ -1189,7 +1140,7 @@ export function PvPRoguelikeScreen({
...side, ...side,
party: nextParty, party: nextParty,
resource: clamp( resource: clamp(
side.resource + 2.4 * (0.75 ** buffStacks(side.debuffs, 'opp-resource-regen-down')), side.resource + 2.4,
0, 0,
maxResource, maxResource,
), ),
@@ -1221,8 +1172,8 @@ export function PvPRoguelikeScreen({
if (nextPlayer.enemyHealth <= 0 && playerClearedEncounterRef.current !== encounterIndex) { if (nextPlayer.enemyHealth <= 0 && playerClearedEncounterRef.current !== encounterIndex) {
playerClearedEncounterRef.current = encounterIndex playerClearedEncounterRef.current = encounterIndex
setEncountersCleared((value) => value + 1) setEncountersCleared((value) => value + 1)
awardEncounterReward(encounterIndex)
if (encounter.isBoss) { if (encounter.isBoss) {
awardBossReward(encounterIndex)
const nextCheckpoint = recordPvpRoguelikeCheckpoint( const nextCheckpoint = recordPvpRoguelikeCheckpoint(
profile.character.id, profile.character.id,
contentType, contentType,
@@ -1266,7 +1217,7 @@ export function PvPRoguelikeScreen({
} }
}, TICK_MS / speedMultiplier) }, TICK_MS / speedMultiplier)
return () => window.clearInterval(timer) return () => window.clearInterval(timer)
}, [addLog, advanceSide, awardBossReward, beginUpgradePhase, checkpointStage, contentType, cpuDifficulty, cpuTakeTurn, encounter, encounterIndex, encountersCleared, finishRoguelikeRun, liveMatch, paused, profile.character.id, speedMultiplier, stage, status]) }, [addLog, advanceSide, awardEncounterReward, beginUpgradePhase, checkpointStage, contentType, cpuDifficulty, cpuTakeTurn, encounter, encounterIndex, encountersCleared, finishRoguelikeRun, liveMatch, paused, profile.character.id, speedMultiplier, stage, status])
useEffect(() => { useEffect(() => {
if ((status !== 'won' && status !== 'lost') || recordedRunRef.current || !cpuDifficulty) return if ((status !== 'won' && status !== 'lost') || recordedRunRef.current || !cpuDifficulty) return
@@ -1348,9 +1299,7 @@ export function PvPRoguelikeScreen({
...playerRef.current, ...playerRef.current,
buffs: [...playerRef.current.buffs, submittedBuff.id], buffs: [...playerRef.current.buffs, submittedBuff.id],
} }
if (opponentChoice.debuffId === 'opp-purge-random-buff') { if (opponentDebuffChoicesCatalog.some((choice) => choice.id === opponentChoice.debuffId)) {
nextPlayer = removeRandomBuff(nextPlayer)
} else if (opponentDebuffChoicesCatalog.some((choice) => choice.id === opponentChoice.debuffId)) {
nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, opponentChoice.debuffId as OpponentDebuffId] } nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, opponentChoice.debuffId as OpponentDebuffId] }
} }
@@ -1457,17 +1406,9 @@ export function PvPRoguelikeScreen({
buffs: [...cpuRef.current.buffs, cpuBuff.id], buffs: [...cpuRef.current.buffs, cpuBuff.id],
} }
if (chosenDebuff.id === 'opp-purge-random-buff') {
nextCpu = removeRandomBuff(nextCpu)
} else {
nextCpu = { ...nextCpu, debuffs: [...nextCpu.debuffs, chosenDebuff.id] } nextCpu = { ...nextCpu, debuffs: [...nextCpu.debuffs, chosenDebuff.id] }
}
if (cpuDebuff.id === 'opp-purge-random-buff') {
nextPlayer = removeRandomBuff(nextPlayer)
} else {
nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, cpuDebuff.id] } nextPlayer = { ...nextPlayer, debuffs: [...nextPlayer.debuffs, cpuDebuff.id] }
}
const clearedBoss = encounter.isBoss const clearedBoss = encounter.isBoss
if (clearedBoss && cpuDefeatedRef.current) { if (clearedBoss && cpuDefeatedRef.current) {
+35 -1
View File
@@ -37,7 +37,8 @@ export interface GameRepository {
durationSeconds: number, durationSeconds: number,
options?: { options?: {
bossesCleared?: number bossesCleared?: number
experienceMode?: 'default' | 'pvp-boss-quarter-level' experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level'
fightsCleared?: number
lootSourceEncounterId?: number lootSourceEncounterId?: number
roguelikeStage?: number roguelikeStage?: number
}, },
@@ -503,6 +504,31 @@ function scaledPvpBossExperience(
return { experience, level } return { experience, level }
} }
function scaledPvpFightExperience(
startingExperience: number,
startingLevel: number,
fightsCleared: number,
maxLevel: number,
targetLevel = startingLevel,
) {
let experience = startingExperience
let level = startingLevel
const maxExperience = experienceForLevel(maxLevel)
for (let fightIndex = 0; fightIndex < fightsCleared && experience < maxExperience; fightIndex += 1) {
const currentLevelFloor = experienceForLevel(level)
const nextLevelExperience = level >= maxLevel
? maxExperience
: experienceForLevel(level + 1)
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
const rewardRate = targetLevel > level ? 1 / 6 : 1 / 12
experience = Math.min(maxExperience, experience + Math.round(levelBand * rewardRate))
while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
level += 1
}
}
return { experience, level }
}
function talentEffectCapacity(level: number) { function talentEffectCapacity(level: number) {
return Math.min(4, Math.max(0, Math.floor(level / 5))) return Math.min(4, Math.max(0, Math.floor(level / 5)))
} }
@@ -1142,6 +1168,14 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
const bossesCleared = Math.max(0, Math.floor(options?.bossesCleared ?? encountersCleared / 3)) const bossesCleared = Math.max(0, Math.floor(options?.bossesCleared ?? encountersCleared / 3))
const scaledReward = options?.experienceMode === 'pvp-boss-quarter-level' const scaledReward = options?.experienceMode === 'pvp-boss-quarter-level'
? scaledPvpBossExperience(previousExperience, previousLevel, bossesCleared, profile.maxLevel, highestOtherClassLevel(save)) ? scaledPvpBossExperience(previousExperience, previousLevel, bossesCleared, profile.maxLevel, highestOtherClassLevel(save))
: options?.experienceMode === 'pvp-fight-twelfth-level'
? scaledPvpFightExperience(
previousExperience,
previousLevel,
Math.max(0, Math.floor(options.fightsCleared ?? encountersCleared)),
profile.maxLevel,
highestOtherClassLevel(save),
)
: null : null
const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3)) const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3))
const newExperience = scaledReward const newExperience = scaledReward
+2 -1
View File
@@ -349,7 +349,8 @@ export async function completeRoguelike(
durationSeconds: number, durationSeconds: number,
options?: { options?: {
bossesCleared?: number bossesCleared?: number
experienceMode?: 'default' | 'pvp-boss-quarter-level' experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level'
fightsCleared?: number
lootSourceEncounterId?: number lootSourceEncounterId?: number
roguelikeStage?: number roguelikeStage?: number
}, },