diff --git a/IWantToHeal-Thor-v1.1.35.apk b/IWantToHeal-Thor-v1.1.35.apk new file mode 100644 index 0000000..eac6f64 Binary files /dev/null and b/IWantToHeal-Thor-v1.1.35.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index db5c189..0240e9c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 115 - versionName "1.1.34" + versionCode 116 + versionName "1.1.35" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/scripts/iwt2-pvp-roguelike-boss-sim.mjs b/scripts/iwt2-pvp-roguelike-boss-sim.mjs index 00dfb60..20ea40e 100644 --- a/scripts/iwt2-pvp-roguelike-boss-sim.mjs +++ b/scripts/iwt2-pvp-roguelike-boss-sim.mjs @@ -4,10 +4,30 @@ import { createServer } from 'vite' const DEFAULT_WORKERS = Math.max(1, Math.min(8, Number.parseInt(process.env.IWT2_SIM_WORKERS ?? '8', 10))) const DEFAULT_MAX_SECONDS = Math.max(30, Number.parseFloat(process.env.IWT2_SIM_SECONDS ?? '180')) const DEFAULT_DT = 1 / 30 +const DEFAULT_TICKS_PER_WORKER_SECOND = 45000 +const ESTIMATE_STARTUP_SECONDS = 0.8 const PVP_BOSS_HEALTH_MULTIPLIER = 0.7 const BOSS_HEALTH_PER_STAGE = 0.1 const ARENA_BOUNDS = { width: 960, height: 540 } const HEALER_STYLES = ['dawnweaver', 'lifebinder', 'runesage'] +const BOSS_IDS = [ + 'bulldrome', + 'yian-kut-ku', + 'great-jaggi', + 'khezu', + 'rathian', + 'barroth', + 'tobi-kadachi', + 'rimebastion', + 'ember-mantis-duelist', + 'cinderback-ricochet', + 'obsidian-ram-golem', + 'stormcoil-wyrm', + 'venom-orchid-hydra', + 'sandglass-scorpion', + 'crystal-bat-matriarch', + 'hollowcrown-revenant', +] const activeChildren = new Set() for (const signal of ['SIGINT', 'SIGTERM']) { @@ -32,6 +52,7 @@ const config = { bossFilter: parseList(args.get('--bosses') ?? 'all'), bossHpPercent: parsePositiveNumber(args.get('--boss-hp-percent') ?? '100', 100), classes: parseList(args.get('--classes') ?? args.get('--class') ?? 'all'), + estimateOnly: args.has('--estimate-only'), gearLevel: parseNonNegativeInt(args.get('--gear-level') ?? args.get('--gear') ?? '5', 5), maxSeconds: Number.parseFloat(args.get('--seconds') ?? String(DEFAULT_MAX_SECONDS)), repeats: parsePositiveInt(args.get('--repeats') ?? '1', 1), @@ -66,6 +87,7 @@ async function runMain({ bossFilter, bossHpPercent, classes, + estimateOnly, gearLevel, maxSeconds, repeats, @@ -75,6 +97,33 @@ async function runMain({ workers, }) { const safeWorkers = Math.max(1, Math.floor(workers)) + const estimate = estimateRun({ + bossCount, + bossFilter, + classes, + maxSeconds, + repeats, + requiredBosses, + stages, + workers: safeWorkers, + }) + if (estimateOnly) { + console.log(JSON.stringify({ + config: { + bossCount, + bosses: bossFilter, + classes, + repeats, + requiredBosses, + seconds: maxSeconds, + stages, + workers: safeWorkers, + }, + estimate, + }, null, 2)) + return + } + const startedAt = Date.now() const childResults = await Promise.all(Array.from({ length: safeWorkers }, (_, shardIndex) => ( runChild({ bossCount, @@ -92,6 +141,7 @@ async function runMain({ }) ))) const results = childResults.flat() + const elapsedSeconds = round((Date.now() - startedAt) / 1000) const summary = summarizeResults(results) console.log(JSON.stringify({ config: { @@ -101,6 +151,8 @@ async function runMain({ bosses: bossFilter, classes, gearLevel, + elapsedSeconds, + estimate, orderedTrials: results.length, repeats, requiredBosses, @@ -549,6 +601,7 @@ Options: --bosses all|bulldrome,yian-kut-ku,... --required-bosses none|yian-kut-ku,... --repeats 1 + --estimate-only --boss-hp-percent 100 --boss-damage-percent 100 --stages 1,2 @@ -562,9 +615,43 @@ Examples: node scripts/iwt2-pvp-roguelike-boss-sim.mjs --boss-hp-percent 125 --boss-damage-percent 110 node scripts/iwt2-pvp-roguelike-boss-sim.mjs --bosses yian-kut-ku,rathian,rimebastion --boss-count 2 node scripts/iwt2-pvp-roguelike-boss-sim.mjs --required-bosses yian-kut-ku --repeats 10 + node scripts/iwt2-pvp-roguelike-boss-sim.mjs --required-bosses yian-kut-ku --estimate-only `) } +function estimateRun({ + bossCount, + bossFilter, + classes, + maxSeconds, + repeats, + requiredBosses, + stages, + workers, +}) { + const bossIds = filteredBossIds(BOSS_IDS, bossFilter) + const requiredBossIds = filteredRequiredBossIds(bossIds, requiredBosses) + const healerStyles = filteredClasses(classes) + const orderedBossGroups = permutations(bossIds, bossCount) + .filter((group) => requiredBossIds.every((bossId) => group.includes(bossId))) + const trials = orderedBossGroups.length * healerStyles.length * stages.length * repeats + const maxTicks = trials * maxSeconds * (1 / DEFAULT_DT) + const estimatedWallSeconds = ESTIMATE_STARTUP_SECONDS + (maxTicks / (Math.max(1, workers) * DEFAULT_TICKS_PER_WORKER_SECOND)) + return { + bossPoolSize: bossIds.length, + classCount: healerStyles.length, + orderedBossGroupCount: orderedBossGroups.length, + requiredBosses: requiredBossIds, + stageCount: stages.length, + repeats, + trials, + maxSimulatedSeconds: round(trials * maxSeconds), + maxTicks: Math.round(maxTicks), + estimatedWallSeconds: round(estimatedWallSeconds), + note: 'Rough wall-time estimate; victories or defeats can end trials early.', + } +} + function filteredBossIds(allBossIds, filter) { if (filter.length === 0 || filter.includes('all')) return allBossIds const known = new Set(allBossIds) diff --git a/scripts/iwt2-pvp-sim-gui-server.mjs b/scripts/iwt2-pvp-sim-gui-server.mjs index 60ec5d2..13aaa95 100644 --- a/scripts/iwt2-pvp-sim-gui-server.mjs +++ b/scripts/iwt2-pvp-sim-gui-server.mjs @@ -169,6 +169,9 @@ function sendHtml(response) { .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .bosses { max-height: 300px; overflow: auto; border: 1px solid #2a3039; border-radius: 6px; padding: 8px; background: #111419; } .buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 16px; } + .estimate { display: grid; gap: 6px; background: #111419; border: 1px solid #2a3039; border-radius: 6px; color: #d9e3ee; font-size: 13px; margin-top: 16px; padding: 10px; } + .estimate strong { font-size: 18px; } + .estimate span { color: #96a3b5; } button { border: 1px solid #3a4656; color: #eef2f6; background: #202733; border-radius: 6px; padding: 9px 10px; cursor: pointer; } button.primary { background: #245a7a; border-color: #327aa3; } button:disabled { opacity: .55; cursor: not-allowed; } @@ -232,6 +235,8 @@ function sendHtml(response) {