Add chance simulator toolbox and refine checklist interactions
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s

This commit is contained in:
Shinuwa 2026-08-31 15:16:36 +02:00
parent 35fe877d58
commit 6b85e58765
25 changed files with 1518 additions and 18 deletions

View file

@ -12,8 +12,9 @@ import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, l
import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js";
import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js";
import { createBalancedTeams, drawWeightedEntries, filterExclusionsForPlayers, getDiceShape, rollDice } from "../website/src/features/toolboxes/modules/randomizerUtils.js";
import { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngSummary, getEffectiveRate, normalizeRateString, simulateRngDraws } from "../website/src/features/toolboxes/modules/rngSimulatorUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { compactModuleDataForStorage, createToolboxExportPayload, moduleStorageKey, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
function createTextImportContext() {
@ -34,6 +35,7 @@ function createTextImportContext() {
normalizeEquipmentPlannerData,
normalizeImageAnnotationData,
normalizeRandomizerData,
normalizeRngSimulatorData,
normalizeUrl
};
}
@ -284,6 +286,94 @@ test("randomizer text exchange round trips full configuration", () => {
);
});
test("rng simulator preserves rate precision and compacts storage", () => {
assert.equal(normalizeRateString("2.500"), "2.500");
assert.equal(normalizeRateString("0.001"), "0.001");
assert.equal(normalizeRateString("0.000004%"), "0.000004");
assert.equal(normalizeRateString("100.500"), "100");
const normalized = normalizeRngSimulatorData({
activeRateId: "rate2",
rates: [
{ id: "rate1", label: "Drop rare", rate: "2.500" },
{ id: "rate2", label: "Ultra rare", rate: "0.000004" },
{ id: "bad", label: "", rate: "4" }
],
settings: { multiplier: 50, simulationDraws: 2000, perHour: 30, perDay: 120, perMonth: 3600 },
history: [
{ id: "h1", at: "2026-08-30T12:00:00.000Z", label: "Drop rare", rate: "2.500", multiplier: 50, draws: 100, successes: 4, fails: 400, longestFailStreak: 40 }
]
});
assert.equal(normalized.activeRateId, "rate2");
assert.deepEqual(normalized.rates.map((rate) => [rate.label, rate.rate]), [["Drop rare", "2.500"], ["Ultra rare", "0.000004"]]);
assert.equal(normalized.history, undefined);
assert.equal(compactModuleDataForStorage("rngSimulator", normalizeRngSimulatorData({})), null);
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).settings.multiplier, 50);
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).settings.perMonth, 3600);
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).history, undefined);
});
test("rng simulator calculates probability milestones and deterministic simulations", () => {
assert.equal(calculateAtLeastOneProbability("100", 50), 1);
assert.equal(calculateDrawsForChance("50", 0.5), 1);
assert.equal(calculateDrawsForChance("1", 0.5), 69);
assert.equal(getEffectiveRate("10", 50), 15);
assert.equal(getEffectiveRate("10", -50), 5);
assert.equal(getEffectiveRate("75", 100), 100);
const hourlySummary = calculateRngSummary({ rate: "25", simulationDraws: 8, perHour: 2, perDay: 0, perMonth: 0 });
assert.equal(hourlySummary.timeByFrequency.hour, 2);
assert.equal(hourlySummary.timeByFrequency.day, 2 / 24);
assert.equal(hourlySummary.timeByFrequency.month, 2 / 24 / 30);
const multipliedSummary = calculateRngSummary({ rate: "10", multiplier: 100, simulationDraws: 1, perHour: 0, perDay: 0, perMonth: 0 });
assert.equal(multipliedSummary.effectiveRate, 20);
assert.equal(multipliedSummary.averageDraws, 5);
const result = simulateRngDraws("25", 8, (() => {
const values = [0.1, 0.9, 0.2, 0.8, 0.7, 0.1, 0.6, 0.5];
let index = 0;
return () => values[index++];
})());
assert.deepEqual(result, {
draws: 8,
successes: 3,
fails: 5,
observedRate: 37.5,
longestFailStreak: 2
});
const multipliedResult = simulateRngDraws("10", 4, (() => {
const values = [0.15, 0.25, 0.05, 0.9];
let index = 0;
return () => values[index++];
})(), 100);
assert.equal(multipliedResult.successes, 2);
});
test("rng simulator text exchange round trips rates and settings", () => {
const context = createTextImportContext();
const rngSimulator = normalizeRngSimulatorData({
activeRateId: "rate2",
rates: [
{ id: "rate1", label: "Drop rare", rate: "2.500" },
{ id: "rate2", label: "Ultra rare", rate: "0.000004" }
],
settings: { multiplier: 50, simulationDraws: 5000, perHour: 40, perDay: 120, perMonth: 3600 }
});
const exported = exportModuleText("rngSimulator", rngSimulator);
assert.doesNotMatch(exported, /\[history\]/);
const imported = importModuleText("rngSimulator", exported, context);
assert.equal(imported.ok, true);
assert.deepEqual(imported.data.rates.map((rate) => [rate.label, rate.rate]), [["Drop rare", "2.500"], ["Ultra rare", "0.000004"]]);
assert.equal(imported.data.rates.find((rate) => rate.id === imported.data.activeRateId)?.label, "Ultra rare");
assert.equal(imported.data.settings.multiplier, 50);
assert.equal(imported.data.settings.simulationDraws, 5000);
assert.equal(imported.data.settings.perHour, 40);
assert.equal(imported.data.history, undefined);
});
test("notepad helpers keep empty html and invalid command states stable", () => {
assert.equal(isHtmlEmpty("<p><br></p>", ""), true);
assert.equal(isHtmlEmpty("<p>&nbsp;</p>", ""), true);