Compare commits

...
3 Commits
Author SHA1 Message Date
Warren H f48e6130b8 Android build v1.1.30 2026-07-06 15:35:18 -04:00
Warren H 61de17d63e Android build v1.1.29 2026-07-06 15:31:42 -04:00
Warren H 5629041879 Android build v1.1.29 2026-07-06 15:30:04 -04:00
17 changed files with 174 additions and 7 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 108
versionName "1.1.28"
versionCode 111
versionName "1.1.30"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+12 -5
View File
@@ -46,6 +46,11 @@ import {
type Iwt2RoguelikeSelfBuffId,
type Iwt2RoguelikeVariant,
} from './content/roguelike'
import {
createIwt2PveRoguelikeBossPair,
createUniformIwt2RoguelikeBossPair,
IWT2_PVE_ROGUELIKE_WEIGHTED_BOSS_PROGRESSION_ENABLED,
} from './content/roguelikeBossProgression'
type Iwt2Screen =
| 'menu'
@@ -522,7 +527,7 @@ function createRoguelikeRun(
contentType: Iwt2RoguelikeContentType,
): Iwt2RoguelikeRunState {
return {
bossIds: createRandomRoguelikeBossPair(),
bossIds: createRoguelikeBossPair(variant, 1),
buffs: [],
contentType,
debuffs: [],
@@ -590,15 +595,17 @@ function applyRoguelikeChoice(
return {
...run,
...nextBase,
bossIds: createRandomRoguelikeBossPair(),
bossIds: createRoguelikeBossPair(run.variant, run.stage + 1),
...buildRoguelikeChoices(save, run.variant),
stage: run.stage + 1,
}
}
function createRandomRoguelikeBossPair(): Iwt2BossId[] {
const pool = Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]
return chooseRunChoices(pool, 2)
function createRoguelikeBossPair(variant: Iwt2RoguelikeVariant, stage: number): Iwt2BossId[] {
if (variant === 'pve' && IWT2_PVE_ROGUELIKE_WEIGHTED_BOSS_PROGRESSION_ENABLED) {
return createIwt2PveRoguelikeBossPair(stage)
}
return createUniformIwt2RoguelikeBossPair()
}
function chooseRunChoices<T>(items: readonly T[], count: number): T[] {
@@ -0,0 +1,160 @@
import { IWT2_BOSS_METADATA, type Iwt2BossId } from './bosses'
export const IWT2_PVE_ROGUELIKE_WEIGHTED_BOSS_PROGRESSION_ENABLED = true
type Iwt2RoguelikeBossTier = 'early' | 'mid' | 'late'
type TierWeight = {
tier: Iwt2RoguelikeBossTier
weight: number
}
const IWT2_ROGUELIKE_BOSS_TIERS: Record<Iwt2RoguelikeBossTier, readonly Iwt2BossId[]> = {
early: ['bulldrome', 'yian-kut-ku', 'great-jaggi', 'rathian', 'stormcoil-wyrm'],
mid: ['khezu', 'barroth', 'tobi-kadachi', 'ember-mantis-duelist', 'crystal-bat-matriarch', 'hollowcrown-revenant'],
late: ['rimebastion', 'cinderback-ricochet', 'obsidian-ram-golem', 'venom-orchid-hydra', 'sandglass-scorpion'],
}
const IWT2_ROGUELIKE_BOSS_THREAT: Record<Iwt2RoguelikeBossTier, number> = {
early: 1,
mid: 2,
late: 3,
}
const IWT2_ROGUELIKE_BOSS_TIER_BY_ID: Record<Iwt2BossId, Iwt2RoguelikeBossTier> = {
bulldrome: 'early',
'yian-kut-ku': 'early',
'great-jaggi': 'early',
rathian: 'early',
'stormcoil-wyrm': 'early',
khezu: 'mid',
barroth: 'mid',
'tobi-kadachi': 'mid',
'ember-mantis-duelist': 'mid',
'crystal-bat-matriarch': 'mid',
'hollowcrown-revenant': 'mid',
rimebastion: 'late',
'cinderback-ricochet': 'late',
'obsidian-ram-golem': 'late',
'venom-orchid-hydra': 'late',
'sandglass-scorpion': 'late',
}
export function createIwt2PveRoguelikeBossPair(
stage: number,
random: () => number = Math.random,
): Iwt2BossId[] {
const choices: Iwt2BossId[] = []
const maxThreat = maxThreatForStage(stage)
while (choices.length < 2) {
const next = chooseWeightedBossForStage(stage, choices, maxThreat, random)
if (!next) break
choices.push(next)
}
return choices.length === 2
? choices
: createUniformIwt2RoguelikeBossPair(random)
}
export function createUniformIwt2RoguelikeBossPair(random: () => number = Math.random): Iwt2BossId[] {
const pool = Object.keys(IWT2_BOSS_METADATA) as Iwt2BossId[]
const choices: Iwt2BossId[] = []
while (pool.length > 0 && choices.length < 2) {
const index = randomIndex(pool.length, random)
const [choice] = pool.splice(index, 1)
if (choice) choices.push(choice)
}
return choices
}
function chooseWeightedBossForStage(
stage: number,
selected: readonly Iwt2BossId[],
maxThreat: number,
random: () => number,
): Iwt2BossId | undefined {
const selectedSet = new Set(selected)
const selectedThreat = selected.reduce((total, bossId) => total + threatForBoss(bossId), 0)
const weightedTiers = tierWeightsForStage(stage)
.map((entry) => ({
...entry,
bosses: IWT2_ROGUELIKE_BOSS_TIERS[entry.tier].filter((bossId) => (
!selectedSet.has(bossId) && selectedThreat + threatForBoss(bossId) <= maxThreat
)),
}))
.filter((entry) => entry.weight > 0 && entry.bosses.length > 0)
const totalWeight = weightedTiers.reduce((total, entry) => total + entry.weight, 0)
if (totalWeight <= 0) return undefined
let roll = safeRandom(random) * totalWeight
for (const entry of weightedTiers) {
roll -= entry.weight
if (roll <= 0) {
return entry.bosses[randomIndex(entry.bosses.length, random)]
}
}
const lastEntry = weightedTiers[weightedTiers.length - 1]
return lastEntry?.bosses[randomIndex(lastEntry.bosses.length, random)]
}
function tierWeightsForStage(stage: number): TierWeight[] {
const safeStage = Math.max(1, Math.floor(stage))
if (safeStage <= 2) {
return [
{ tier: 'early', weight: 85 },
{ tier: 'mid', weight: 15 },
{ tier: 'late', weight: 0 },
]
}
if (safeStage === 3) {
return [
{ tier: 'early', weight: 60 },
{ tier: 'mid', weight: 35 },
{ tier: 'late', weight: 5 },
]
}
if (safeStage <= 5) {
return [
{ tier: 'early', weight: 25 },
{ tier: 'mid', weight: 60 },
{ tier: 'late', weight: 15 },
]
}
if (safeStage === 6) {
return [
{ tier: 'early', weight: 10 },
{ tier: 'mid', weight: 50 },
{ tier: 'late', weight: 40 },
]
}
return [
{ tier: 'early', weight: 5 },
{ tier: 'mid', weight: 30 },
{ tier: 'late', weight: 65 },
]
}
function maxThreatForStage(stage: number): number {
const safeStage = Math.max(1, Math.floor(stage))
if (safeStage <= 2) return 3
if (safeStage === 3) return 4
if (safeStage <= 5) return 5
return 6
}
function threatForBoss(bossId: Iwt2BossId): number {
return IWT2_ROGUELIKE_BOSS_THREAT[IWT2_ROGUELIKE_BOSS_TIER_BY_ID[bossId]]
}
function randomIndex(length: number, random: () => number): number {
return Math.min(length - 1, Math.floor(safeRandom(random) * length))
}
function safeRandom(random: () => number): number {
const value = random()
return Number.isFinite(value) ? Math.min(0.999999999, Math.max(0, value)) : 0
}