Android build v1.1.2

This commit is contained in:
Warren H
2026-06-29 22:35:49 -04:00
parent cbe42b6164
commit c6251167a5
52 changed files with 5554 additions and 3117 deletions
+30 -100
View File
@@ -1,5 +1,14 @@
import starterProfile from './offline-starter-profile.json'
import { bundledCatalogHash } from './offline-catalog-meta'
import {
catchUpExperienceReward as catchUpExperienceRewardForTarget,
coinDropQuantity,
experienceForLevel,
roguelikeCoinItemLevel,
scaledCurrentLevelExperience,
scaledPvpBossExperience,
scaledPvpFightExperience,
} from './shared/rewardRules.mjs'
import type {
Account,
AuthSession,
@@ -37,7 +46,7 @@ export interface GameRepository {
durationSeconds: number,
options?: {
bossesCleared?: number
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level' | 'pvp-stadium-round-win-quarter-level' | 'pvp-stadium-round-loss-tenth-level' | 'pvp-stadium-match-half-level'
experienceMode?: 'default' | 'pvp-boss-quarter-level' | 'pvp-fight-twelfth-level' | 'pvp-match-win-half-level' | 'pvp-stadium-round-win-quarter-level' | 'pvp-stadium-round-loss-tenth-level' | 'pvp-stadium-match-half-level'
fightsCleared?: number
lootSourceEncounterId?: number
roguelikeStage?: number
@@ -115,6 +124,7 @@ const catalogBundleKey = 'chronicle.catalog.bundleHash.v1'
const authTokenKey = 'chronicle.authToken.v1'
const offlineAccount = { id: -1, username: 'Offline' }
const ABILITY_SLOT_COUNT = 6
let activeCatalogCache: CatalogCache | null = null
function clone<T>(value: T): T {
return structuredClone(value)
@@ -303,6 +313,7 @@ function readCatalogCache(): CatalogCache | null {
if (localStorage.getItem(catalogBundleKey) !== bundledCatalogHash) {
localStorage.removeItem(catalogCacheKey)
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
activeCatalogCache = null
return null
}
const serialized = localStorage.getItem(catalogCacheKey)
@@ -319,10 +330,13 @@ function readCatalogCache(): CatalogCache | null {
function writeCatalogCache(cache: CatalogCache) {
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
localStorage.setItem(catalogCacheKey, JSON.stringify(cache))
activeCatalogCache = cache
}
function activeCatalog(): CatalogCache {
return readCatalogCache() ?? bundledCatalog()
if (activeCatalogCache) return activeCatalogCache
activeCatalogCache = readCatalogCache() ?? bundledCatalog()
return activeCatalogCache
}
function buildProfile(save: OfflineSave): CharacterProfile {
@@ -454,10 +468,6 @@ function selectUpgradeRecipe(
return candidates.find((recipe) => recipe.item.itemLevel === nextItemLevel)
}
function experienceForLevel(level: number) {
return (level - 1) * (level - 1) * 100
}
function catchUpExperienceReward(
baseReward: number,
currentExperience: number,
@@ -465,11 +475,11 @@ function catchUpExperienceReward(
targetLevel: number,
) {
if (targetLevel <= currentLevel) return baseReward
const targetExperience = experienceForLevel(targetLevel)
const gap = Math.max(0, targetExperience - currentExperience)
if (gap <= 0) return baseReward
const doubledBase = Math.min(baseReward, Math.ceil(gap / 2))
return doubledBase * 2 + (baseReward - doubledBase)
return catchUpExperienceRewardForTarget(
baseReward,
currentExperience,
experienceForLevel(targetLevel),
)
}
function highestOtherClassLevel(save: OfflineSave) {
@@ -479,77 +489,6 @@ function highestOtherClassLevel(save: OfflineSave) {
.reduce((highest, [, character]) => Math.max(highest, character.level), 0)
}
function scaledPvpBossExperience(
startingExperience: number,
startingLevel: number,
bossesCleared: number,
maxLevel: number,
targetLevel = startingLevel,
) {
let experience = startingExperience
let level = startingLevel
const maxExperience = experienceForLevel(maxLevel)
for (let bossIndex = 0; bossIndex < bossesCleared && experience < maxExperience; bossIndex += 1) {
const currentLevelFloor = experienceForLevel(level)
const nextLevelExperience = level >= maxLevel
? maxExperience
: experienceForLevel(level + 1)
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
const rewardRate = targetLevel > level ? 0.5 : 0.25
experience = Math.min(maxExperience, experience + Math.round(levelBand * rewardRate))
while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
level += 1
}
}
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 scaledCurrentLevelExperience(
startingExperience: number,
startingLevel: number,
maxLevel: number,
rate: number,
) {
let experience = startingExperience
let level = startingLevel
const maxExperience = experienceForLevel(maxLevel)
const currentLevelFloor = experienceForLevel(level)
const nextLevelExperience = level >= maxLevel
? maxExperience
: experienceForLevel(level + 1)
const levelBand = Math.max(1, nextLevelExperience - currentLevelFloor)
experience = Math.min(maxExperience, experience + Math.round(levelBand * rate))
while (level < maxLevel && experienceForLevel(level + 1) <= experience) {
level += 1
}
return { experience, level }
}
function talentEffectCapacity(level: number) {
return Math.min(4, Math.max(0, Math.floor(level / 5)))
}
@@ -631,17 +570,6 @@ function rollWeightedLootEntry<T extends { dropWeight: number }>(entries: T[]):
return entries[entries.length - 1]
}
function coinDropQuantity() {
const roll = Math.random()
if (roll < 0.15) return 3
if (roll < 0.5) return 2
return 1
}
function roguelikeCoinItemLevel(stage: number) {
return Math.min(25, 5 + Math.max(0, Math.floor(stage / 5)) * 5)
}
function awardRoguelikeCoin(
profile: CharacterProfile,
sourceEncounterId: number | undefined,
@@ -1197,13 +1125,15 @@ function createLocalRepository(store: LocalSaveStore): GameRepository {
profile.maxLevel,
highestOtherClassLevel(save),
)
: options?.experienceMode === 'pvp-stadium-round-win-quarter-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.25)
: options?.experienceMode === 'pvp-stadium-round-loss-tenth-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.1)
: options?.experienceMode === 'pvp-stadium-match-half-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.5)
: null
: options?.experienceMode === 'pvp-match-win-half-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.5)
: options?.experienceMode === 'pvp-stadium-round-win-quarter-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.25)
: options?.experienceMode === 'pvp-stadium-round-loss-tenth-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.1)
: options?.experienceMode === 'pvp-stadium-match-half-level'
? scaledCurrentLevelExperience(previousExperience, previousLevel, profile.maxLevel, 0.5)
: null
const baseRoguelikeReward = Math.round(dungeon.experienceReward * difficulty.experienceMultiplier * (encountersCleared / 3))
const newExperience = scaledReward
? scaledReward.experience