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

@ -310,6 +310,60 @@ export function validateSiteContent(site) {
"toolboxes.modules.randomizer.exportSuccess",
"toolboxes.modules.randomizer.importError",
"toolboxes.modules.randomizer.exportInfo",
"toolboxes.modules.rngSimulator.ratesTitle",
"toolboxes.modules.rngSimulator.addRateTitle",
"toolboxes.modules.rngSimulator.editRateTitle",
"toolboxes.modules.rngSimulator.emptyRates",
"toolboxes.modules.rngSimulator.labelLabel",
"toolboxes.modules.rngSimulator.labelPlaceholder",
"toolboxes.modules.rngSimulator.rateLabel",
"toolboxes.modules.rngSimulator.rateLabelRequired",
"toolboxes.modules.rngSimulator.rateInvalid",
"toolboxes.modules.rngSimulator.addRateButton",
"toolboxes.modules.rngSimulator.saveRateButton",
"toolboxes.modules.rngSimulator.cancelButton",
"toolboxes.modules.rngSimulator.deleteTitle",
"toolboxes.modules.rngSimulator.noActiveRateTitle",
"toolboxes.modules.rngSimulator.simulationDrawsLabel",
"toolboxes.modules.rngSimulator.perHourLabel",
"toolboxes.modules.rngSimulator.perDayLabel",
"toolboxes.modules.rngSimulator.perMonthLabel",
"toolboxes.modules.rngSimulator.probabilitySectionTitle",
"toolboxes.modules.rngSimulator.multiplierLabel",
"toolboxes.modules.rngSimulator.clearMultiplierButton",
"toolboxes.modules.rngSimulator.averageDrawsLabel",
"toolboxes.modules.rngSimulator.simulationChanceLabel",
"toolboxes.modules.rngSimulator.averageTimeTitle",
"toolboxes.modules.rngSimulator.occurrenceCountLabel",
"toolboxes.modules.rngSimulator.hourTimeLabel",
"toolboxes.modules.rngSimulator.dayTimeLabel",
"toolboxes.modules.rngSimulator.monthTimeLabel",
"toolboxes.modules.rngSimulator.chanceTargetsTitle",
"toolboxes.modules.rngSimulator.drawsUnit",
"toolboxes.modules.rngSimulator.hoursUnit",
"toolboxes.modules.rngSimulator.daysUnit",
"toolboxes.modules.rngSimulator.monthsUnit",
"toolboxes.modules.rngSimulator.emptySummary",
"toolboxes.modules.rngSimulator.simulationTitle",
"toolboxes.modules.rngSimulator.simulateButton",
"toolboxes.modules.rngSimulator.clearHistoryButton",
"toolboxes.modules.rngSimulator.emptyHistory",
"toolboxes.modules.rngSimulator.successesLabel",
"toolboxes.modules.rngSimulator.failsLabel",
"toolboxes.modules.rngSimulator.observedRateLabel",
"toolboxes.modules.rngSimulator.multiplierShortLabel",
"toolboxes.modules.rngSimulator.longestFailStreakLabel",
"toolboxes.modules.rngSimulator.importPlaceholder",
"toolboxes.modules.rngSimulator.importOpenButton",
"toolboxes.modules.rngSimulator.exportOpenButton",
"toolboxes.modules.rngSimulator.importModalTitle",
"toolboxes.modules.rngSimulator.exportModalTitle",
"toolboxes.modules.rngSimulator.importButton",
"toolboxes.modules.rngSimulator.exportButton",
"toolboxes.modules.rngSimulator.importSuccess",
"toolboxes.modules.rngSimulator.exportSuccess",
"toolboxes.modules.rngSimulator.importError",
"toolboxes.modules.rngSimulator.exportInfo",
"toolboxes.modules.combos.deviceLabel",
"toolboxes.modules.combos.keyboardLayoutLabel",
"toolboxes.modules.combos.categoryPlaceholder",
@ -711,7 +765,7 @@ export function validateSiteContent(site) {
});
const libraryToolDocs = valueAt(site, "library.toolDocs");
["memos", "notepad", "checklist", "counters", "randomizer", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
["memos", "notepad", "checklist", "counters", "randomizer", "rngSimulator", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
const docs = libraryToolDocs?.[toolType];
assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`);
assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`);

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);