Android build v1.1.35

This commit is contained in:
Warren H
2026-07-08 22:30:44 -04:00
parent d61cee55ed
commit 802dadc7f3
4 changed files with 187 additions and 5 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 115 versionCode 116
versionName "1.1.34" versionName "1.1.35"
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.
+87
View File
@@ -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_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_MAX_SECONDS = Math.max(30, Number.parseFloat(process.env.IWT2_SIM_SECONDS ?? '180'))
const DEFAULT_DT = 1 / 30 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 PVP_BOSS_HEALTH_MULTIPLIER = 0.7
const BOSS_HEALTH_PER_STAGE = 0.1 const BOSS_HEALTH_PER_STAGE = 0.1
const ARENA_BOUNDS = { width: 960, height: 540 } const ARENA_BOUNDS = { width: 960, height: 540 }
const HEALER_STYLES = ['dawnweaver', 'lifebinder', 'runesage'] 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() const activeChildren = new Set()
for (const signal of ['SIGINT', 'SIGTERM']) { for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -32,6 +52,7 @@ const config = {
bossFilter: parseList(args.get('--bosses') ?? 'all'), bossFilter: parseList(args.get('--bosses') ?? 'all'),
bossHpPercent: parsePositiveNumber(args.get('--boss-hp-percent') ?? '100', 100), bossHpPercent: parsePositiveNumber(args.get('--boss-hp-percent') ?? '100', 100),
classes: parseList(args.get('--classes') ?? args.get('--class') ?? 'all'), 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), gearLevel: parseNonNegativeInt(args.get('--gear-level') ?? args.get('--gear') ?? '5', 5),
maxSeconds: Number.parseFloat(args.get('--seconds') ?? String(DEFAULT_MAX_SECONDS)), maxSeconds: Number.parseFloat(args.get('--seconds') ?? String(DEFAULT_MAX_SECONDS)),
repeats: parsePositiveInt(args.get('--repeats') ?? '1', 1), repeats: parsePositiveInt(args.get('--repeats') ?? '1', 1),
@@ -66,6 +87,7 @@ async function runMain({
bossFilter, bossFilter,
bossHpPercent, bossHpPercent,
classes, classes,
estimateOnly,
gearLevel, gearLevel,
maxSeconds, maxSeconds,
repeats, repeats,
@@ -75,6 +97,33 @@ async function runMain({
workers, workers,
}) { }) {
const safeWorkers = Math.max(1, Math.floor(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) => ( const childResults = await Promise.all(Array.from({ length: safeWorkers }, (_, shardIndex) => (
runChild({ runChild({
bossCount, bossCount,
@@ -92,6 +141,7 @@ async function runMain({
}) })
))) )))
const results = childResults.flat() const results = childResults.flat()
const elapsedSeconds = round((Date.now() - startedAt) / 1000)
const summary = summarizeResults(results) const summary = summarizeResults(results)
console.log(JSON.stringify({ console.log(JSON.stringify({
config: { config: {
@@ -101,6 +151,8 @@ async function runMain({
bosses: bossFilter, bosses: bossFilter,
classes, classes,
gearLevel, gearLevel,
elapsedSeconds,
estimate,
orderedTrials: results.length, orderedTrials: results.length,
repeats, repeats,
requiredBosses, requiredBosses,
@@ -549,6 +601,7 @@ Options:
--bosses all|bulldrome,yian-kut-ku,... --bosses all|bulldrome,yian-kut-ku,...
--required-bosses none|yian-kut-ku,... --required-bosses none|yian-kut-ku,...
--repeats 1 --repeats 1
--estimate-only
--boss-hp-percent 100 --boss-hp-percent 100
--boss-damage-percent 100 --boss-damage-percent 100
--stages 1,2 --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 --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 --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 --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) { function filteredBossIds(allBossIds, filter) {
if (filter.length === 0 || filter.includes('all')) return allBossIds if (filter.length === 0 || filter.includes('all')) return allBossIds
const known = new Set(allBossIds) const known = new Set(allBossIds)
+98 -3
View File
@@ -169,6 +169,9 @@ function sendHtml(response) {
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .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; } .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; } .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 { border: 1px solid #3a4656; color: #eef2f6; background: #202733; border-radius: 6px; padding: 9px 10px; cursor: pointer; }
button.primary { background: #245a7a; border-color: #327aa3; } button.primary { background: #245a7a; border-color: #327aa3; }
button:disabled { opacity: .55; cursor: not-allowed; } button:disabled { opacity: .55; cursor: not-allowed; }
@@ -232,6 +235,8 @@ function sendHtml(response) {
<h2>Required Bosses</h2> <h2>Required Bosses</h2>
<div id="requiredBosses" class="bosses"></div> <div id="requiredBosses" class="bosses"></div>
<div id="estimate" class="estimate"></div>
<div class="buttons"> <div class="buttons">
<button id="run" class="primary">Run</button> <button id="run" class="primary">Run</button>
<button id="stop" disabled>Stop</button> <button id="stop" disabled>Stop</button>
@@ -255,6 +260,7 @@ function sendHtml(response) {
const status = document.getElementById("status"); const status = document.getElementById("status");
const raw = document.getElementById("raw"); const raw = document.getElementById("raw");
const dashboard = document.getElementById("dashboard"); const dashboard = document.getElementById("dashboard");
const estimateBox = document.getElementById("estimate");
const run = document.getElementById("run"); const run = document.getElementById("run");
const stop = document.getElementById("stop"); const stop = document.getElementById("stop");
@@ -265,6 +271,8 @@ function sendHtml(response) {
renderChecks("requiredBosses", config.bosses, false); renderChecks("requiredBosses", config.bosses, false);
wireAll("allClasses", "classes"); wireAll("allClasses", "classes");
wireAll("allBosses", "bosses"); wireAll("allBosses", "bosses");
wireEstimateUpdates();
updateEstimate();
}); });
function renderChecks(containerId, values, checked) { function renderChecks(containerId, values, checked) {
@@ -277,9 +285,11 @@ function sendHtml(response) {
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
master.addEventListener("change", () => { master.addEventListener("change", () => {
container.querySelectorAll("input").forEach(input => input.checked = master.checked); container.querySelectorAll("input").forEach(input => input.checked = master.checked);
updateEstimate();
}); });
container.addEventListener("change", () => { container.addEventListener("change", () => {
master.checked = [...container.querySelectorAll("input")].every(input => input.checked); master.checked = [...container.querySelectorAll("input")].every(input => input.checked);
updateEstimate();
}); });
} }
@@ -333,15 +343,100 @@ function sendHtml(response) {
].join(" "); ].join(" ");
} }
function wireEstimateUpdates() {
document.querySelector("aside").addEventListener("input", updateEstimate);
document.querySelector("aside").addEventListener("change", updateEstimate);
}
function updateEstimate() {
if (!state.config) return;
const estimate = estimateFor(payload());
estimateBox.innerHTML = '<strong>' + number(estimate.trials) + ' tests</strong>' +
'<span>' + number(estimate.orderedBossGroupCount) + ' ordered boss groups x ' + number(estimate.classCount) + ' classes x ' + number(estimate.stageCount) + ' stages x ' + number(estimate.repeats) + ' repeats</span>' +
'<span>Max sim time: ' + duration(estimate.maxSimulatedSeconds) + ' / rough wall time: ' + duration(estimate.estimatedWallSeconds) + '</span>';
}
function estimateFor(options) {
const bossPool = options.bosses.includes("all") ? state.config.bosses : options.bosses;
const requiredBosses = options.requiredBosses.includes("none")
? []
: options.requiredBosses.filter(bossId => bossPool.includes(bossId));
const bossCount = intValue(options.bossCount, 2);
const orderedBossGroupCount = countOrderedBossGroups(bossPool.length, bossCount, requiredBosses.length);
const classCount = options.classes.includes("all") ? state.config.classes.length : Math.max(1, options.classes.length);
const stageCount = Math.max(1, parseStages(options.stages).length);
const repeats = intValue(options.repeats, 1);
const seconds = numberValue(options.seconds, 180);
const workers = intValue(options.workers, 8);
const trials = orderedBossGroupCount * classCount * stageCount * repeats;
const maxSimulatedSeconds = trials * seconds;
const maxTicks = maxSimulatedSeconds * 30;
const estimatedWallSeconds = 0.8 + (maxTicks / (Math.max(1, workers) * 45000));
return { classCount, estimatedWallSeconds, maxSimulatedSeconds, orderedBossGroupCount, repeats, stageCount, trials };
}
function countOrderedBossGroups(poolSize, bossCount, requiredCount) {
const safeBossCount = Math.max(1, Math.min(poolSize, Math.floor(bossCount)));
if (requiredCount > safeBossCount || requiredCount > poolSize) return 0;
return combination(poolSize - requiredCount, safeBossCount - requiredCount) * permutation(safeBossCount, safeBossCount);
}
function combination(n, k) {
if (k < 0 || k > n) return 0;
const safeK = Math.min(k, n - k);
let result = 1;
for (let index = 1; index <= safeK; index += 1) {
result = (result * (n - safeK + index)) / index;
}
return Math.round(result);
}
function permutation(n, k) {
if (k < 0 || k > n) return 0;
let result = 1;
for (let value = n - k + 1; value <= n; value += 1) result *= value;
return result;
}
function parseStages(value) {
return String(value).split(",").map(stage => Number.parseInt(stage.trim(), 10)).filter(Number.isFinite);
}
function intValue(value, fallback) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function numberValue(value, fallback) {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function duration(seconds) {
const safeSeconds = Math.max(0, Number(seconds) || 0);
if (safeSeconds < 60) return fixed(safeSeconds) + 's';
const minutes = Math.floor(safeSeconds / 60);
const remaining = Math.round(safeSeconds % 60);
if (minutes < 60) return minutes + 'm ' + remaining + 's';
const hours = Math.floor(minutes / 60);
return hours + 'h ' + (minutes % 60) + 'm';
}
function number(value) {
return Number(value ?? 0).toLocaleString();
}
run.addEventListener("click", async () => { run.addEventListener("click", async () => {
const options = payload(); const options = payload();
raw.textContent = "$ " + commandFor(options) + "\\n\\n"; const estimate = estimateFor(options);
raw.textContent = "$ " + commandFor(options) + "\\n" +
"# estimated tests: " + number(estimate.trials) + ", rough wall time: " + duration(estimate.estimatedWallSeconds) + "\\n\\n";
dashboard.innerHTML = '<div class="empty">Simulation running...</div>'; dashboard.innerHTML = '<div class="empty">Simulation running...</div>';
state.lastJson = null; state.lastJson = null;
state.running = true; state.running = true;
run.disabled = true; run.disabled = true;
stop.disabled = false; stop.disabled = false;
status.textContent = "Running..."; status.textContent = "Running " + number(estimate.trials) + " tests, rough estimate " + duration(estimate.estimatedWallSeconds) + "...";
const response = await fetch("/run", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(options) }); const response = await fetch("/run", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(options) });
if (!response.ok || !response.body) { if (!response.ok || !response.body) {
status.textContent = "Failed to start"; status.textContent = "Failed to start";
@@ -393,8 +488,8 @@ function sendHtml(response) {
return [ return [
'<div class="cards">', '<div class="cards">',
metric('Trials', totalTrials), metric('Trials', totalTrials),
metric('Elapsed', duration(data.config?.elapsedSeconds ?? 0)),
metric('Worst fail rate', worst ? percent(failRate(worst)) : 'n/a'), metric('Worst fail rate', worst ? percent(failRate(worst)) : 'n/a'),
metric('Worst combo', worst ? escapeHtml(worst.key) : 'n/a'),
metric('Gear / repeats', 'Gear +' + escapeHtml(data.config?.gearLevel ?? '?') + ' / ' + escapeHtml(data.config?.repeats ?? '?') + 'x'), metric('Gear / repeats', 'Gear +' + escapeHtml(data.config?.gearLevel ?? '?') + ' / ' + escapeHtml(data.config?.repeats ?? '?') + 'x'),
'</div>', '</div>',
totalFailures === 0 ? '<div class="empty risk-low">No failures found in returned hardest-combo rows. Review deaths and fight time for near-fails.</div>' : '', totalFailures === 0 ? '<div class="empty risk-low">No failures found in returned hardest-combo rows. Review deaths and fight time for near-fails.</div>' : '',