Refine RNG simulator layout and simulation stats
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
055fb78d60
commit
f8231c748f
11 changed files with 825 additions and 394 deletions
|
|
@ -819,10 +819,10 @@ Type : `rngSimulator`
|
|||
"id": "rate1",
|
||||
"label": "Drop rare",
|
||||
"rate": "2.500",
|
||||
"costPerDraw": "150",
|
||||
"multiplier": 50,
|
||||
"perHour": 30,
|
||||
"perDay": 120,
|
||||
"perMonth": 3600
|
||||
"occurrenceValue": 30,
|
||||
"occurrenceUnit": "perHour"
|
||||
},
|
||||
{
|
||||
"id": "rate2",
|
||||
|
|
@ -841,14 +841,29 @@ Stockage compact :
|
|||
- `rates` contient les taux enregistrés du module; un taux sans libellé est ignoré.
|
||||
- `rate` est une chaîne en pourcentage afin de conserver la précision saisie, par exemple `"2.500"` ou `"0.000004"`.
|
||||
- un taux doit être strictement supérieur à `0` et inférieur ou égal à `100`; les valeurs supérieures sont bornées à `"100"`.
|
||||
- `costPerDraw` est une chaîne décimale optionnelle, associée au taux, utilisée comme unité libre pour les calculs de coût.
|
||||
- `activeRateId` référence un taux existant; sinon le premier taux est sélectionné.
|
||||
- `rates[].multiplier` est un bonus ou malus en pourcentage associé au taux, vaut 0 par défaut et le taux effectif est borné entre 0% et 100%.
|
||||
- `rates[].perHour`, `rates[].perDay` et `rates[].perMonth` sont numériques, positifs ou nuls, associés au taux, et valent 0 par défaut.
|
||||
- `rates[].occurrenceValue` est numérique, positif ou nul, associé au taux, et vaut 0 par défaut.
|
||||
- `rates[].occurrenceUnit` vaut `perHour`, `perDay` ou `perMonth`, et vaut `perHour` par défaut.
|
||||
- `settings.simulationDraws` est borné de 1 à 1 000 000 et vaut 1000 par défaut.
|
||||
- `settings` est omis si `simulationDraws` vaut sa valeur par défaut.
|
||||
- `rates[].multiplier`, `rates[].perHour`, `rates[].perDay` et `rates[].perMonth` sont omis si leur valeur vaut 0.
|
||||
- `rates[].costPerDraw` est omis si vide ou nul.
|
||||
- `rates[].multiplier` est omis si sa valeur vaut 0.
|
||||
- `rates[].occurrenceValue` et `rates[].occurrenceUnit` sont omis si `occurrenceValue` vaut 0.
|
||||
- l'historique des simulations n'est pas persisté en IndexedDB; il est limité aux 20 dernières entrées en `sessionStorage`.
|
||||
|
||||
Import/export texte :
|
||||
|
||||
```text
|
||||
@activeRateId: rate1
|
||||
[rates]
|
||||
- Drop rare | rate=2.500 | active=true | costPerDraw=150 | multiplier=50 | occurrenceValue=30 | occurrenceUnit=perHour
|
||||
|
||||
[settings]
|
||||
simulationDraws: 1000
|
||||
```
|
||||
|
||||
## Outil Combos
|
||||
|
||||
Type : `combos`
|
||||
|
|
|
|||
|
|
@ -317,6 +317,8 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.labelLabel",
|
||||
"toolboxes.modules.rngSimulator.labelPlaceholder",
|
||||
"toolboxes.modules.rngSimulator.rateLabel",
|
||||
"toolboxes.modules.rngSimulator.costPerDrawLabel",
|
||||
"toolboxes.modules.rngSimulator.costPerDrawPlaceholder",
|
||||
"toolboxes.modules.rngSimulator.rateLabelRequired",
|
||||
"toolboxes.modules.rngSimulator.rateInvalid",
|
||||
"toolboxes.modules.rngSimulator.addRateButton",
|
||||
|
|
@ -324,21 +326,29 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.cancelButton",
|
||||
"toolboxes.modules.rngSimulator.deleteTitle",
|
||||
"toolboxes.modules.rngSimulator.noActiveRateTitle",
|
||||
"toolboxes.modules.rngSimulator.activeRateLabel",
|
||||
"toolboxes.modules.rngSimulator.effectiveRateLabel",
|
||||
"toolboxes.modules.rngSimulator.statsTabLabel",
|
||||
"toolboxes.modules.rngSimulator.simulationTabLabel",
|
||||
"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.simulationChanceWithoutMultiplierLabel",
|
||||
"toolboxes.modules.rngSimulator.averageCostLabel",
|
||||
"toolboxes.modules.rngSimulator.averageTimeTitle",
|
||||
"toolboxes.modules.rngSimulator.periodLabel",
|
||||
"toolboxes.modules.rngSimulator.averageTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.occurrenceCountLabel",
|
||||
"toolboxes.modules.rngSimulator.hourTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.dayTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.monthTimeLabel",
|
||||
"toolboxes.modules.rngSimulator.chanceTargetsTitle",
|
||||
"toolboxes.modules.rngSimulator.chanceLabel",
|
||||
"toolboxes.modules.rngSimulator.costLabel",
|
||||
"toolboxes.modules.rngSimulator.drawsUnit",
|
||||
"toolboxes.modules.rngSimulator.hoursUnit",
|
||||
"toolboxes.modules.rngSimulator.daysUnit",
|
||||
|
|
@ -351,8 +361,15 @@ export function validateSiteContent(site) {
|
|||
"toolboxes.modules.rngSimulator.successesLabel",
|
||||
"toolboxes.modules.rngSimulator.failsLabel",
|
||||
"toolboxes.modules.rngSimulator.observedRateLabel",
|
||||
"toolboxes.modules.rngSimulator.expectedSuccessesLabel",
|
||||
"toolboxes.modules.rngSimulator.multiplierShortLabel",
|
||||
"toolboxes.modules.rngSimulator.longestFailStreakLabel",
|
||||
"toolboxes.modules.rngSimulator.longestSuccessStreakLabel",
|
||||
"toolboxes.modules.rngSimulator.firstSuccessCostLabel",
|
||||
"toolboxes.modules.rngSimulator.costPerSuccessLabel",
|
||||
"toolboxes.modules.rngSimulator.luckySimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.neutralSimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.unluckySimulationLabel",
|
||||
"toolboxes.modules.rngSimulator.importPlaceholder",
|
||||
"toolboxes.modules.rngSimulator.importOpenButton",
|
||||
"toolboxes.modules.rngSimulator.exportOpenButton",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ 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 { calculateAtLeastOneProbability, calculateDrawsForChance, calculateRngCostSummary, calculateRngSummary, getEffectiveRate, normalizeCostString, 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, 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";
|
||||
|
|
@ -300,8 +300,8 @@ test("rng simulator preserves rate precision and compacts storage", () => {
|
|||
const normalized = normalizeRngSimulatorData({
|
||||
activeRateId: "rate2",
|
||||
rates: [
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", multiplier: 25, perHour: 10 },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", multiplier: 50, perHour: 30, perDay: 120, perMonth: 3600 },
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "", multiplier: 25, occurrenceValue: 10, occurrenceUnit: "perHour" },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "12,500", multiplier: 50, occurrenceValue: 120, occurrenceUnit: "perDay" },
|
||||
{ id: "bad", label: "", rate: "4" }
|
||||
],
|
||||
settings: { simulationDraws: 2000 },
|
||||
|
|
@ -312,11 +312,17 @@ test("rng simulator preserves rate precision and compacts storage", () => {
|
|||
|
||||
assert.equal(normalized.activeRateId, "rate2");
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.label, rate.rate]), [["Drop rare", "2.500"], ["Ultra rare", "0.000004"]]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.multiplier, rate.perHour, rate.perDay, rate.perMonth]), [[25, 10, 0, 0], [50, 30, 120, 3600]]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => rate.costPerDraw), ["", "12.500"]);
|
||||
assert.deepEqual(normalized.rates.map((rate) => [rate.multiplier, rate.occurrenceValue, rate.occurrenceUnit]), [[25, 10, "perHour"], [50, 120, "perDay"]]);
|
||||
assert.equal(normalized.history, undefined);
|
||||
assert.equal(normalizeCostString("0"), "");
|
||||
assert.equal(normalizeCostString("0012,500"), "12.500");
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalizeRngSimulatorData({})), null);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[0].costPerDraw, undefined);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].costPerDraw, "12.500");
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].multiplier, 50);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].perMonth, 3600);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].occurrenceValue, 120);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).rates[1].occurrenceUnit, "perDay");
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).settings.simulationDraws, 2000);
|
||||
assert.equal(compactModuleDataForStorage("rngSimulator", normalized).history, undefined);
|
||||
});
|
||||
|
|
@ -329,13 +335,24 @@ test("rng simulator calculates probability milestones and deterministic simulati
|
|||
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 });
|
||||
const hourlySummary = calculateRngSummary({ rate: "25", simulationDraws: 8, occurrenceValue: 2, occurrenceUnit: "perHour" });
|
||||
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 });
|
||||
const dailySummary = calculateRngSummary({ rate: "25", simulationDraws: 8, occurrenceValue: 2, occurrenceUnit: "perDay" });
|
||||
assert.equal(dailySummary.timeByFrequency.hour, 48);
|
||||
assert.equal(dailySummary.timeByFrequency.day, 2);
|
||||
const multipliedSummary = calculateRngSummary({ rate: "10", multiplier: 100, simulationDraws: 1, occurrenceValue: 0, occurrenceUnit: "perHour" });
|
||||
assert.equal(multipliedSummary.effectiveRate, 20);
|
||||
assert.equal(multipliedSummary.averageDraws, 5);
|
||||
const costSummary = calculateRngCostSummary({ costPerDraw: "12,5", averageDraws: 5, drawsForChance: { 50: 4, 90: 12 }, simulationDraws: 8, simulationSuccesses: 2 });
|
||||
assert.equal(costSummary.costPerDraw, 12.5);
|
||||
assert.equal(costSummary.averageSuccessCost, 62.5);
|
||||
assert.equal(costSummary.targetCosts[50], 50);
|
||||
assert.equal(costSummary.targetCosts[90], 150);
|
||||
assert.equal(costSummary.simulationCostPerSuccess, 50);
|
||||
assert.equal(calculateRngCostSummary({ costPerDraw: "12,5", averageDraws: 5, drawsForChance: {}, simulationDraws: 8, simulationSuccesses: 2, firstSuccessDraw: 3 }).simulationFirstSuccessCost, 37.5);
|
||||
assert.equal(calculateRngCostSummary({ costPerDraw: "0", averageDraws: 5, drawsForChance: {}, simulationDraws: 8, simulationSuccesses: 2 }), null);
|
||||
|
||||
const result = simulateRngDraws("25", 8, (() => {
|
||||
const values = [0.1, 0.9, 0.2, 0.8, 0.7, 0.1, 0.6, 0.5];
|
||||
|
|
@ -348,7 +365,9 @@ test("rng simulator calculates probability milestones and deterministic simulati
|
|||
successes: 3,
|
||||
fails: 5,
|
||||
observedRate: 37.5,
|
||||
longestFailStreak: 2
|
||||
longestFailStreak: 2,
|
||||
longestSuccessStreak: 1,
|
||||
firstSuccessDraw: 1
|
||||
});
|
||||
|
||||
const multipliedResult = simulateRngDraws("10", 4, (() => {
|
||||
|
|
@ -357,6 +376,8 @@ test("rng simulator calculates probability milestones and deterministic simulati
|
|||
return () => values[index++];
|
||||
})(), 100);
|
||||
assert.equal(multipliedResult.successes, 2);
|
||||
assert.equal(multipliedResult.longestSuccessStreak, 1);
|
||||
assert.equal(multipliedResult.firstSuccessDraw, 1);
|
||||
});
|
||||
|
||||
test("rng simulator text exchange round trips rates and settings", () => {
|
||||
|
|
@ -364,8 +385,8 @@ test("rng simulator text exchange round trips rates and settings", () => {
|
|||
const rngSimulator = normalizeRngSimulatorData({
|
||||
activeRateId: "rate2",
|
||||
rates: [
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", multiplier: 25, perHour: 20 },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", multiplier: 50, perHour: 40, perDay: 120, perMonth: 3600 }
|
||||
{ id: "rate1", label: "Drop rare", rate: "2.500", costPerDraw: "150", multiplier: 25, occurrenceValue: 20, occurrenceUnit: "perHour" },
|
||||
{ id: "rate2", label: "Ultra rare", rate: "0.000004", costPerDraw: "", multiplier: 50, occurrenceValue: 120, occurrenceUnit: "perDay" }
|
||||
],
|
||||
settings: { simulationDraws: 5000 }
|
||||
});
|
||||
|
|
@ -374,11 +395,13 @@ test("rng simulator text exchange round trips rates and settings", () => {
|
|||
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.label === "Drop rare")?.costPerDraw, "150");
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.costPerDraw, "");
|
||||
assert.equal(imported.data.rates.find((rate) => rate.id === imported.data.activeRateId)?.label, "Ultra rare");
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Drop rare")?.multiplier, 25);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.multiplier, 50);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.perHour, 40);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.perMonth, 3600);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.occurrenceValue, 120);
|
||||
assert.equal(imported.data.rates.find((rate) => rate.label === "Ultra rare")?.occurrenceUnit, "perDay");
|
||||
assert.equal(imported.data.settings.simulationDraws, 5000);
|
||||
assert.equal(imported.data.history, undefined);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4771,20 +4771,21 @@
|
|||
{
|
||||
"id": "fnn88zy",
|
||||
"label": "Elten Piece",
|
||||
"rate": "0.000004"
|
||||
"rate": "0.000004",
|
||||
"costPerDraw": "150",
|
||||
"multiplier": 320,
|
||||
"occurrenceValue": 60,
|
||||
"occurrenceUnit": "perHour"
|
||||
},
|
||||
{
|
||||
"id": "pen-eklecta",
|
||||
"label": "Pen Eklecta",
|
||||
"rate": "20.02"
|
||||
"rate": "20.02",
|
||||
"costPerDraw": "12.5"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"multiplier": 320,
|
||||
"simulationDraws": 100000,
|
||||
"perHour": 60,
|
||||
"perDay": 0,
|
||||
"perMonth": 0
|
||||
"simulationDraws": 100000
|
||||
}
|
||||
},
|
||||
"m9": {
|
||||
|
|
|
|||
|
|
@ -498,8 +498,17 @@
|
|||
"icon": "clover",
|
||||
"title": "Enregistrer un taux",
|
||||
"preview": {
|
||||
"kind": "textButton",
|
||||
"label": "%"
|
||||
"kind": "iconButton",
|
||||
"icon": "add"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Taux",
|
||||
"icon": "edit",
|
||||
"title": "Modifier un taux",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "edit"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -524,26 +533,26 @@
|
|||
"features": [
|
||||
{
|
||||
"title": "Taux sauvegardés",
|
||||
"text": "Chaque taux possède un libellé et garde la précision saisie, même pour des probabilités très faibles.",
|
||||
"text": "Chaque taux possède un libellé, un coût optionnel et garde la précision saisie, même pour des probabilités très faibles.",
|
||||
"details": [
|
||||
"La saisie par défaut utilise trois décimales, mais le stockage conserve la chaîne exacte normalisée.",
|
||||
"Un taux comme 0.000004% reste disponible tel quel pour les calculs et les simulations.",
|
||||
"La sélection active permet de comparer rapidement plusieurs drops ou chances de réussite."
|
||||
"Le coût d'un tirage permet d'estimer le coût moyen ou simulé sans imposer d'unité."
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Lecture probabiliste",
|
||||
"text": "L'outil affiche la moyenne d'essais avant réussite, le temps moyen selon votre rythme et les paliers de chance cumulée.",
|
||||
"text": "L'onglet Calculs affiche la moyenne d'essais avant réussite, le temps moyen selon votre rythme, les coûts et les paliers de chance cumulée.",
|
||||
"details": [
|
||||
"Les occurrences par heure, jour et mois sont indépendantes pour s'adapter à différents usages.",
|
||||
"Les paliers 50%, 90%, 95% et 99% donnent un repère concret sur la variance.",
|
||||
"La simulation agrège réussites, échecs et pire série d'échecs sans stocker chaque tirage."
|
||||
"L'onglet Simulation agrège réussites, échecs, pire série d'échecs et coûts sans stocker chaque tirage."
|
||||
]
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"text": "Le format texte contient les sections [rates] et [settings]. Les taux sont exprimés en pourcentage.",
|
||||
"example": "@activeRateId: fnn88zy\n[rates]\n- Elten Piece | rate=0.000004 | active=true | multiplier=320 | perHour=60 | perDay=0 | perMonth=0\n- Pen Eklecta | rate=20.02 | multiplier=0 | perHour=0 | perDay=0 | perMonth=0\n\n[settings]\nsimulationDraws: 1000"
|
||||
"text": "Le format texte contient les sections [rates] et [settings]. Les taux sont exprimés en pourcentage et le coût d'un tirage reste optionnel.",
|
||||
"example": "@activeRateId: fnn88zy\n[rates]\n- Elten Piece | rate=0.000004 | active=true | costPerDraw=150 | multiplier=320 | occurrenceValue=60 | occurrenceUnit=perHour\n- Pen Eklecta | rate=20.02 | costPerDraw=12.5 | multiplier=0\n\n[settings]\nsimulationDraws: 1000"
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
|
|
@ -2106,6 +2115,8 @@
|
|||
"labelLabel": "Libellé",
|
||||
"labelPlaceholder": "Drop rare",
|
||||
"rateLabel": "Taux (%)",
|
||||
"costPerDrawLabel": "Coût d'un tirage",
|
||||
"costPerDrawPlaceholder": "Optionnel",
|
||||
"rateLabelRequired": "Libellé requis.",
|
||||
"rateInvalid": "Taux invalide. Utilisez une valeur supérieure à 0 et inférieure ou égale à 100.",
|
||||
"addRateButton": "Ajouter",
|
||||
|
|
@ -2113,21 +2124,29 @@
|
|||
"cancelButton": "Annuler",
|
||||
"deleteTitle": "Supprimer",
|
||||
"noActiveRateTitle": "Aucun taux actif",
|
||||
"activeRateLabel": "Taux brut",
|
||||
"effectiveRateLabel": "Taux final",
|
||||
"statsTabLabel": "Calculs",
|
||||
"simulationTabLabel": "Simulation",
|
||||
"simulationDrawsLabel": "Tirages simulés",
|
||||
"perHourLabel": "Par heure",
|
||||
"perDayLabel": "Par jour",
|
||||
"perMonthLabel": "Par mois",
|
||||
"probabilitySectionTitle": "Probabilités avec multiplicateur",
|
||||
"multiplierLabel": "Multiplicateur (%)",
|
||||
"clearMultiplierButton": "Réinitialiser le multiplicateur",
|
||||
"averageDrawsLabel": "Moyenne avant réussite après multiplicateur",
|
||||
"simulationChanceLabel": "Chance sur les tirages saisis après multiplicateur",
|
||||
"averageDrawsLabel": "Moyenne avant réussite",
|
||||
"simulationChanceLabel": "Chance sur les tirages saisis",
|
||||
"simulationChanceWithoutMultiplierLabel": "Chance sur les tirages saisis sans multiplicateur",
|
||||
"averageCostLabel": "Coût moyen avant réussite",
|
||||
"averageTimeTitle": "Temps moyen avant réussite",
|
||||
"periodLabel": "Période",
|
||||
"averageTimeLabel": "Temps moyen",
|
||||
"occurrenceCountLabel": "Nombre d'occurrences",
|
||||
"hourTimeLabel": "En heures",
|
||||
"dayTimeLabel": "En jours",
|
||||
"monthTimeLabel": "En mois",
|
||||
"chanceTargetsTitle": "Tirages pour atteindre une chance cumulée",
|
||||
"chanceLabel": "Chance",
|
||||
"costLabel": "Coût",
|
||||
"drawsUnit": "tirages",
|
||||
"hoursUnit": "h",
|
||||
"daysUnit": "j",
|
||||
|
|
@ -2140,9 +2159,16 @@
|
|||
"successesLabel": "Réussites",
|
||||
"failsLabel": "Fails",
|
||||
"observedRateLabel": "Taux de réussite",
|
||||
"expectedSuccessesLabel": "Réussites attendues",
|
||||
"multiplierShortLabel": "Bonus",
|
||||
"longestFailStreakLabel": "Pire série",
|
||||
"importPlaceholder": "@activeRateId: fnn88zy\n[rates]\n- Elten Piece | rate=0.000004 | active=true | multiplier=320 | perHour=60 | perDay=0 | perMonth=0\n- Pen Eklecta | rate=20.02 | multiplier=0 | perHour=0 | perDay=0 | perMonth=0\n\n[settings]\nsimulationDraws: 1000",
|
||||
"longestFailStreakLabel": "Pire série d'échec",
|
||||
"longestSuccessStreakLabel": "Réussite consécutive",
|
||||
"firstSuccessCostLabel": "Coût de la 1ere réussite",
|
||||
"costPerSuccessLabel": "Coût moyen/réussite",
|
||||
"luckySimulationLabel": "Au-dessus",
|
||||
"neutralSimulationLabel": "Dans la cible",
|
||||
"unluckySimulationLabel": "Sous l'attendu",
|
||||
"importPlaceholder": "@activeRateId: fnn88zy\n[rates]\n- Elten Piece | rate=0.000004 | active=true | costPerDraw=150 | multiplier=320 | occurrenceValue=60 | occurrenceUnit=perHour\n- Pen Eklecta | rate=20.02 | costPerDraw=12.5 | multiplier=0\n\n[settings]\nsimulationDraws: 1000",
|
||||
"importOpenButton": "Importer du texte",
|
||||
"exportOpenButton": "Exporter en format texte",
|
||||
"importModalTitle": "Importer un simulateur de chance",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Rôle : fournit le simulateur de chance pour calculer et simuler des taux enregistrés.
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { Tabs } from "../../../components/Tabs.jsx";
|
||||
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||
import { TextExchangeActions } from "./TextExchangeActions.jsx";
|
||||
import {
|
||||
|
|
@ -8,15 +9,23 @@ import {
|
|||
RNG_SIMULATOR_HISTORY_LIMIT,
|
||||
RNG_SIMULATOR_LIMITS,
|
||||
addRngSimulationHistory,
|
||||
calculateAtLeastOneProbability,
|
||||
calculateRngCostSummary,
|
||||
calculateRngSummary,
|
||||
clampDecimal,
|
||||
clampInteger,
|
||||
getEffectiveRate,
|
||||
isValidRateString,
|
||||
normalizeCostString,
|
||||
normalizeRateString,
|
||||
simulateRngDraws
|
||||
} from "./rngSimulatorUtils.js";
|
||||
|
||||
const CHANCE_TARGETS = [50, 90, 95, 99];
|
||||
const RNG_TABS = [
|
||||
{ id: "stats", label: "Calculs" },
|
||||
{ id: "simulation", label: "Simulation" }
|
||||
];
|
||||
|
||||
function formatNumber(value, maximumFractionDigits = 2) {
|
||||
if (!Number.isFinite(value)) return "—";
|
||||
|
|
@ -28,6 +37,11 @@ function formatPercent(value, maximumFractionDigits = 6) {
|
|||
return `${value.toLocaleString("fr-FR", { maximumFractionDigits })} %`;
|
||||
}
|
||||
|
||||
function formatCost(value) {
|
||||
if (!Number.isFinite(value)) return "—";
|
||||
return formatNumber(value, 0);
|
||||
}
|
||||
|
||||
function formatDrawTarget(value, unit) {
|
||||
if (!Number.isFinite(value)) return `— ${unit}`;
|
||||
if (value >= 1000000) {
|
||||
|
|
@ -73,14 +87,18 @@ function normalizeSessionHistoryEntry(entry, index) {
|
|||
const successes = clampInteger(entry?.successes, 0, draws, 0);
|
||||
return {
|
||||
id: entry?.id || `rng-session-${index}`,
|
||||
rateId: String(entry?.rateId || "").trim(),
|
||||
label: String(entry?.label || "").trim() || "Simulation",
|
||||
rate: normalizeRateString(entry?.rate),
|
||||
costPerDraw: normalizeCostString(entry?.costPerDraw),
|
||||
multiplier: clampDecimal(entry?.multiplier, RNG_SIMULATOR_LIMITS.multiplierMin, RNG_SIMULATOR_LIMITS.multiplierMax, 0),
|
||||
draws,
|
||||
successes,
|
||||
fails: draws - successes,
|
||||
observedRate: draws > 0 ? (successes / draws) * 100 : 0,
|
||||
longestFailStreak: clampInteger(entry?.longestFailStreak, 0, draws, 0)
|
||||
longestFailStreak: clampInteger(entry?.longestFailStreak, 0, draws, 0),
|
||||
longestSuccessStreak: clampInteger(entry?.longestSuccessStreak, 0, draws, 0),
|
||||
firstSuccessDraw: clampInteger(entry?.firstSuccessDraw, 0, draws, 0)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -94,33 +112,81 @@ function writeSimulationHistory(storageKey, history) {
|
|||
}
|
||||
}
|
||||
|
||||
export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
|
||||
function historyEntryMatchesRate(entry, rate) {
|
||||
if (!entry || !rate) return false;
|
||||
if (entry.rateId) return entry.rateId === rate.id;
|
||||
return entry.label === rate.label && entry.rate === rate.rate;
|
||||
}
|
||||
|
||||
export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, textExchangeOpen }) {
|
||||
const data = context.normalizeRngSimulatorData(context.getModuleData(toolboxId, moduleId, {}));
|
||||
const textContent = context.moduleText?.rngSimulator || {};
|
||||
const tabs = useMemo(() => [
|
||||
{ ...RNG_TABS[0], label: textContent.statsTabLabel || RNG_TABS[0].label },
|
||||
{ ...RNG_TABS[1], label: textContent.simulationTabLabel || RNG_TABS[1].label }
|
||||
], [textContent.simulationTabLabel, textContent.statsTabLabel]);
|
||||
const historyStorageKey = `rng-simulator:${toolboxId}:${moduleId}:history`;
|
||||
const [activeTab, setActiveTab] = useState("stats");
|
||||
const [history, setHistory] = useState(() => readSimulationHistory(historyStorageKey));
|
||||
const [editingRateId, setEditingRateId] = useState("");
|
||||
const [multiplierDraft, setMultiplierDraft] = useState(null);
|
||||
const [occurrenceDrafts, setOccurrenceDrafts] = useState({ perHour: null, perDay: null, perMonth: null });
|
||||
const [costDraft, setCostDraft] = useState(null);
|
||||
const [occurrenceUnit, setOccurrenceUnit] = useState("perHour");
|
||||
const [occurrenceDraft, setOccurrenceDraft] = useState(null);
|
||||
const rateFormRef = useRef(null);
|
||||
const previousEditingRef = useRef(editing);
|
||||
const rateDraft = useDraftForm({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
const rateFormOpen = editing || Boolean(editingRateId);
|
||||
const activeRate = data.rates.find((rate) => rate.id === data.activeRateId) || data.rates[0] || null;
|
||||
const summary = useMemo(() => activeRate ? calculateRngSummary({
|
||||
rate: activeRate.rate,
|
||||
multiplier: activeRate.multiplier,
|
||||
simulationDraws: data.settings.simulationDraws,
|
||||
perHour: activeRate.perHour,
|
||||
perDay: activeRate.perDay,
|
||||
perMonth: activeRate.perMonth
|
||||
occurrenceValue: activeRate.occurrenceValue,
|
||||
occurrenceUnit: activeRate.occurrenceUnit
|
||||
}) : null, [activeRate, data.settings.simulationDraws]);
|
||||
const costSummary = useMemo(() => activeRate && summary ? calculateRngCostSummary({
|
||||
costPerDraw: costDraft ?? activeRate.costPerDraw,
|
||||
averageDraws: summary.averageDraws,
|
||||
drawsForChance: summary.drawsForChance,
|
||||
simulationDraws: data.settings.simulationDraws,
|
||||
simulationSuccesses: 0
|
||||
}) : null, [activeRate, costDraft, data.settings.simulationDraws, summary]);
|
||||
const activeHistory = useMemo(() => history.filter((entry) => historyEntryMatchesRate(entry, activeRate)), [activeRate, history]);
|
||||
const chanceWithoutMultiplier = activeRate ? calculateAtLeastOneProbability(activeRate.rate, data.settings.simulationDraws, 0) : 0;
|
||||
|
||||
useEffect(() => {
|
||||
setHistory(readSimulationHistory(historyStorageKey));
|
||||
}, [historyStorageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const wasEditing = previousEditingRef.current;
|
||||
if (wasEditing && !editing) setEditingRateId("");
|
||||
if (!wasEditing && editing && !editingRateId) rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
previousEditingRef.current = editing;
|
||||
// Le hook useDraftForm expose reset sans identité stable; cette transition ne doit dépendre que de l'ouverture.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [editing, editingRateId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rateFormOpen) return undefined;
|
||||
function closeOnOutsidePointer(event) {
|
||||
if (event.target.closest?.(".module-edit-button")) return;
|
||||
if (!rateFormRef.current?.contains(event.target)) {
|
||||
setEditingRateId("");
|
||||
setEditing(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("pointerdown", closeOnOutsidePointer);
|
||||
return () => document.removeEventListener("pointerdown", closeOnOutsidePointer);
|
||||
}, [rateFormOpen, setEditing]);
|
||||
|
||||
useEffect(() => {
|
||||
setMultiplierDraft(null);
|
||||
setOccurrenceDrafts({ perHour: null, perDay: null, perMonth: null });
|
||||
}, [activeRate?.id]);
|
||||
setCostDraft(null);
|
||||
setOccurrenceDraft(null);
|
||||
setOccurrenceUnit(activeRate?.occurrenceUnit || "perHour");
|
||||
}, [activeRate?.id, activeRate?.occurrenceUnit]);
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator");
|
||||
|
|
@ -158,35 +224,57 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeO
|
|||
if (multiplierDraft === null) return;
|
||||
const raw = multiplierDraft.trim();
|
||||
const parsed = Number(raw.replace(",", "."));
|
||||
if (!raw || !Number.isFinite(parsed)) {
|
||||
setMultiplierDraft(null);
|
||||
updateActiveRateSettings("multiplier", 0);
|
||||
return;
|
||||
}
|
||||
if (!raw || !Number.isFinite(parsed)) updateActiveRateSettings("multiplier", 0);
|
||||
setMultiplierDraft(null);
|
||||
}
|
||||
|
||||
function clearMultiplier() {
|
||||
setMultiplierDraft(null);
|
||||
updateActiveRateSettings("multiplier", 0);
|
||||
function updateCost(value) {
|
||||
setCostDraft(value);
|
||||
}
|
||||
|
||||
function updateOccurrence(field, value) {
|
||||
setOccurrenceDrafts((drafts) => ({ ...drafts, [field]: value }));
|
||||
function blurCost() {
|
||||
if (!activeRate || costDraft === null) return;
|
||||
save({
|
||||
...data,
|
||||
rates: data.rates.map((rate) => rate.id === activeRate.id ? { ...rate, costPerDraw: normalizeCostString(costDraft) } : rate)
|
||||
});
|
||||
setCostDraft(null);
|
||||
}
|
||||
|
||||
function updateOccurrence(value) {
|
||||
setOccurrenceDraft(value);
|
||||
if (!activeRate) return;
|
||||
if (String(value).trim() === "") {
|
||||
updateActiveRateSettings(field, 0);
|
||||
save({
|
||||
...data,
|
||||
rates: data.rates.map((rate) => rate.id === activeRate.id ? { ...rate, occurrenceValue: 0, occurrenceUnit } : rate)
|
||||
});
|
||||
return;
|
||||
}
|
||||
updateActiveRateSettings(field, value);
|
||||
const nextValue = clampDecimal(value, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, activeRate.occurrenceValue);
|
||||
save({
|
||||
...data,
|
||||
rates: data.rates.map((rate) => rate.id === activeRate.id ? { ...rate, occurrenceValue: nextValue, occurrenceUnit } : rate)
|
||||
});
|
||||
}
|
||||
|
||||
function blurOccurrence(field) {
|
||||
const value = occurrenceDrafts[field];
|
||||
if (value === null) return;
|
||||
function changeOccurrenceUnit(unit) {
|
||||
setOccurrenceUnit(unit);
|
||||
setOccurrenceDraft(null);
|
||||
if (!activeRate) return;
|
||||
save({
|
||||
...data,
|
||||
rates: data.rates.map((rate) => rate.id === activeRate.id ? { ...rate, occurrenceValue: activeRate.occurrenceValue, occurrenceUnit: unit } : rate)
|
||||
});
|
||||
}
|
||||
|
||||
function blurOccurrence() {
|
||||
if (occurrenceDraft === null) return;
|
||||
const value = occurrenceDraft;
|
||||
const raw = value.trim();
|
||||
const parsed = Number(raw.replace(",", "."));
|
||||
if (!raw || !Number.isFinite(parsed)) updateActiveRateSettings(field, 0);
|
||||
setOccurrenceDrafts((drafts) => ({ ...drafts, [field]: null }));
|
||||
if (!raw || !Number.isFinite(parsed)) updateOccurrence("");
|
||||
setOccurrenceDraft(null);
|
||||
}
|
||||
|
||||
function submitRate(event) {
|
||||
|
|
@ -200,22 +288,28 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeO
|
|||
setEditingRateId("");
|
||||
} else {
|
||||
const id = context.uid("rng");
|
||||
save({ ...data, activeRateId: data.activeRateId || id, rates: [...data.rates, { id, label, rate }] });
|
||||
save({ ...data, activeRateId: id, rates: [...data.rates, { id, label, rate }] });
|
||||
}
|
||||
reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
setEditing(false);
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
function editRate(rate) {
|
||||
save({ ...data, activeRateId: rate.id });
|
||||
setEditingRateId(rate.id);
|
||||
setEditing(true);
|
||||
rateDraft.reset({ label: rate.label, rate: rate.rate });
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
setEditingRateId("");
|
||||
rateDraft.reset({ label: "", rate: RNG_SIMULATOR_DEFAULT_RATE });
|
||||
setEditing(false);
|
||||
}
|
||||
|
||||
function selectRate(rate) {
|
||||
save({ ...data, activeRateId: rate.id });
|
||||
}
|
||||
|
||||
function deleteRate(rateId) {
|
||||
|
|
@ -229,8 +323,10 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeO
|
|||
const result = simulateRngDraws(activeRate.rate, data.settings.simulationDraws, Math.random, activeRate.multiplier);
|
||||
const entry = {
|
||||
id: context.uid("rng"),
|
||||
rateId: activeRate.id,
|
||||
label: activeRate.label,
|
||||
rate: activeRate.rate,
|
||||
costPerDraw: activeRate.costPerDraw,
|
||||
multiplier: activeRate.multiplier,
|
||||
...result
|
||||
};
|
||||
|
|
@ -240,8 +336,9 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeO
|
|||
}
|
||||
|
||||
function clearHistory() {
|
||||
setHistory([]);
|
||||
writeSimulationHistory(historyStorageKey, []);
|
||||
const nextHistory = activeRate ? history.filter((entry) => !historyEntryMatchesRate(entry, activeRate)) : [];
|
||||
setHistory(nextHistory);
|
||||
writeSimulationHistory(historyStorageKey, nextHistory);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -252,170 +349,188 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, textExchangeO
|
|||
</div>
|
||||
)}
|
||||
<div className="rng-simulator-tool">
|
||||
<section className="rng-simulator-grid">
|
||||
<div className="rng-simulator-rates-stack">
|
||||
<div className="rng-simulator-panel rng-simulator-rate-add-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h3>{editingRateId ? textContent.editRateTitle || "Modifier un taux" : textContent.addRateTitle || "Ajouter un taux"}</h3>
|
||||
</div>
|
||||
</header>
|
||||
<form className="rng-simulator-rate-form" onSubmit={submitRate}>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||
<input {...rateDraft.getFieldProps("label", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.labelPlaceholder || "Drop rare" })} />
|
||||
</label>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.rateLabel || "Taux (%)"}</span>
|
||||
<input {...rateDraft.getFieldProps("rate", { className: "nds-input", "data-size": "sm", type: "text", inputMode: "decimal", placeholder: RNG_SIMULATOR_DEFAULT_RATE })} />
|
||||
</label>
|
||||
{rateDraft.error && <p className="rng-simulator-error">{rateDraft.error}</p>}
|
||||
<div className="rng-simulator-form-actions">
|
||||
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">
|
||||
<Icon name={editingRateId ? "save" : "add"} />
|
||||
<span>{editingRateId ? textContent.saveRateButton || "Enregistrer" : textContent.addRateButton || "Ajouter"}</span>
|
||||
</button>
|
||||
{editingRateId && (
|
||||
<button className="nds-button" data-size="sm" type="button" onClick={cancelEdit}>
|
||||
{textContent.cancelButton || "Annuler"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<section className="rng-simulator-panel rng-simulator-rates-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h3>{textContent.ratesTitle || "Taux enregistrés"}</h3>
|
||||
<span className="rng-simulator-count">{data.rates.length}</span>
|
||||
</div>
|
||||
<div className="rng-simulator-panel rng-simulator-rates-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h3>{textContent.ratesTitle || "Taux enregistrés"}</h3>
|
||||
</div>
|
||||
<span className="rng-simulator-count">{data.rates.length}</span>
|
||||
</header>
|
||||
<div className="rng-simulator-rates nds-scrollbox">
|
||||
{data.rates.length ? data.rates.map((rate) => (
|
||||
<article className={`rng-simulator-rate-row ${rate.id === activeRate?.id ? "is-active" : ""}`} key={rate.id}>
|
||||
<button type="button" onClick={() => editRate(rate)}>
|
||||
<strong>{rate.label}</strong>
|
||||
</button>
|
||||
<div className="rng-simulator-rate-actions">
|
||||
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => deleteRate(rate.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${rate.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
)) : <p className="rng-simulator-empty">{textContent.emptyRates || "Aucun taux enregistré."}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="rng-simulator-panel rng-simulator-summary-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h3>{activeRate?.label || textContent.noActiveRateTitle || "Aucun taux actif"}</h3>
|
||||
<div className="rng-simulator-rate-layout">
|
||||
{rateFormOpen && (
|
||||
<form className="rng-simulator-rate-form" ref={rateFormRef} onSubmit={submitRate}>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.labelLabel || "Libellé"}</span>
|
||||
<input {...rateDraft.getFieldProps("label", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.labelPlaceholder || "Drop rare" })} />
|
||||
</label>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.rateLabel || "Taux (%)"}</span>
|
||||
<input {...rateDraft.getFieldProps("rate", { className: "nds-input", "data-size": "sm", type: "text", inputMode: "decimal", placeholder: RNG_SIMULATOR_DEFAULT_RATE })} />
|
||||
</label>
|
||||
{rateDraft.error && <p className="rng-simulator-error">{rateDraft.error}</p>}
|
||||
<div className="rng-simulator-form-actions">
|
||||
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">
|
||||
<Icon name={editingRateId ? "save" : "add"} />
|
||||
<span>{editingRateId ? textContent.saveRateButton || "Enregistrer" : textContent.addRateButton || "Ajouter"}</span>
|
||||
</button>
|
||||
{data.rates.length > 0 && (
|
||||
<button className="nds-button" data-size="sm" type="button" onClick={cancelEdit}>
|
||||
{textContent.cancelButton || "Annuler"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="rng-simulator-rates nds-scrollbox">
|
||||
{data.rates.length ? data.rates.map((rate) => (
|
||||
<article className={`rng-simulator-rate-row ${rate.id === activeRate?.id ? "is-active" : ""}`} key={rate.id}>
|
||||
<button type="button" onClick={() => selectRate(rate)}>
|
||||
<strong>{rate.label}</strong>
|
||||
<span>{rate.rate} %</span>
|
||||
</button>
|
||||
<div className="rng-simulator-rate-actions">
|
||||
<button className="nds-icon-button" data-size="xs" type="button" onClick={() => editRate(rate)} aria-label={`${textContent.editRateTitle || "Modifier"} ${rate.label}`} title={textContent.editRateTitle || "Modifier"}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => deleteRate(rate.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${rate.label}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
)) : <p className="rng-simulator-empty">{textContent.emptyRates || "Aucun taux enregistré."}</p>}
|
||||
</div>
|
||||
|
||||
{activeRate && summary && (
|
||||
<div className="rng-simulator-active-summary">
|
||||
<RngInfo label={textContent.activeRateLabel || "Taux brut"} value={`${activeRate.rate} %`} />
|
||||
</div>
|
||||
{activeRate && <span className="rng-simulator-active-rate">{activeRate.rate} %</span>}
|
||||
</header>
|
||||
{activeRate && summary ? (
|
||||
<>
|
||||
<div className="rng-simulator-section">
|
||||
<span className="rng-simulator-section-title">{textContent.probabilitySectionTitle || "Probabilités avec multiplicateur"}</span>
|
||||
<div className="rng-simulator-field rng-simulator-multiplier-field">
|
||||
<span>{textContent.multiplierLabel || "Multiplicateur"}</span>
|
||||
<div className="rng-simulator-percent-input">
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={multiplierDraft ?? String(activeRate.multiplier ?? 0)} onChange={(event) => updateMultiplier(event.target.value)} onBlur={blurMultiplier} aria-label={textContent.multiplierLabel || "Multiplicateur"} />
|
||||
<span>%</span>
|
||||
<button className="nds-icon-button" data-size="xs" type="button" onClick={clearMultiplier} aria-label={textContent.clearMultiplierButton || "Réinitialiser le multiplicateur"} title={textContent.clearMultiplierButton || "Réinitialiser le multiplicateur"}>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-stat-grid">
|
||||
<RngStat label={textContent.averageDrawsLabel || "Moyenne avant réussite" } value={`${formatNumber(summary.averageDraws, 2)} ${textContent.drawsUnit || "tirages"}`} />
|
||||
<RngStat label={textContent.simulationChanceLabel || "Chance sur les tirages saisis"} value={formatPercent(summary.chanceForSimulationDraws * 100, 6)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-section">
|
||||
<span className="rng-simulator-section-title">{textContent.averageTimeTitle || "Temps moyen avant réussite"}</span>
|
||||
<span className="rng-simulator-subtitle">{textContent.occurrenceCountLabel || "Nombre d'occurrences"}</span>
|
||||
<div className="rng-simulator-settings rng-simulator-three-columns">
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.perHourLabel || "Par heure"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={occurrenceDrafts.perHour ?? String(activeRate.perHour ?? 0)} onChange={(event) => updateOccurrence("perHour", event.target.value)} onBlur={() => blurOccurrence("perHour")} />
|
||||
</label>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.perDayLabel || "Par jour"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={occurrenceDrafts.perDay ?? String(activeRate.perDay ?? 0)} onChange={(event) => updateOccurrence("perDay", event.target.value)} onBlur={() => blurOccurrence("perDay")} />
|
||||
</label>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.perMonthLabel || "Par mois"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={occurrenceDrafts.perMonth ?? String(activeRate.perMonth ?? 0)} onChange={(event) => updateOccurrence("perMonth", event.target.value)} onBlur={() => blurOccurrence("perMonth")} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="rng-simulator-time-grid">
|
||||
<RngStat label={textContent.hourTimeLabel || "En heures"} value={formatTime(summary.timeByFrequency.hour, textContent.hoursUnit || "h")} />
|
||||
<RngStat label={textContent.dayTimeLabel || "En jours"} value={formatTime(summary.timeByFrequency.day, textContent.daysUnit || "j")} />
|
||||
<RngStat label={textContent.monthTimeLabel || "En mois"} value={formatTime(summary.timeByFrequency.month, textContent.monthsUnit || "mois")} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-section">
|
||||
<span className="rng-simulator-section-title">{textContent.chanceTargetsTitle || "Tirages pour atteindre une chance cumulée"}</span>
|
||||
<div className="rng-simulator-targets">
|
||||
{CHANCE_TARGETS.map((target) => (
|
||||
<RngStat key={target} label={`${target} %`} value={formatDrawTarget(summary.drawsForChance[target], textContent.drawsUnit || "tirages")} title={`${formatNumber(summary.drawsForChance[target], 0)} ${textContent.drawsUnit || "tirages"}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : <p className="rng-simulator-empty">{textContent.emptySummary || "Ajoutez un taux pour afficher les calculs."}</p>}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rng-simulator-panel rng-simulator-simulation-panel">
|
||||
<section className="rng-simulator-panel rng-simulator-tabs-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h3>{textContent.simulationTitle || "Résultats simulés"}</h3>
|
||||
</div>
|
||||
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} className="rng-simulator-tabs" />
|
||||
</header>
|
||||
<div className="rng-simulator-simulation-bar">
|
||||
<div className="rng-simulator-simulation-actions">
|
||||
<label className="rng-simulator-field rng-simulator-inline-field rng-simulator-simulation-draws">
|
||||
<span>{textContent.simulationDrawsLabel || "Tirages simulés"}</span>
|
||||
<input className="nds-input" data-size="sm" type="number" min="1" max={RNG_SIMULATOR_LIMITS.simulationDrawsMax} value={data.settings.simulationDraws} onChange={(event) => updateSettings("simulationDraws", event.target.value)} />
|
||||
</label>
|
||||
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={runSimulation} disabled={!activeRate}>
|
||||
<Icon name="play-circle" />
|
||||
<span>{textContent.simulateButton || "Simuler"}</span>
|
||||
</button>
|
||||
|
||||
{activeTab === "stats" && (
|
||||
<div className="rng-simulator-tab-content">
|
||||
{activeRate && summary ? (
|
||||
<>
|
||||
<div className="rng-simulator-section">
|
||||
<div className="rng-simulator-primary-controls">
|
||||
<div className="rng-simulator-primary-column">
|
||||
<div className="rng-simulator-field rng-simulator-multiplier-field">
|
||||
<span>{textContent.multiplierLabel || "Multiplicateur (%)"}</span>
|
||||
<div className="rng-simulator-percent-input">
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={multiplierDraft ?? String(activeRate.multiplier ?? 0)} onChange={(event) => updateMultiplier(event.target.value)} onBlur={blurMultiplier} aria-label={textContent.multiplierLabel || "Multiplicateur (%)"} />
|
||||
<span>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-stat-grid">
|
||||
<RngStat label={textContent.effectiveRateLabel || "Taux final"} value={formatPercent(summary.effectiveRate, 6)} />
|
||||
<RngStat label={textContent.averageDrawsLabel || "Moyenne avant réussite"} value={`${formatNumber(summary.averageDraws, 2)} ${textContent.drawsUnit || "tirages"}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-cost-column">
|
||||
<label className="rng-simulator-field rng-simulator-cost-field">
|
||||
<span>{textContent.costPerDrawLabel || "Coût d'un tirage"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={costDraft ?? activeRate.costPerDraw ?? ""} onChange={(event) => updateCost(event.target.value)} onBlur={blurCost} placeholder={textContent.costPerDrawPlaceholder || "Optionnel"} />
|
||||
</label>
|
||||
<RngStat label={textContent.averageCostLabel || "Coût moyen avant réussite"} value={formatCost(costSummary?.averageSuccessCost)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rng-simulator-section">
|
||||
<span className="rng-simulator-section-title">{textContent.averageTimeTitle || "Temps moyen avant réussite"}</span>
|
||||
<div className="rng-simulator-occurrence-control">
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.occurrenceCountLabel || "Nombre d'occurrences"}</span>
|
||||
<input className="nds-input" data-size="sm" type="text" inputMode="decimal" value={occurrenceDraft ?? String(activeRate.occurrenceValue ?? 0)} onChange={(event) => updateOccurrence(event.target.value)} onBlur={blurOccurrence} />
|
||||
</label>
|
||||
<label className="rng-simulator-field">
|
||||
<span>{textContent.periodLabel || "Période"}</span>
|
||||
<select className="nds-select" data-size="sm" value={occurrenceUnit} onChange={(event) => changeOccurrenceUnit(event.target.value)}>
|
||||
<option value="perHour">{textContent.perHourLabel || "Par heure"}</option>
|
||||
<option value="perDay">{textContent.perDayLabel || "Par jour"}</option>
|
||||
<option value="perMonth">{textContent.perMonthLabel || "Par mois"}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="rng-simulator-time-results">
|
||||
<RngStat label={textContent.hourTimeLabel || "En heures"} value={formatTime(summary.timeByFrequency.hour, textContent.hoursUnit || "h")} />
|
||||
<RngStat label={textContent.dayTimeLabel || "En jours"} value={formatTime(summary.timeByFrequency.day, textContent.daysUnit || "j")} />
|
||||
<RngStat label={textContent.monthTimeLabel || "En mois"} value={formatTime(summary.timeByFrequency.month, textContent.monthsUnit || "mois")} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rng-simulator-section">
|
||||
<span className="rng-simulator-section-title">{textContent.chanceTargetsTitle || "Tirages pour atteindre une chance cumulée"}</span>
|
||||
<div className="rng-simulator-table rng-simulator-target-table">
|
||||
<span>{textContent.chanceLabel || "Chance"}</span>
|
||||
<span>{textContent.drawsUnit || "Tirages"}</span>
|
||||
<span>{textContent.costLabel || "Coût"}</span>
|
||||
{CHANCE_TARGETS.map((target) => (
|
||||
<RngTargetRow costSummary={costSummary} draws={summary.drawsForChance[target]} key={target} target={target} textContent={textContent} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : <p className="rng-simulator-empty">{textContent.emptySummary || "Ajoutez un taux pour afficher les calculs."}</p>}
|
||||
</div>
|
||||
<button className="nds-button" data-size="sm" data-variant="danger" type="button" onClick={clearHistory} disabled={!history.length}>
|
||||
<Icon name="rubber" />
|
||||
<span>{textContent.clearHistoryButton || "Supprimer les simulations"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="rng-simulator-history nds-scrollbox">
|
||||
{history.length ? history.map((entry, index) => (
|
||||
<article className={`rng-simulator-history-row ${index === 0 ? "is-latest" : ""}`} key={entry.id}>
|
||||
<div>
|
||||
<strong>{entry.label}</strong>
|
||||
)}
|
||||
|
||||
{activeTab === "simulation" && (
|
||||
<div className="rng-simulator-tab-content">
|
||||
<div className="rng-simulator-simulation-overview">
|
||||
<div className="rng-simulator-simulation-actions">
|
||||
<label className="rng-simulator-field rng-simulator-inline-field rng-simulator-simulation-draws">
|
||||
<span>{textContent.simulationDrawsLabel || "Tirages simulés"}</span>
|
||||
<input className="nds-input" data-size="sm" type="number" min="1" max={RNG_SIMULATOR_LIMITS.simulationDrawsMax} value={data.settings.simulationDraws} onChange={(event) => updateSettings("simulationDraws", event.target.value)} />
|
||||
</label>
|
||||
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={runSimulation} disabled={!activeRate}>
|
||||
<Icon name="play-circle" />
|
||||
<span>{textContent.simulateButton || "Simuler"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="rng-simulator-history-rate">
|
||||
<span>{entry.rate} %</span>
|
||||
<small>{textContent.multiplierShortLabel || "Bonus"} {formatSignedPercent(entry.multiplier ?? 0)}</small>
|
||||
</div>
|
||||
<div className="rng-simulator-history-metrics">
|
||||
<span><small>{textContent.successesLabel || "Réussites"}</small><b>{entry.successes}/{entry.draws}</b></span>
|
||||
<span><small>{textContent.failsLabel || "Fails"}</small><b>{entry.fails}</b></span>
|
||||
<span><small>{textContent.observedRateLabel || "Taux de réussite"}</small><b>{formatPercent((entry.successes / entry.draws) * 100, 6)}</b></span>
|
||||
<span><small>{textContent.longestFailStreakLabel || "Pire série"}</small><b>{entry.longestFailStreak}</b></span>
|
||||
</div>
|
||||
</article>
|
||||
)) : <p className="rng-simulator-empty">{textContent.emptyHistory || "Aucune simulation."}</p>}
|
||||
</div>
|
||||
{activeRate && summary && (
|
||||
<div className="rng-simulator-simulation-summary">
|
||||
<RngStat label={textContent.simulationChanceLabel || "Chance sur les tirages saisis"} value={formatPercent(summary.chanceForSimulationDraws * 100, 6)} />
|
||||
<RngStat label={textContent.simulationChanceWithoutMultiplierLabel || "Chance sur les tirages saisis sans multiplicateur"} value={formatPercent(chanceWithoutMultiplier * 100, 6)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rng-simulator-history-toolbar">
|
||||
<button className="nds-button" data-size="sm" data-variant="danger" type="button" onClick={clearHistory} disabled={!activeHistory.length}>
|
||||
<Icon name="rubber" />
|
||||
<span>{textContent.clearHistoryButton || "Supprimer les simulations"}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="rng-simulator-history nds-scrollbox">
|
||||
{activeHistory.length ? activeHistory.map((entry, index) => (
|
||||
<RngHistoryRow entry={entry} index={index} key={entry.id} textContent={textContent} />
|
||||
)) : <p className="rng-simulator-empty">{textContent.emptyHistory || "Aucune simulation."}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RngInfo({ label, value }) {
|
||||
return (
|
||||
<span>
|
||||
<small>{label}</small>
|
||||
<strong>{value}</strong>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RngStat({ label, value, title }) {
|
||||
return (
|
||||
<article className="rng-simulator-stat" title={title}>
|
||||
|
|
@ -424,3 +539,59 @@ function RngStat({ label, value, title }) {
|
|||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function RngTargetRow({ costSummary, draws, target, textContent }) {
|
||||
return (
|
||||
<>
|
||||
<strong>{target} %</strong>
|
||||
<span title={`${formatNumber(draws, 0)} ${textContent.drawsUnit || "tirages"}`}>{formatDrawTarget(draws, textContent.drawsUnit || "tirages")}</span>
|
||||
<span>{formatCost(costSummary?.targetCosts?.[target])}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RngHistoryRow({ entry, index, textContent }) {
|
||||
const expectedSuccesses = entry.draws * (getEffectiveRate(entry.rate, entry.multiplier) / 100);
|
||||
const successDelta = entry.successes - expectedSuccesses;
|
||||
const luckLabel = successDelta > 0
|
||||
? textContent.luckySimulationLabel || "Au-dessus"
|
||||
: successDelta < 0
|
||||
? textContent.unluckySimulationLabel || "Sous l'attendu"
|
||||
: textContent.neutralSimulationLabel || "Dans la cible";
|
||||
const costs = calculateRngCostSummary({
|
||||
costPerDraw: entry.costPerDraw,
|
||||
averageDraws: 0,
|
||||
drawsForChance: {},
|
||||
simulationDraws: entry.draws,
|
||||
simulationSuccesses: entry.successes,
|
||||
firstSuccessDraw: entry.firstSuccessDraw
|
||||
});
|
||||
return (
|
||||
<article className={`rng-simulator-history-row ${index === 0 ? "is-latest" : ""}`}>
|
||||
<div className="rng-simulator-history-heading">
|
||||
<div className="rng-simulator-history-identity">
|
||||
<strong>{entry.label}</strong>
|
||||
<small>{luckLabel} · {formatSignedPercent((successDelta / entry.draws) * 100, 4)}</small>
|
||||
</div>
|
||||
<div className="rng-simulator-history-score">
|
||||
<b>{entry.successes}</b>
|
||||
<small>{textContent.successesLabel || "Réussites"}</small>
|
||||
</div>
|
||||
<div className="rng-simulator-history-rate">
|
||||
<span>{entry.rate} %</span>
|
||||
<small>{textContent.multiplierShortLabel || "Bonus"} {formatSignedPercent(entry.multiplier ?? 0)}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rng-simulator-history-metrics">
|
||||
<span><small>{textContent.successesLabel || "Réussites"}</small><b>{entry.successes}/{entry.draws}</b></span>
|
||||
<span><small>{textContent.failsLabel || "Fails"}</small><b>{entry.fails}</b></span>
|
||||
<span><small>{textContent.observedRateLabel || "Taux de réussite"}</small><b>{formatPercent((entry.successes / entry.draws) * 100, 6)}</b></span>
|
||||
<span><small>{textContent.expectedSuccessesLabel || "Réussites attendues"}</small><b>{formatNumber(expectedSuccesses, 2)}</b></span>
|
||||
<span><small>{textContent.longestFailStreakLabel || "Pire série d'échec"}</small><b>{entry.longestFailStreak}</b></span>
|
||||
<span><small>{textContent.longestSuccessStreakLabel || "Réussite consécutive"}</small><b>{entry.longestSuccessStreak}</b></span>
|
||||
<span><small>{textContent.firstSuccessCostLabel || "Coût de la 1ere réussite"}</small><b>{entry.firstSuccessDraw > 0 ? formatCost(costs?.simulationFirstSuccessCost) : "—"}</b></span>
|
||||
<span><small>{textContent.costPerSuccessLabel || "Coût moyen/réussite"}</small><b>{entry.successes > 0 ? formatCost(costs?.simulationCostPerSuccess) : "—"}</b></span>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export const RNG_SIMULATOR_HISTORY_LIMIT = 20;
|
|||
export const RNG_SIMULATOR_DEFAULT_RATE = "2.500";
|
||||
export const RNG_SIMULATOR_LIMITS = {
|
||||
rateMax: 100,
|
||||
costPerDrawMax: 1000000000000,
|
||||
multiplierMin: -1000000,
|
||||
multiplierMax: 1000000,
|
||||
simulationDrawsMin: 1,
|
||||
|
|
@ -11,6 +12,8 @@ export const RNG_SIMULATOR_LIMITS = {
|
|||
occurrenceRateMax: 1000000000
|
||||
};
|
||||
|
||||
export const RNG_SIMULATOR_OCCURRENCE_UNITS = ["perHour", "perDay", "perMonth"];
|
||||
|
||||
export function clampInteger(value, min, max, fallback = min) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
|
|
@ -40,6 +43,21 @@ export function normalizeRateString(value, fallback = RNG_SIMULATOR_DEFAULT_RATE
|
|||
return cleanInteger;
|
||||
}
|
||||
|
||||
export function normalizeCostString(value, fallback = "") {
|
||||
const raw = String(value ?? "").trim().replace(",", ".");
|
||||
if (!raw) return fallback;
|
||||
if (!/^\d*(?:\.\d+)?$/.test(raw) || raw === ".") return fallback;
|
||||
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||
if (parsed > RNG_SIMULATOR_LIMITS.costPerDrawMax) return String(RNG_SIMULATOR_LIMITS.costPerDrawMax);
|
||||
|
||||
const [integerPart = "0", decimalPart] = raw.split(".");
|
||||
const cleanInteger = String(Number.parseInt(integerPart || "0", 10));
|
||||
if (decimalPart !== undefined) return `${cleanInteger}.${decimalPart}`;
|
||||
return cleanInteger;
|
||||
}
|
||||
|
||||
export function isValidRateString(value) {
|
||||
const raw = String(value ?? "").trim().replace(",", ".").replace(/%$/, "").trim();
|
||||
if (!raw || !/^\d*(?:\.\d+)?$/.test(raw) || raw === ".") return false;
|
||||
|
|
@ -72,21 +90,20 @@ export function calculateDrawsForChance(rate, targetChance, multiplier = 0) {
|
|||
return Math.ceil(Math.log1p(-target) / Math.log1p(-probability));
|
||||
}
|
||||
|
||||
export function calculateRngSummary({ rate, multiplier = 0, simulationDraws, perHour, perDay, perMonth }) {
|
||||
export function normalizeOccurrenceUnit(value) {
|
||||
return RNG_SIMULATOR_OCCURRENCE_UNITS.includes(value) ? value : "perHour";
|
||||
}
|
||||
|
||||
export function calculateRngSummary({ rate, multiplier = 0, simulationDraws, occurrenceValue = 0, occurrenceUnit = "perHour" }) {
|
||||
const effectiveRate = getEffectiveRate(rate, multiplier);
|
||||
const probability = effectiveRate / 100;
|
||||
const averageDraws = probability > 0 ? 1 / probability : 0;
|
||||
const cleanPerHour = clampDecimal(perHour, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0);
|
||||
const cleanPerDay = clampDecimal(perDay, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0);
|
||||
const cleanPerMonth = clampDecimal(perMonth, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0);
|
||||
const cleanOccurrenceValue = clampDecimal(occurrenceValue, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0);
|
||||
const cleanOccurrenceUnit = normalizeOccurrenceUnit(occurrenceUnit);
|
||||
let averageHours = 0;
|
||||
if (cleanPerHour > 0) averageHours = averageDraws / cleanPerHour;
|
||||
else if (cleanPerDay > 0) averageHours = (averageDraws / cleanPerDay) * 24;
|
||||
else if (cleanPerMonth > 0) averageHours = (averageDraws / cleanPerMonth) * 24 * 30;
|
||||
|
||||
const explicitHourTime = cleanPerHour > 0 && averageDraws > 0 ? averageDraws / cleanPerHour : 0;
|
||||
const explicitDayTime = cleanPerDay > 0 && averageDraws > 0 ? averageDraws / cleanPerDay : 0;
|
||||
const explicitMonthTime = cleanPerMonth > 0 && averageDraws > 0 ? averageDraws / cleanPerMonth : 0;
|
||||
if (cleanOccurrenceValue > 0 && cleanOccurrenceUnit === "perHour") averageHours = averageDraws / cleanOccurrenceValue;
|
||||
else if (cleanOccurrenceValue > 0 && cleanOccurrenceUnit === "perDay") averageHours = (averageDraws / cleanOccurrenceValue) * 24;
|
||||
else if (cleanOccurrenceValue > 0 && cleanOccurrenceUnit === "perMonth") averageHours = (averageDraws / cleanOccurrenceValue) * 24 * 30;
|
||||
|
||||
return {
|
||||
probability,
|
||||
|
|
@ -94,9 +111,9 @@ export function calculateRngSummary({ rate, multiplier = 0, simulationDraws, per
|
|||
averageDraws,
|
||||
chanceForSimulationDraws: calculateAtLeastOneProbability(rate, simulationDraws, multiplier),
|
||||
timeByFrequency: {
|
||||
hour: explicitHourTime || averageHours,
|
||||
day: explicitDayTime || (averageHours > 0 ? averageHours / 24 : 0),
|
||||
month: explicitMonthTime || (averageHours > 0 ? averageHours / 24 / 30 : 0)
|
||||
hour: averageHours,
|
||||
day: averageHours > 0 ? averageHours / 24 : 0,
|
||||
month: averageHours > 0 ? averageHours / 24 / 30 : 0
|
||||
},
|
||||
drawsForChance: {
|
||||
50: calculateDrawsForChance(rate, 0.5, multiplier),
|
||||
|
|
@ -107,19 +124,45 @@ export function calculateRngSummary({ rate, multiplier = 0, simulationDraws, per
|
|||
};
|
||||
}
|
||||
|
||||
export function calculateRngCostSummary({ costPerDraw, averageDraws, drawsForChance, simulationDraws, simulationSuccesses, firstSuccessDraw = 0 }) {
|
||||
const cleanCost = Number(normalizeCostString(costPerDraw));
|
||||
if (!Number.isFinite(cleanCost) || cleanCost <= 0) return null;
|
||||
const cleanSimulationDraws = clampInteger(simulationDraws, RNG_SIMULATOR_LIMITS.simulationDrawsMin, RNG_SIMULATOR_LIMITS.simulationDrawsMax, 0);
|
||||
const cleanSuccesses = clampInteger(simulationSuccesses, 0, cleanSimulationDraws, 0);
|
||||
const cleanFirstSuccessDraw = clampInteger(firstSuccessDraw, 0, cleanSimulationDraws, 0);
|
||||
const targetCosts = {};
|
||||
Object.entries(drawsForChance || {}).forEach(([target, draws]) => {
|
||||
targetCosts[target] = Number.isFinite(draws) ? draws * cleanCost : Number.POSITIVE_INFINITY;
|
||||
});
|
||||
return {
|
||||
costPerDraw: cleanCost,
|
||||
averageSuccessCost: Number.isFinite(averageDraws) ? averageDraws * cleanCost : 0,
|
||||
targetCosts,
|
||||
simulationCostPerSuccess: cleanSuccesses > 0 ? (cleanSimulationDraws * cleanCost) / cleanSuccesses : 0,
|
||||
simulationFirstSuccessCost: cleanFirstSuccessDraw > 0 ? cleanFirstSuccessDraw * cleanCost : 0
|
||||
};
|
||||
}
|
||||
|
||||
export function simulateRngDraws(rate, draws, random = Math.random, multiplier = 0) {
|
||||
const probability = getEffectiveRate(rate, multiplier) / 100;
|
||||
const cleanDraws = clampInteger(draws, RNG_SIMULATOR_LIMITS.simulationDrawsMin, RNG_SIMULATOR_LIMITS.simulationDrawsMax, 1);
|
||||
let successes = 0;
|
||||
let currentFailStreak = 0;
|
||||
let currentSuccessStreak = 0;
|
||||
let firstSuccessDraw = 0;
|
||||
let longestFailStreak = 0;
|
||||
let longestSuccessStreak = 0;
|
||||
|
||||
for (let index = 0; index < cleanDraws; index += 1) {
|
||||
if (random() < probability) {
|
||||
successes += 1;
|
||||
if (!firstSuccessDraw) firstSuccessDraw = index + 1;
|
||||
currentSuccessStreak += 1;
|
||||
longestSuccessStreak = Math.max(longestSuccessStreak, currentSuccessStreak);
|
||||
currentFailStreak = 0;
|
||||
} else {
|
||||
currentFailStreak += 1;
|
||||
currentSuccessStreak = 0;
|
||||
longestFailStreak = Math.max(longestFailStreak, currentFailStreak);
|
||||
}
|
||||
}
|
||||
|
|
@ -129,7 +172,9 @@ export function simulateRngDraws(rate, draws, random = Math.random, multiplier =
|
|||
successes,
|
||||
fails: cleanDraws - successes,
|
||||
observedRate: cleanDraws > 0 ? (successes / cleanDraws) * 100 : 0,
|
||||
longestFailStreak
|
||||
longestFailStreak,
|
||||
longestSuccessStreak,
|
||||
firstSuccessDraw
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ export function exportRngSimulatorText(data) {
|
|||
lines.push("@activeRateId: " + (data.activeRateId || ""));
|
||||
lines.push("[rates]");
|
||||
(data.rates || []).forEach((rate) => {
|
||||
lines.push(`- ${rate.label} | rate=${rate.rate}${rate.id === data.activeRateId ? " | active=true" : ""} | multiplier=${rate.multiplier ?? 0} | perHour=${rate.perHour || 0} | perDay=${rate.perDay || 0} | perMonth=${rate.perMonth || 0}`);
|
||||
const cost = rate.costPerDraw ? ` | costPerDraw=${rate.costPerDraw}` : "";
|
||||
const occurrences = rate.occurrenceValue ? ` | occurrenceValue=${rate.occurrenceValue} | occurrenceUnit=${rate.occurrenceUnit || "perHour"}` : "";
|
||||
lines.push(`- ${rate.label} | rate=${rate.rate}${rate.id === data.activeRateId ? " | active=true" : ""}${cost} | multiplier=${rate.multiplier ?? 0}${occurrences}`);
|
||||
});
|
||||
lines.push(
|
||||
"",
|
||||
|
|
@ -51,10 +53,10 @@ export function importRngSimulatorText(text, context) {
|
|||
id,
|
||||
label: title,
|
||||
rate: pipeMeta.rate,
|
||||
costPerDraw: pipeMeta.costPerDraw,
|
||||
multiplier: pipeMeta.multiplier,
|
||||
perHour: pipeMeta.perHour,
|
||||
perDay: pipeMeta.perDay,
|
||||
perMonth: pipeMeta.perMonth
|
||||
occurrenceValue: pipeMeta.occurrenceValue,
|
||||
occurrenceUnit: pipeMeta.occurrenceUnit
|
||||
});
|
||||
if (pipeMeta.active === "true") data.activeRateId = id;
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import {
|
|||
RNG_SIMULATOR_LIMITS,
|
||||
clampDecimal,
|
||||
clampInteger,
|
||||
normalizeCostString,
|
||||
normalizeOccurrenceUnit,
|
||||
normalizeRateString
|
||||
} from "../../modules/rngSimulatorUtils.js";
|
||||
import { uid } from "./shared.js";
|
||||
|
|
@ -14,10 +16,10 @@ function normalizeRateEntry(entry) {
|
|||
id: entry?.id || uid("rng"),
|
||||
label,
|
||||
rate: normalizeRateString(entry?.rate),
|
||||
costPerDraw: normalizeCostString(entry?.costPerDraw),
|
||||
multiplier: clampDecimal(entry?.multiplier, RNG_SIMULATOR_LIMITS.multiplierMin, RNG_SIMULATOR_LIMITS.multiplierMax, 0),
|
||||
perHour: clampDecimal(entry?.perHour, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0),
|
||||
perDay: clampDecimal(entry?.perDay, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0),
|
||||
perMonth: clampDecimal(entry?.perMonth, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0)
|
||||
occurrenceValue: clampDecimal(entry?.occurrenceValue, RNG_SIMULATOR_LIMITS.occurrenceRateMin, RNG_SIMULATOR_LIMITS.occurrenceRateMax, 0),
|
||||
occurrenceUnit: normalizeOccurrenceUnit(entry?.occurrenceUnit)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -46,10 +48,12 @@ export function compactRngSimulatorDataForStorage(value) {
|
|||
if (normalized.rates.length) {
|
||||
compact.rates = normalized.rates.map((rate) => {
|
||||
const compactRate = { id: rate.id, label: rate.label, rate: rate.rate };
|
||||
if (rate.costPerDraw) compactRate.costPerDraw = rate.costPerDraw;
|
||||
if (rate.multiplier !== 0) compactRate.multiplier = rate.multiplier;
|
||||
if (rate.perHour !== 0) compactRate.perHour = rate.perHour;
|
||||
if (rate.perDay !== 0) compactRate.perDay = rate.perDay;
|
||||
if (rate.perMonth !== 0) compactRate.perMonth = rate.perMonth;
|
||||
if (rate.occurrenceValue !== 0) {
|
||||
compactRate.occurrenceValue = rate.occurrenceValue;
|
||||
compactRate.occurrenceUnit = rate.occurrenceUnit;
|
||||
}
|
||||
return compactRate;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,6 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
|
|||
mode: "Taux + simulations",
|
||||
icon: "clover",
|
||||
editable: true,
|
||||
addable: false,
|
||||
scrollable: true,
|
||||
libraryCategory: "calculationData"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,26 +1,18 @@
|
|||
// Rôle : porte les surfaces et grilles propres au simulateur de chance.
|
||||
.rng-simulator-tool {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.rng-simulator-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 0.9fr) minmax(0, 1.4fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.rng-simulator-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(16, 20, 38, 0.48);
|
||||
background: rgba(16, 20, 38, 0.44);
|
||||
}
|
||||
|
||||
.rng-simulator-panel > header {
|
||||
|
|
@ -30,14 +22,15 @@
|
|||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border-bottom: 1px solid rgba(150, 165, 205, 0.12);
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 1px solid rgba(150, 165, 205, 0.1);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rng-simulator-panel > header > div:first-child {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -50,10 +43,9 @@
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.rng-simulator-count,
|
||||
.rng-simulator-active-rate {
|
||||
.rng-simulator-count {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 8px;
|
||||
padding: 3px 7px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.28);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-accent-gold);
|
||||
|
|
@ -62,20 +54,35 @@
|
|||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.rng-simulator-rate-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.9fr) minmax(260px, 1.3fr);
|
||||
gap: var(--space-3);
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-rate-form {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr)) auto;
|
||||
gap: var(--space-3);
|
||||
align-items: end;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border: 1px solid rgba(150, 165, 205, 0.12);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.rng-simulator-rates {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 220px;
|
||||
min-height: 72px;
|
||||
max-height: 176px;
|
||||
min-height: 52px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.rng-simulator-rates-stack {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-rate-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
|
@ -84,7 +91,7 @@
|
|||
min-width: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(11, 14, 27, 0.56);
|
||||
background: rgba(11, 14, 27, 0.54);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
@ -104,10 +111,10 @@
|
|||
}
|
||||
|
||||
.rng-simulator-rate-row > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 7px 8px;
|
||||
border: 0;
|
||||
|
|
@ -144,44 +151,57 @@
|
|||
padding: 3px;
|
||||
}
|
||||
|
||||
.rng-simulator-rate-form,
|
||||
.rng-simulator-settings {
|
||||
.rng-simulator-active-summary {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rng-simulator-settings {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-three-columns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-section {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid rgba(150, 165, 205, 0.1);
|
||||
}
|
||||
|
||||
.rng-simulator-summary-panel > header + .rng-simulator-section {
|
||||
padding-top: var(--space-2);
|
||||
border-top: 0;
|
||||
.rng-simulator-active-summary > span,
|
||||
.rng-simulator-stat {
|
||||
display: grid;
|
||||
align-content: space-between;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 90px;
|
||||
padding: 9px;
|
||||
border: 1px solid rgba(150, 165, 205, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(5, 7, 17, 0.22);
|
||||
}
|
||||
|
||||
.rng-simulator-section-title {
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.rng-simulator-subtitle {
|
||||
.rng-simulator-active-summary small,
|
||||
.rng-simulator-table > span:nth-child(-n + 3),
|
||||
.rng-simulator-history-metrics small {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rng-simulator-stat span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.rng-simulator-active-summary strong,
|
||||
.rng-simulator-stat strong,
|
||||
.rng-simulator-table strong,
|
||||
.rng-simulator-history-row b,
|
||||
.rng-simulator-history-rate > span,
|
||||
.rng-simulator-history-metrics b {
|
||||
color: var(--color-accent-gold);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.rng-simulator-rate-form .rng-simulator-form-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.rng-simulator-field {
|
||||
|
|
@ -198,9 +218,85 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.rng-simulator-error {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
color: #fca5a5;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.rng-simulator-tabs-panel > header {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-tabs {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.rng-simulator-tabs.tabs.nds-radio-button-group[data-size="sm"] > button {
|
||||
min-width: 96px;
|
||||
min-height: 34px;
|
||||
padding-block: 8px;
|
||||
padding-inline: var(--space-3);
|
||||
}
|
||||
|
||||
.rng-simulator-tab-content {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-section {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid rgba(150, 165, 205, 0.1);
|
||||
}
|
||||
|
||||
.rng-simulator-tab-content > .rng-simulator-section:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-section-title {
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.rng-simulator-primary-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(180px, 1fr);
|
||||
gap: var(--space-4);
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.rng-simulator-primary-column,
|
||||
.rng-simulator-cost-column {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.rng-simulator-cost-column {
|
||||
padding-left: var(--space-4);
|
||||
border-left: 1px solid rgba(150, 165, 205, 0.12);
|
||||
}
|
||||
|
||||
.rng-simulator-percent-input {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
@ -211,90 +307,88 @@
|
|||
font-weight: 900;
|
||||
}
|
||||
|
||||
.rng-simulator-error {
|
||||
margin: 0;
|
||||
color: #fca5a5;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.rng-simulator-form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.rng-simulator-occurrence-control {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(160px, 1fr) minmax(130px, 0.7fr);
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
justify-content: flex-end;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.rng-simulator-stat-grid,
|
||||
.rng-simulator-targets,
|
||||
.rng-simulator-time-grid {
|
||||
.rng-simulator-time-results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rng-simulator-stat-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-targets {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-targets .rng-simulator-stat strong {
|
||||
overflow-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.rng-simulator-time-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-stat {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(5, 7, 17, 0.32);
|
||||
}
|
||||
|
||||
.rng-simulator-stat span {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rng-simulator-stat strong {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-panel {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.rng-simulator-table {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(150, 165, 205, 0.1);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(150, 165, 205, 0.08);
|
||||
}
|
||||
|
||||
.rng-simulator-table > * {
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
}
|
||||
|
||||
.rng-simulator-table > span:not(:nth-child(-n + 3)) {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.rng-simulator-target-table {
|
||||
grid-template-columns: minmax(88px, 0.6fr) minmax(140px, 1fr) minmax(120px, 1fr);
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
align-items: stretch;
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid rgba(150, 165, 205, 0.12);
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
.rng-simulator-simulation-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-actions {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-actions > .nds-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rng-simulator-history-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +406,14 @@
|
|||
width: 150px;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-draws {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-draws > .nds-input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.rng-simulator-history {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
|
@ -320,8 +422,7 @@
|
|||
|
||||
.rng-simulator-history-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px 12px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
|
|
@ -332,26 +433,43 @@
|
|||
|
||||
.rng-simulator-history-row.is-latest {
|
||||
border-color: rgba(139, 92, 246, 0.38);
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.18), rgba(246, 196, 83, 0.06)),
|
||||
rgba(11, 14, 27, 0.52);
|
||||
}
|
||||
|
||||
.rng-simulator-history-row > div {
|
||||
.rng-simulator-history-row > .rng-simulator-history-heading {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-history-rate {
|
||||
justify-items: end;
|
||||
text-align: right;
|
||||
.rng-simulator-history-row .rng-simulator-history-identity {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-history-row b,
|
||||
.rng-simulator-history-rate > span,
|
||||
.rng-simulator-history-metrics b {
|
||||
color: var(--color-accent-gold);
|
||||
font-weight: 900;
|
||||
font-variant-numeric: tabular-nums;
|
||||
.rng-simulator-history-row .rng-simulator-history-score {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-history-row .rng-simulator-history-score b {
|
||||
color: var(--color-text-primary);
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rng-simulator-history-rate {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
justify-items: end;
|
||||
min-width: 72px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rng-simulator-history-metrics {
|
||||
|
|
@ -371,44 +489,54 @@
|
|||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.rng-simulator-history-metrics small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rng-simulator-empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.rng-simulator-grid,
|
||||
.rng-simulator-settings,
|
||||
.rng-simulator-three-columns,
|
||||
.rng-simulator-rate-layout,
|
||||
.rng-simulator-rate-form,
|
||||
.rng-simulator-active-summary,
|
||||
.rng-simulator-primary-controls,
|
||||
.rng-simulator-stat-grid,
|
||||
.rng-simulator-targets,
|
||||
.rng-simulator-time-grid {
|
||||
.rng-simulator-simulation-summary,
|
||||
.rng-simulator-occurrence-control,
|
||||
.rng-simulator-time-results,
|
||||
.rng-simulator-target-table {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.rng-simulator-cost-column {
|
||||
padding-top: var(--space-3);
|
||||
padding-left: 0;
|
||||
border-top: 1px solid rgba(150, 165, 205, 0.12);
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.rng-simulator-history-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.rng-simulator-history-rate {
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
.rng-simulator-history-row > .rng-simulator-history-heading {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.rng-simulator-history-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-bar {
|
||||
justify-content: flex-start;
|
||||
.rng-simulator-history-rate {
|
||||
grid-column: 1 / -1;
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-overview,
|
||||
.rng-simulator-simulation-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rng-simulator-simulation-overview {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue