Android build v1.1.35
This commit is contained in:
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
<h2>Required Bosses</h2>
|
||||
<div id="requiredBosses" class="bosses"></div>
|
||||
|
||||
<div id="estimate" class="estimate"></div>
|
||||
|
||||
<div class="buttons">
|
||||
<button id="run" class="primary">Run</button>
|
||||
<button id="stop" disabled>Stop</button>
|
||||
@@ -255,6 +260,7 @@ function sendHtml(response) {
|
||||
const status = document.getElementById("status");
|
||||
const raw = document.getElementById("raw");
|
||||
const dashboard = document.getElementById("dashboard");
|
||||
const estimateBox = document.getElementById("estimate");
|
||||
const run = document.getElementById("run");
|
||||
const stop = document.getElementById("stop");
|
||||
|
||||
@@ -265,6 +271,8 @@ function sendHtml(response) {
|
||||
renderChecks("requiredBosses", config.bosses, false);
|
||||
wireAll("allClasses", "classes");
|
||||
wireAll("allBosses", "bosses");
|
||||
wireEstimateUpdates();
|
||||
updateEstimate();
|
||||
});
|
||||
|
||||
function renderChecks(containerId, values, checked) {
|
||||
@@ -277,9 +285,11 @@ function sendHtml(response) {
|
||||
const container = document.getElementById(containerId);
|
||||
master.addEventListener("change", () => {
|
||||
container.querySelectorAll("input").forEach(input => input.checked = master.checked);
|
||||
updateEstimate();
|
||||
});
|
||||
container.addEventListener("change", () => {
|
||||
master.checked = [...container.querySelectorAll("input")].every(input => input.checked);
|
||||
updateEstimate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,15 +343,100 @@ function sendHtml(response) {
|
||||
].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 () => {
|
||||
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>';
|
||||
state.lastJson = null;
|
||||
state.running = true;
|
||||
run.disabled = true;
|
||||
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) });
|
||||
if (!response.ok || !response.body) {
|
||||
status.textContent = "Failed to start";
|
||||
@@ -393,8 +488,8 @@ function sendHtml(response) {
|
||||
return [
|
||||
'<div class="cards">',
|
||||
metric('Trials', totalTrials),
|
||||
metric('Elapsed', duration(data.config?.elapsedSeconds ?? 0)),
|
||||
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'),
|
||||
'</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>' : '',
|
||||
|
||||
Reference in New Issue
Block a user