Wave 3: refactor generators batch 1 to crypto random
- refactor(generators): migrate Card, Coin, Color, Country to crypto random - refactor(generators): migrate Date, Emoji, FontPair to crypto random - refactor(dice): deduplicate DiceGenerator by importing from dice-engine.ts
This commit is contained in:
+54
-2
@@ -8,7 +8,7 @@
|
||||
"plan_name": "critical-refactor",
|
||||
"status": "active",
|
||||
"started_at": "2026-05-13T16:25:19.266Z",
|
||||
"updated_at": "2026-05-13T16:42:05.595Z",
|
||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
||||
"session_ids": [
|
||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||
],
|
||||
@@ -42,6 +42,32 @@
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T16:42:05.595Z",
|
||||
"elapsed_ms": 68147
|
||||
},
|
||||
"todo:6": {
|
||||
"task_key": "todo:6",
|
||||
"task_label": "6",
|
||||
"task_title": "Refactor CardGenerator.astro",
|
||||
"session_id": "ses_1ddc76889ffe240ehScl9taAza",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T16:47:10.433Z",
|
||||
"started_at": "2026-05-13T16:44:57.812Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T16:47:10.433Z",
|
||||
"elapsed_ms": 132621
|
||||
},
|
||||
"todo:10": {
|
||||
"task_key": "todo:10",
|
||||
"task_label": "10",
|
||||
"task_title": "Refactor DateGenerator.astro",
|
||||
"session_id": "ses_1ddc380cbffe0YA1da3ew6QlNf",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
||||
"started_at": "2026-05-13T16:49:43.859Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:16:10.363Z",
|
||||
"elapsed_ms": 1586504
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +75,7 @@
|
||||
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md",
|
||||
"started_at": "2026-05-13T16:25:19.266Z",
|
||||
"status": "active",
|
||||
"updated_at": "2026-05-13T16:42:05.595Z",
|
||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
||||
"session_ids": [
|
||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||
],
|
||||
@@ -83,6 +109,32 @@
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T16:42:05.595Z",
|
||||
"elapsed_ms": 68147
|
||||
},
|
||||
"todo:6": {
|
||||
"task_key": "todo:6",
|
||||
"task_label": "6",
|
||||
"task_title": "Refactor CardGenerator.astro",
|
||||
"session_id": "ses_1ddc76889ffe240ehScl9taAza",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T16:47:10.433Z",
|
||||
"started_at": "2026-05-13T16:44:57.812Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T16:47:10.433Z",
|
||||
"elapsed_ms": 132621
|
||||
},
|
||||
"todo:10": {
|
||||
"task_key": "todo:10",
|
||||
"task_label": "10",
|
||||
"task_title": "Refactor DateGenerator.astro",
|
||||
"session_id": "ses_1ddc380cbffe0YA1da3ew6QlNf",
|
||||
"agent": "Sisyphus-Junior",
|
||||
"category": "unspecified-high",
|
||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
||||
"started_at": "2026-05-13T16:49:43.859Z",
|
||||
"status": "completed",
|
||||
"ended_at": "2026-05-13T17:16:10.363Z",
|
||||
"elapsed_ms": 1586504
|
||||
}
|
||||
},
|
||||
"agent": "atlas"
|
||||
|
||||
@@ -427,7 +427,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
- Message: `feat(components): add ResultBox and CopyButton shared components`
|
||||
- Files: `src/components/ResultBox.astro`, `src/components/CopyButton.astro`
|
||||
|
||||
- [ ] 6. Refactor CardGenerator.astro
|
||||
- [x] 6. Refactor CardGenerator.astro
|
||||
|
||||
**What to do**:
|
||||
- Replace `Math.random()` with `random.ts` functions
|
||||
@@ -457,7 +457,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
|
||||
**Commit**: NO (groups with batch)
|
||||
|
||||
- [ ] 7. Refactor CoinGenerator.astro
|
||||
- [x] 7. Refactor CoinGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -467,7 +467,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**QA Scenarios**: grep Math.random → empty
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 8. Refactor ColorGenerator.astro
|
||||
- [x] 8. Refactor ColorGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -477,7 +477,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**QA Scenarios**: grep Math.random → empty
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 9. Refactor CountryGenerator.astro
|
||||
- [x] 9. Refactor CountryGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -487,7 +487,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**QA Scenarios**: grep Math.random → empty
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 10. Refactor DateGenerator.astro
|
||||
- [x] 10. Refactor DateGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -497,7 +497,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**QA Scenarios**: grep Math.random → empty
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 11. Refactor EmojiGenerator.astro
|
||||
- [x] 11. Refactor EmojiGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -507,7 +507,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
**QA Scenarios**: grep Math.random → empty
|
||||
**Commit**: NO
|
||||
|
||||
- [ ] 12. Refactor FontPairGenerator.astro
|
||||
- [x] 12. Refactor FontPairGenerator.astro
|
||||
|
||||
**What to do**: Same pattern as Task 6.
|
||||
**Recommended Agent Profile**: `unspecified-high`
|
||||
@@ -685,7 +685,7 @@ Max Concurrent: 8 (Wave 3)
|
||||
- Message: `refactor(generators): migrate batch 4 to crypto random (Teams, Time, Uuid, Weighted, WheelSpinner, YesNo)`
|
||||
- Files: `src/components/generators/{Teams,Time,Uuid,Weighted,YesNo}Generator.astro`, `src/components/generators/WheelSpinner.astro`
|
||||
|
||||
- [ ] 33. Refactor DiceGenerator to import dice-engine.ts
|
||||
- [x] 33. Refactor DiceGenerator to import dice-engine.ts
|
||||
|
||||
**What to do**:
|
||||
- Replace inline dice engine code in `DiceGenerator.astro:274-958` with imports from `src/lib/dice-engine.ts`
|
||||
|
||||
@@ -116,19 +116,15 @@ const T = useT(isRu ? "ru" : "en");
|
||||
<script>
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { randomInt, shuffleArray } from "@/lib/client/random";
|
||||
import { getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
||||
const ERR_AT_LEAST = isRu
|
||||
? "Вытащите хотя бы 1 карту."
|
||||
: "Draw at least 1 card.";
|
||||
const ERR_UNIQUE = isRu
|
||||
? "Нельзя вытащить более 52 уникальных карт."
|
||||
: "Cannot draw more than 52 unique cards.";
|
||||
const ERR_MAX52 = isRu
|
||||
? "Максимум 52 карты одновременно."
|
||||
: "Maximum 52 cards at once.";
|
||||
const T = getClientT();
|
||||
const COPY_LABEL = T.copy;
|
||||
const COPIED_LABEL = T.copied;
|
||||
const ERR_AT_LEAST = T.errDrawAtLeast;
|
||||
const ERR_UNIQUE = T.errMaxUniqueCards;
|
||||
const ERR_MAX52 = T.errMax52Cards;
|
||||
|
||||
const countInput = document.getElementById("crd-count") as HTMLInputElement;
|
||||
const replaceCb = document.getElementById("crd-replace") as HTMLInputElement;
|
||||
@@ -195,15 +191,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
if (withReplacement) {
|
||||
lastCards = Array.from(
|
||||
{ length: count },
|
||||
() => DECK[Math.floor(Math.random() * DECK.length)],
|
||||
() => DECK[randomInt(0, DECK.length - 1)],
|
||||
);
|
||||
} else {
|
||||
const deck = [...DECK];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const j = i + Math.floor(Math.random() * (deck.length - i));
|
||||
[deck[i], deck[j]] = [deck[j], deck[i]];
|
||||
}
|
||||
lastCards = deck.slice(0, count);
|
||||
lastCards = shuffleArray(DECK).slice(0, count);
|
||||
}
|
||||
|
||||
renderCards();
|
||||
|
||||
@@ -100,16 +100,18 @@ const T = useT(isRu ? "ru" : "en");
|
||||
|
||||
<script>
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { randomFloat } from "@/lib/client/random";
|
||||
import { getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
||||
const HEADS_LABEL = isRu ? "О" : "H";
|
||||
const TAILS_LABEL = isRu ? "Р" : "T";
|
||||
const HEADS_WORD = isRu ? "орёл" : "Heads";
|
||||
const TAILS_WORD = isRu ? "решка" : "Tails";
|
||||
const HEADS_SUMMARY = isRu ? "орёл" : "heads";
|
||||
const TAILS_SUMMARY = isRu ? "решка" : "tails";
|
||||
const T = getClientT();
|
||||
const COPY_LABEL = T.copy;
|
||||
const COPIED_LABEL = T.copied;
|
||||
const HEADS_LABEL = T.headsLabel;
|
||||
const TAILS_LABEL = T.tailsLabel;
|
||||
const HEADS_WORD = T.headsWord;
|
||||
const TAILS_WORD = T.tailsWord;
|
||||
const HEADS_SUMMARY = T.headsWord;
|
||||
const TAILS_SUMMARY = T.tailsWord;
|
||||
|
||||
const countInput = document.getElementById("coin-count") as HTMLInputElement;
|
||||
const countVal = document.getElementById("coin-count-val") as HTMLSpanElement;
|
||||
@@ -135,7 +137,7 @@ const T = useT(isRu ? "ru" : "en");
|
||||
|
||||
function flip() {
|
||||
const count = parseInt(countInput.value, 10);
|
||||
lastResults = Array.from({ length: count }, () => Math.random() < 0.5);
|
||||
lastResults = Array.from({ length: count }, () => randomFloat() < 0.5);
|
||||
|
||||
const heads = lastResults.filter(Boolean).length;
|
||||
const tails = count - heads;
|
||||
|
||||
@@ -95,6 +95,10 @@ const T = useT(isRu ? "ru" : "en");
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const T = getClientT();
|
||||
const swatch = document.getElementById("cg-swatch") as HTMLDivElement;
|
||||
const valuesEl = document.getElementById("cg-values") as HTMLDivElement;
|
||||
const hexEl = document.getElementById("cg-hex") as HTMLSpanElement;
|
||||
@@ -166,9 +170,9 @@ const T = useT(isRu ? "ru" : "en");
|
||||
);
|
||||
|
||||
function generate() {
|
||||
const r = Math.floor(Math.random() * 256);
|
||||
const g = Math.floor(Math.random() * 256);
|
||||
const b = Math.floor(Math.random() * 256);
|
||||
const r = randomInt(0, 255);
|
||||
const g = randomInt(0, 255);
|
||||
const b = randomInt(0, 255);
|
||||
const [h, s, l] = rgbToHsl(r, g, b);
|
||||
|
||||
const hex = `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
||||
|
||||
@@ -124,6 +124,9 @@ const regions = isRu
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
interface Country {
|
||||
name: string;
|
||||
ruName: string;
|
||||
@@ -190,7 +193,9 @@ const regions = isRu
|
||||
const checkIcon = document.getElementById("cg-check-icon") as SVGElement;
|
||||
const copyText = document.getElementById("cg-copy-text") as HTMLSpanElement;
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const lang = getClientLang();
|
||||
const isRu = lang === "ru";
|
||||
const T = getClientT();
|
||||
|
||||
function formatPopulation(n: number): string {
|
||||
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1).replace(/\.0$/, "") + "B";
|
||||
@@ -208,7 +213,7 @@ const regions = isRu
|
||||
function generate() {
|
||||
const list = getFilteredCountries();
|
||||
if (list.length === 0) return;
|
||||
const country = list[Math.floor(Math.random() * list.length)];
|
||||
const country = list[randomInt(0, list.length - 1)];
|
||||
|
||||
flagEl.textContent = country.flagEmoji;
|
||||
nameEl.textContent = isRu ? country.ruName : country.name;
|
||||
@@ -235,13 +240,13 @@ const regions = isRu
|
||||
|
||||
copyIcon.classList.add("hidden");
|
||||
checkIcon.classList.remove("hidden");
|
||||
copyText.textContent = isRu ? "Скопировано" : "Copied";
|
||||
copyText.textContent = T.copied;
|
||||
|
||||
if (copyTimer) clearTimeout(copyTimer);
|
||||
copyTimer = setTimeout(() => {
|
||||
checkIcon.classList.add("hidden");
|
||||
copyIcon.classList.remove("hidden");
|
||||
copyText.textContent = isRu ? "Копировать" : "Copy";
|
||||
copyText.textContent = T.copy;
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
|
||||
@@ -113,8 +113,12 @@ const T = useT(isRu ? "ru" : "en");
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { popElement } from "@/lib/client/animations";
|
||||
import { randomFloat } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const lang = getClientLang();
|
||||
const isRu = lang === "ru";
|
||||
const T = getClientT();
|
||||
const ERR_BOTH_DATES = isRu
|
||||
? "Укажите обе даты."
|
||||
: "Please enter both dates.";
|
||||
@@ -158,7 +162,7 @@ const T = useT(isRu ? "ru" : "en");
|
||||
|
||||
const fromMs = fromDate.getTime();
|
||||
const toMs = toDate.getTime();
|
||||
const randomMs = fromMs + Math.random() * (toMs - fromMs);
|
||||
const randomMs = fromMs + randomFloat() * (toMs - fromMs);
|
||||
const resultDate = new Date(randomMs);
|
||||
|
||||
const formatted = resultDate.toLocaleDateString(isRu ? "ru-RU" : "en-US", {
|
||||
|
||||
@@ -270,217 +270,19 @@ const presets = [
|
||||
|
||||
<!-- Script section appended to DiceGenerator.astro -->
|
||||
|
||||
<script type="module">
|
||||
// ═══ Inline Dice Engine ═══════════════════════════════════════════════════════
|
||||
<script>
|
||||
import { parseDiceNotation, rollDice, rollAdvantage, buildNotation } from "@/lib/dice-engine";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const EXPLODE_CAP = 100;
|
||||
const REROLL_CAP = 1000;
|
||||
|
||||
function rollDie(sides, explode, reroll) {
|
||||
let value = Math.floor(Math.random() * sides) + 1;
|
||||
const original = value;
|
||||
let rerolledFrom = null;
|
||||
const explosions = [];
|
||||
let exploded = false;
|
||||
|
||||
if (reroll.active && reroll.values.has(value)) {
|
||||
rerolledFrom = value;
|
||||
let rc = 0;
|
||||
while (reroll.values.has(value) && rc < REROLL_CAP) {
|
||||
value = Math.floor(Math.random() * sides) + 1;
|
||||
rc++;
|
||||
if (reroll.once) break;
|
||||
}
|
||||
}
|
||||
const isRu = getClientLang() === "ru";
|
||||
const T = getClientT();
|
||||
|
||||
const threshold = explode.threshold ?? sides;
|
||||
if (explode.active && value >= threshold) {
|
||||
exploded = true;
|
||||
let chain = 0;
|
||||
while (chain < EXPLODE_CAP) {
|
||||
let next = Math.floor(Math.random() * sides) + 1;
|
||||
if (explode.penetrating) next = Math.max(1, next - 1);
|
||||
explosions.push(next);
|
||||
if (next < threshold) break;
|
||||
chain++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
original,
|
||||
exploded,
|
||||
explosions,
|
||||
rerolledFrom,
|
||||
dropped: false,
|
||||
};
|
||||
}
|
||||
|
||||
function parseDiceNotation(raw) {
|
||||
const s = raw.replace(/\s+/g, "").toLowerCase();
|
||||
if (!s) return null;
|
||||
const main = s.match(/^(\d*)d(\d+)(.*)$/);
|
||||
if (!main) return null;
|
||||
const count = main[1] === "" ? 1 : parseInt(main[1], 10);
|
||||
const sides = parseInt(main[2], 10);
|
||||
let remaining = main[3];
|
||||
if (count < 1 || count > 20 || sides < 1 || sides > 9999) return null;
|
||||
|
||||
let keepDrop = null;
|
||||
const kd = remaining.match(/^(kh|kl|dh|dl)(\d+)/);
|
||||
if (kd) {
|
||||
const kc = parseInt(kd[2], 10);
|
||||
if (kc < 1 || kc >= count) return null;
|
||||
keepDrop = { type: kd[1], count: kc };
|
||||
remaining = remaining.slice(kd[0].length);
|
||||
}
|
||||
|
||||
const explode = { active: false, threshold: null, penetrating: false };
|
||||
const em = remaining.match(/^(!p|!)(>?)(\d*)/);
|
||||
if (em) {
|
||||
explode.active = true;
|
||||
explode.penetrating = em[1] === "!p";
|
||||
if (em[2] === ">" && em[3]) {
|
||||
explode.threshold = parseInt(em[3], 10);
|
||||
if (explode.threshold < 2 || explode.threshold > sides) return null;
|
||||
}
|
||||
remaining = remaining.slice(em[0].length);
|
||||
}
|
||||
|
||||
const reroll = {
|
||||
active: false,
|
||||
values: new Set(),
|
||||
once: false,
|
||||
operator: "eq",
|
||||
};
|
||||
const rm = remaining.match(/^(ro|r)([<>]?)(\d+)/);
|
||||
if (rm) {
|
||||
reroll.active = true;
|
||||
reroll.once = rm[1] === "ro";
|
||||
const op = rm[2],
|
||||
val = parseInt(rm[3], 10);
|
||||
if (op === "<") {
|
||||
reroll.operator = "lt";
|
||||
for (let i = 1; i < val && i < sides; i++) reroll.values.add(i);
|
||||
} else if (op === ">") {
|
||||
reroll.operator = "gt";
|
||||
for (let i = val + 1; i <= sides; i++) reroll.values.add(i);
|
||||
} else {
|
||||
reroll.operator = "eq";
|
||||
reroll.values.add(val);
|
||||
}
|
||||
remaining = remaining.slice(rm[0].length);
|
||||
}
|
||||
|
||||
let modifier = 0;
|
||||
const mm = remaining.match(/^([+-]\d+)$/);
|
||||
if (mm) {
|
||||
modifier = parseInt(mm[1], 10);
|
||||
if (Math.abs(modifier) > 999) return null;
|
||||
remaining = remaining.slice(mm[0].length);
|
||||
}
|
||||
if (remaining.length > 0) return null;
|
||||
|
||||
return {
|
||||
count,
|
||||
sides,
|
||||
modifier,
|
||||
keepDrop,
|
||||
explode,
|
||||
reroll,
|
||||
advantage: false,
|
||||
disadvantage: false,
|
||||
};
|
||||
}
|
||||
|
||||
function rollDice(parsed) {
|
||||
const dice = [];
|
||||
for (let i = 0; i < parsed.count; i++)
|
||||
dice.push(rollDie(parsed.sides, parsed.explode, parsed.reroll));
|
||||
|
||||
let kept = [...dice];
|
||||
let dropped = [];
|
||||
|
||||
if (parsed.keepDrop) {
|
||||
const indexed = dice.map((d, i) => ({ die: d, idx: i }));
|
||||
if (parsed.keepDrop.type === "kh")
|
||||
indexed.sort((a, b) => b.die.value - a.die.value);
|
||||
else if (parsed.keepDrop.type === "kl")
|
||||
indexed.sort((a, b) => a.die.value - b.die.value);
|
||||
else if (parsed.keepDrop.type === "dh")
|
||||
indexed.sort((a, b) => b.die.value - a.die.value);
|
||||
else if (parsed.keepDrop.type === "dl")
|
||||
indexed.sort((a, b) => a.die.value - b.die.value);
|
||||
|
||||
const keepN = parsed.keepDrop.type.startsWith("k")
|
||||
? parsed.keepDrop.count
|
||||
: parsed.count - parsed.keepDrop.count;
|
||||
const keepSet = new Set(indexed.slice(0, keepN).map((x) => x.idx));
|
||||
kept = dice.filter((_, i) => keepSet.has(i));
|
||||
dropped = dice.filter((_, i) => !keepSet.has(i));
|
||||
}
|
||||
dropped.forEach((d) => (d.dropped = true));
|
||||
|
||||
let total = kept.reduce((s, d) => s + d.value, 0);
|
||||
total += kept.reduce(
|
||||
(s, d) => s + d.explosions.reduce((ss, v) => ss + v, 0),
|
||||
0,
|
||||
);
|
||||
total += parsed.modifier;
|
||||
|
||||
return {
|
||||
dice,
|
||||
kept,
|
||||
dropped,
|
||||
modifier: parsed.modifier,
|
||||
total,
|
||||
notation: "",
|
||||
advantageRolls: null,
|
||||
};
|
||||
}
|
||||
|
||||
function rollAdvantage(sides, modifier, advantage) {
|
||||
const r1 = Math.floor(Math.random() * sides) + 1;
|
||||
const r2 = Math.floor(Math.random() * sides) + 1;
|
||||
const kv = advantage ? Math.max(r1, r2) : Math.min(r1, r2);
|
||||
const die = {
|
||||
value: kv,
|
||||
original: kv,
|
||||
exploded: false,
|
||||
explosions: [],
|
||||
rerolledFrom: null,
|
||||
dropped: false,
|
||||
};
|
||||
return {
|
||||
dice: [die],
|
||||
kept: [die],
|
||||
dropped: [],
|
||||
modifier,
|
||||
total: kv + modifier,
|
||||
notation: "",
|
||||
advantageRolls: [r1, r2],
|
||||
};
|
||||
}
|
||||
|
||||
function buildNotation(count, sides, modifier) {
|
||||
let s = `${count}d${sides}`;
|
||||
if (modifier > 0) s += `+${modifier}`;
|
||||
else if (modifier < 0) s += modifier;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ═══ DOM + Logic ══════════════════════════════════════════════════════════════
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
|
||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
||||
const ERR_COUNT = isRu
|
||||
? "Количество кубиков должно быть не менее 1."
|
||||
: "Number of dice must be at least 1.";
|
||||
const ERR_MAX = isRu
|
||||
? "Максимум 20 кубиков одновременно."
|
||||
: "Maximum 20 dice at once.";
|
||||
const COPY_LABEL = T.copy;
|
||||
const COPIED_LABEL = T.copied;
|
||||
const ERR_COUNT = T.errDiceAtLeast;
|
||||
const ERR_MAX = T.errDiceMax;
|
||||
const ERR_NOTATION = isRu
|
||||
? "Неверный формат — попробуйте 2d6+3"
|
||||
: "Invalid notation — try 2d6+3";
|
||||
|
||||
@@ -139,12 +139,15 @@ const clickToCopyLabel = isRu ? "Нажмите, чтобы скопироват
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||
import { popElement } from "@/lib/client/animations";
|
||||
import { shuffleArray } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const COPIED_LABEL = isRu ? "Скопировано!" : "Copied!";
|
||||
const COPY_ALL_LABEL = isRu ? "Копировать всё" : "Copy all";
|
||||
const isRu = getClientLang() === "ru";
|
||||
const T = getClientT();
|
||||
const COPIED_LABEL = T.copied;
|
||||
const COPY_ALL_LABEL = T.copyAll;
|
||||
const ERR_COUNT_RANGE = isRu ? "Укажите количество от 1 до 50." : "Enter a count between 1 and 50.";
|
||||
const TOOLTIP_COPIED = isRu ? "Скопировано!" : "Copied!";
|
||||
const TOOLTIP_COPIED = T.copied;
|
||||
|
||||
const EMOJIS: Record<string, string[]> = {
|
||||
smileys: ["😀","😃","😄","😁","😆","😅","😂","🤣","😊","😇","🙂","🙃","😉","😌","😍","🥰","😘","😗","😙","😚","😋","😛","😝","😜","🤪","🤨","🧐","🤓","😎","🥸","🤩","🥳","😏","😒","😞","😔","😟","😕","🙁","☹️","😣","😖","😫","😩","🥺","😢","😭","😤","😠","😡","🤬","🤯","😳","🥵","🥶","😱","😨","😰","😥","😓","🤗","🤔","🤭","🤫","🤥","😶","😐","😑","😬","🙄","😯","😦","😧","😮","😲","🥱","😴","🤤","😪","😵","🤐","🥴","🤢","🤮","🤧","😷","🤒","🤕","🤑","🤠","😈","👿","👹","👺","🤡","💩","👻","💀","☠️","👽","👾","🤖","🎃","😺","😸","😹","😻","😼","😽","🙀","😿","😾"],
|
||||
@@ -552,15 +555,6 @@ const clickToCopyLabel = isRu ? "Нажмите, чтобы скопироват
|
||||
|
||||
const errors = createErrorDisplay(errorEl);
|
||||
|
||||
function shuffleArray<T>(arr: T[]): T[] {
|
||||
const a = [...arr];
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function getEmojiName(emoji: string): string {
|
||||
if (isRu) {
|
||||
return EMOJI_NAMES_RU[emoji] || EMOJI_NAMES_EN[emoji] || emoji;
|
||||
|
||||
@@ -170,8 +170,11 @@ const i18n = {
|
||||
|
||||
<script>
|
||||
import { createErrorDisplay } from "@/lib/client/validation";
|
||||
import { randomInt } from "@/lib/client/random";
|
||||
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||
|
||||
const isRu = document.documentElement.lang === "ru";
|
||||
const isRu = getClientLang() === "ru";
|
||||
const clientT = getClientT();
|
||||
|
||||
const generateBtn = document.getElementById("fp-btn") as HTMLButtonElement;
|
||||
const shuffleHeadingBtn = document.getElementById("fp-shuffle-heading") as HTMLButtonElement;
|
||||
@@ -309,7 +312,7 @@ const i18n = {
|
||||
let loadedFonts = new Set<string>();
|
||||
|
||||
function randomItem<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
return arr[randomInt(0, arr.length - 1)];
|
||||
}
|
||||
|
||||
function getGoogleFontsUrl(heading: Font, body: Font): string {
|
||||
@@ -472,7 +475,7 @@ const i18n = {
|
||||
await navigator.clipboard.writeText(fullText);
|
||||
|
||||
const originalHtml = copyBtn.innerHTML;
|
||||
copyBtn.innerHTML = `<span class="inline-flex items-center gap-2">${checkIconSvg} ${isRu ? "Скопировано!" : "Copied!"}</span>`;
|
||||
copyBtn.innerHTML = `<span class="inline-flex items-center gap-2">${checkIconSvg} ${clientT.copied}</span>`;
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = originalHtml;
|
||||
}, 1500);
|
||||
|
||||
Reference in New Issue
Block a user