diff --git a/app/main/diceRoll/diceRollSessionStore.test.ts b/app/main/diceRoll/diceRollSessionStore.test.ts new file mode 100644 index 0000000..187af2b --- /dev/null +++ b/app/main/diceRoll/diceRollSessionStore.test.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { DiceRollSessionStore } from './diceRollSessionStore'; +import { + D100_FACE_VALUES, + formatPredeterminedDiceNotation, + snapD100Value, +} from '../../shared/types/diceRoll'; + +void test('diceRoll: roll produces values in range and notation', () => { + const store = new DiceRollSessionStore(); + store.dispatch({ kind: 'roll', count: 3, sides: 6 }); + const st = store.getState(); + assert.equal(st.active, true); + assert.equal(st.notation, '3d6'); + assert.equal(st.values.length, 3); + assert.ok(st.values.every((v) => v >= 1 && v <= 6)); + assert.equal(st.total, st.values.reduce((a, b) => a + b, 0)); +}); + +void test('diceRoll: d100 values are tens faces only', () => { + const store = new DiceRollSessionStore(); + store.dispatch({ kind: 'roll', count: 5, sides: 100 }); + const st = store.getState(); + assert.equal(st.notation, '5d100'); + assert.equal(st.values.length, 5); + const allowed = new Set(D100_FACE_VALUES); + assert.ok(st.values.every((v) => allowed.has(v))); + assert.equal(st.total, st.values.reduce((a, b) => a + b, 0)); +}); + +void test('diceRoll: clear resets', () => { + const store = new DiceRollSessionStore(); + store.dispatch({ kind: 'roll', count: 1, sides: 20 }); + store.dispatch({ kind: 'clear' }); + assert.equal(store.getState().active, false); + assert.deepEqual(store.getState().values, []); +}); + +void test('diceRoll: hideOverlay keeps values for master UI', () => { + const store = new DiceRollSessionStore(); + store.dispatch({ kind: 'roll', count: 2, sides: 6 }); + const values = [...store.getState().values]; + const total = store.getState().total; + store.dispatch({ kind: 'hideOverlay' }); + assert.equal(store.getState().active, false); + assert.deepEqual(store.getState().values, values); + assert.equal(store.getState().total, total); + assert.equal(store.getState().notation, '2d6'); +}); + +void test('formatPredeterminedDiceNotation', () => { + assert.equal(formatPredeterminedDiceNotation(2, 20, [15, 7]), '2d20@15,7'); + assert.equal(formatPredeterminedDiceNotation(2, 100, [20, 100]), '2d100@20,100'); + assert.equal(formatPredeterminedDiceNotation(1, 100, [27]), '1d100@30'); +}); + +void test('snapD100Value', () => { + assert.equal(snapD100Value(20), 20); + assert.equal(snapD100Value(21), 30); + assert.equal(snapD100Value(100), 100); + assert.equal(snapD100Value(1), 10); +}); diff --git a/app/main/diceRoll/diceRollSessionStore.ts b/app/main/diceRoll/diceRollSessionStore.ts new file mode 100644 index 0000000..17c5aa5 --- /dev/null +++ b/app/main/diceRoll/diceRollSessionStore.ts @@ -0,0 +1,64 @@ +import { + clampDiceCount, + createInitialDiceRollState, + formatDiceNotation, + isDiceSides, + rollDiceValues, + type DiceRollEvent, + type DiceRollState, +} from '../../shared/types/diceRoll'; + +export class DiceRollSessionStore { + private state: DiceRollState = createInitialDiceRollState(); + + getState(): DiceRollState { + return this.state; + } + + clear(): DiceRollState { + this.state = createInitialDiceRollState(); + return this.state; + } + + dispatch(event: DiceRollEvent): DiceRollState { + switch (event.kind) { + case 'clear': { + if (!this.state.active && this.state.values.length === 0) return this.state; + this.state = { + ...createInitialDiceRollState(), + revision: this.state.revision + 1, + }; + return this.state; + } + case 'hideOverlay': { + if (!this.state.active) return this.state; + this.state = { + ...this.state, + revision: this.state.revision + 1, + active: false, + startedAt: null, + }; + return this.state; + } + case 'roll': { + if (!isDiceSides(event.sides)) return this.state; + const count = clampDiceCount(event.count); + const values = rollDiceValues(count, event.sides); + const total = values.reduce((a, b) => a + b, 0); + this.state = { + revision: this.state.revision + 1, + active: true, + notation: formatDiceNotation(count, event.sides), + values, + total, + startedAt: Date.now(), + }; + return this.state; + } + default: { + const _exhaustive: never = event; + return _exhaustive; + } + } + } +} diff --git a/app/main/index.ts b/app/main/index.ts index cd350d2..bdc33c5 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -37,6 +37,7 @@ import { MaterialsOverlayStore } from './materials/materialsOverlayStore'; import { NpcsOverlayStore } from './npcs/npcsOverlayStore'; import { TimerSessionStore } from './sessionTimer/timerSessionStore'; import { InitiativeSessionStore } from './initiative/initiativeSessionStore'; +import { DiceRollSessionStore } from './diceRoll/diceRollSessionStore'; import { ZipProjectStore } from './project/zipStore'; import { SceneViewStore } from './sceneView/sceneViewStore'; import { registerDndAssetProtocol } from './protocol/dndAssetProtocol'; @@ -72,6 +73,7 @@ import { openPlaylistsWindow, openTimerWindow, openInitiativeWindow, + openDiceWindow, openMultiWindow, openNpcsEditorWindow, openSceneEditorWindow, @@ -82,6 +84,7 @@ import { closePlaylistsWindow, closeTimerWindow, closeInitiativeWindow, + closeDiceWindow, closeNpcsEditorWindow, closeSceneEditorWindow, closeTokenPathEditorWindow, @@ -187,6 +190,7 @@ const materialsOverlayStore = new MaterialsOverlayStore(); const npcsOverlayStore = new NpcsOverlayStore(); const timerSessionStore = new TimerSessionStore(); const initiativeSessionStore = new InitiativeSessionStore(); +const diceRollSessionStore = new DiceRollSessionStore(); const sceneTokensSessionStore = new SceneTokensSessionStore(); const tokenPathSessionStore = new TokenPathSessionStore(); const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore(); @@ -300,6 +304,18 @@ function emitInitiativeState(): void { } } +function emitDiceRollState(): void { + const state = diceRollSessionStore.getState(); + for (const win of BrowserWindow.getAllWindows()) { + if (win.isDestroyed() || win.webContents.isDestroyed()) continue; + try { + win.webContents.send(ipcChannels.diceRoll.stateChanged, { state }); + } catch { + /* teardown */ + } + } +} + function syncInitiativePresenceFromSession(): void { const project = projectStore.getOpenProject(); const sceneId = project?.currentSceneId ?? null; @@ -637,6 +653,7 @@ async function main() { tokenGridSnapSessionStore.reset(); timerSessionStore.clear(); initiativeSessionStore.clear(); + diceRollSessionStore.clear(); if (USERS_BRANCH_FEATURES_ENABLED) { const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : []; if (playerIds.length > 0) { @@ -665,15 +682,18 @@ async function main() { emitSessionTimerState(); syncInitiativePresenceFromSession(); emitInitiativeState(); + emitDiceRollState(); return { ok: true }; }); registerHandler(ipcChannels.windows.closeMultiWindow, () => { closeMultiWindow(); timerSessionStore.clear(); initiativeSessionStore.clear(); + diceRollSessionStore.clear(); tokenGridSnapSessionStore.reset(); emitSessionTimerState(); emitInitiativeState(); + emitDiceRollState(); emitTokenGridSnapState(); return { ok: true }; }); @@ -755,6 +775,14 @@ async function main() { emitInitiativeState(); return { ok: true }; }); + registerHandler(ipcChannels.windows.openDice, () => { + openDiceWindow(); + return { ok: true }; + }); + registerHandler(ipcChannels.windows.closeDice, () => { + closeDiceWindow(); + return { ok: true }; + }); registerHandler(ipcChannels.windows.openNpcsEditor, () => { openNpcsEditorWindow(); return { ok: true }; @@ -827,6 +855,14 @@ async function main() { emitInitiativeState(); return { state: initiativeSessionStore.getState() }; }); + registerHandler(ipcChannels.diceRoll.getState, () => { + return { state: diceRollSessionStore.getState() }; + }); + registerHandler(ipcChannels.diceRoll.dispatch, ({ event }) => { + diceRollSessionStore.dispatch(event); + emitDiceRollState(); + return { state: diceRollSessionStore.getState() }; + }); registerHandler(ipcChannels.project.list, async () => { const projects = await projectStore.listProjects(); @@ -870,6 +906,7 @@ async function main() { npcsOverlayStore.clear(); timerSessionStore.clear(); initiativeSessionStore.clear(); + diceRollSessionStore.clear(); sceneDarknessStore.resetSession(); sceneMarkerStore.resetSession(); sceneTrapsStore.resetSession(); @@ -884,6 +921,7 @@ async function main() { emitNpcsOverlayState(); emitSessionTimerState(); emitInitiativeState(); + emitDiceRollState(); emitSceneDarknessState(); emitSceneMarkerState(); emitSceneTrapsState(); diff --git a/app/main/windows/createWindows.editorClose.test.ts b/app/main/windows/createWindows.editorClose.test.ts index fc5b1ed..71b5709 100644 --- a/app/main/windows/createWindows.editorClose.test.ts +++ b/app/main/windows/createWindows.editorClose.test.ts @@ -68,6 +68,14 @@ void test('createWindows: окно инициативы закрывается assert.match(src, /closePlaySessionAuxiliaryWindows[\s\S]*closeInitiativeWindow/); }); +void test('createWindows: окно кубиков закрывается с multi-window', () => { + const src = readCreateWindows(); + assert.ok(src.includes('openDiceWindow')); + assert.ok(src.includes('closeDiceWindow')); + assert.ok(src.includes("createWindow('dice'")); + assert.match(src, /closePlaySessionAuxiliaryWindows[\s\S]*closeDiceWindow/); +}); + void test('createWindows: окно НПС закрывается с multi-window', () => { const src = readCreateWindows(); assert.ok(src.includes('openNpcsWindow')); diff --git a/app/main/windows/createWindows.ts b/app/main/windows/createWindows.ts index 1a6a3bc..b97b2d3 100644 --- a/app/main/windows/createWindows.ts +++ b/app/main/windows/createWindows.ts @@ -23,6 +23,7 @@ export type WindowKind = | 'playlists' | 'timer' | 'initiative' + | 'dice' | 'npcsEditor' | 'sceneEditor' | 'tokenPathEditor' @@ -69,6 +70,10 @@ const TIMER_WINDOW_HEIGHT = 420; const INITIATIVE_WINDOW_WIDTH = 920; const INITIATIVE_WINDOW_HEIGHT = 420; +/** Компактное окно броска кубиков. */ +const DICE_WINDOW_WIDTH = 380; +const DICE_WINDOW_HEIGHT = 420; + /** Редактор НПС — как основной редактор, шире инспектор. */ const NPCS_EDITOR_WINDOW_WIDTH = 1400; const NPCS_EDITOR_WINDOW_HEIGHT = 860; @@ -181,6 +186,7 @@ function closePlaySessionAuxiliaryWindows(): void { closePlaylistsWindow(); closeTimerWindow(); closeInitiativeWindow(); + closeDiceWindow(); closeNpcsWindow(); } @@ -235,6 +241,8 @@ function pageNameForKind(kind: WindowKind): string { return 'timer.html'; case 'initiative': return 'initiative.html'; + case 'dice': + return 'dice.html'; case 'npcsEditor': return 'npcsEditor.html'; case 'sceneEditor': @@ -313,6 +321,7 @@ function windowSizeForKind(kind: WindowKind): { width: number; height: number } if (kind === 'playlists') return { width: PLAYLISTS_WINDOW_WIDTH, height: PLAYLISTS_WINDOW_HEIGHT }; if (kind === 'timer') return { width: TIMER_WINDOW_WIDTH, height: TIMER_WINDOW_HEIGHT }; if (kind === 'initiative') return { width: INITIATIVE_WINDOW_WIDTH, height: INITIATIVE_WINDOW_HEIGHT }; + if (kind === 'dice') return { width: DICE_WINDOW_WIDTH, height: DICE_WINDOW_HEIGHT }; if (kind === 'npcsEditor') return { width: NPCS_EDITOR_WINDOW_WIDTH, height: NPCS_EDITOR_WINDOW_HEIGHT }; if (kind === 'sceneEditor') return { width: 1280, height: 800 }; if (kind === 'tokenPathEditor') return { width: 1100, height: 760 }; @@ -636,6 +645,13 @@ export function closeInitiativeWindow(): void { } } +export function closeDiceWindow(): void { + const win = windows.get('dice'); + if (win && !win.isDestroyed()) { + win.close(); + } +} + export function closeNpcsEditorWindow(): void { const win = windows.get('npcsEditor'); if (win && !win.isDestroyed()) { @@ -890,6 +906,36 @@ export function openInitiativeWindow(opts?: { onClosed?: () => void }): void { }); } +export function openDiceWindow(): void { + const existing = windows.get('dice'); + if (existing && !existing.isDestroyed()) { + if (existing.isMinimized()) existing.restore(); + existing.show(); + existing.focus(); + existing.moveTop(); + return; + } + + const parent = windows.get('control') ?? windows.get('presentation'); + const win = createWindow('dice', parent ? { parent } : undefined); + const { width, height } = win.getBounds(); + const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds()); + const { x, y, width: dw, height: dh } = display.workArea; + win.setBounds({ + x: Math.round(x + Math.max(0, (dw - width) / 2)), + y: Math.round(y + Math.max(0, (dh - height) / 2)), + width, + height, + }); + win.webContents.once('did-finish-load', () => { + if (!win.isDestroyed()) { + win.show(); + win.focus(); + win.moveTop(); + } + }); +} + function positionNpcsEditorWindow(win: BrowserWindow): void { const parent = windows.get('editor'); const { width, height } = win.getBounds(); diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index 52290a4..44e9948 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -1875,6 +1875,22 @@ export function ControlApp() { ⚔ + + ))} + + +
{t('dice.count')}
+
+ +
+ {count} +
+ +
+ +
+ + +
+ + {state && state.values.length > 0 ? ( +
+
{t('dice.result')}
+
{state.notation}
+
+ {state.values.map((v, i) => ( + + {v} + + ))} +
+
+ {t('dice.total')}: {state.total} +
+ {state.active ?
{t('dice.rolling')}
: null} +
+ ) : ( +
{t('dice.noResult')}
+ )} + + ); +} diff --git a/app/renderer/dice/main.tsx b/app/renderer/dice/main.tsx new file mode 100644 index 0000000..341575d --- /dev/null +++ b/app/renderer/dice/main.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; + +import '../shared/ui/globals.css'; +import { EditorI18nProvider } from '../editor/i18n/EditorI18nContext'; +import { WindowErrorBoundary } from '../shared/ui/WindowErrorBoundary'; + +import { DiceApp } from './DiceApp'; + +const rootEl = document.getElementById('root'); +if (!rootEl) { + throw new Error('Missing #root element'); +} + +createRoot(rootEl).render( + + + + + + + , +); diff --git a/app/renderer/editor/i18n/editorMessages.ts b/app/renderer/editor/i18n/editorMessages.ts index 8559308..b58d6e5 100644 --- a/app/renderer/editor/i18n/editorMessages.ts +++ b/app/renderer/editor/i18n/editorMessages.ts @@ -660,8 +660,20 @@ export const EDITOR_MESSAGES: Record> = { 'control.playlistsTool': 'Плейлисты', 'control.timerTool': 'Таймер', 'control.initiativeTool': 'Инициатива', + 'control.diceTool': 'Кубики', 'control.npcsTool': 'НПС', + 'dice.sides': 'Кубик', + 'dice.count': 'Количество', + 'dice.roll': 'Бросить', + 'dice.clear': 'Убрать', + 'dice.result': 'Результат', + 'dice.total': 'Сумма', + 'dice.rolling': 'Анимация на экране…', + 'dice.noResult': 'Пока нет броска', + 'dice.inc': 'Больше', + 'dice.dec': 'Меньше', + 'sessionTimer.duration': 'Длительность (MM:SS)', 'sessionTimer.start': 'Старт', 'sessionTimer.pause': 'Пауза', @@ -1392,8 +1404,20 @@ export const EDITOR_MESSAGES: Record> = { 'control.playlistsTool': 'Playlists', 'control.timerTool': 'Timer', 'control.initiativeTool': 'Initiative', + 'control.diceTool': 'Dice', 'control.npcsTool': 'NPCs', + 'dice.sides': 'Die', + 'dice.count': 'Count', + 'dice.roll': 'Roll', + 'dice.clear': 'Clear', + 'dice.result': 'Result', + 'dice.total': 'Total', + 'dice.rolling': 'Animating on screen…', + 'dice.noResult': 'No roll yet', + 'dice.inc': 'More', + 'dice.dec': 'Fewer', + 'sessionTimer.duration': 'Duration (MM:SS)', 'sessionTimer.start': 'Start', 'sessionTimer.pause': 'Pause', diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin1.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin1.mp3 new file mode 100644 index 0000000..d07a721 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin2.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin2.mp3 new file mode 100644 index 0000000..de52f9b Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin3.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin3.mp3 new file mode 100644 index 0000000..712a471 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin4.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin4.mp3 new file mode 100644 index 0000000..1dcbe59 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin5.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin5.mp3 new file mode 100644 index 0000000..265b6b9 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin6.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin6.mp3 new file mode 100644 index 0000000..f9a06d4 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_coin6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal1.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal1.mp3 new file mode 100644 index 0000000..077c478 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal10.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal10.mp3 new file mode 100644 index 0000000..255eea2 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal10.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal11.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal11.mp3 new file mode 100644 index 0000000..0bab2a8 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal11.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal12.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal12.mp3 new file mode 100644 index 0000000..7f08549 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal12.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal2.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal2.mp3 new file mode 100644 index 0000000..f03b98c Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal3.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal3.mp3 new file mode 100644 index 0000000..a5dd435 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal4.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal4.mp3 new file mode 100644 index 0000000..26c1803 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal5.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal5.mp3 new file mode 100644 index 0000000..a5544be Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal6.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal6.mp3 new file mode 100644 index 0000000..c07180f Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal7.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal7.mp3 new file mode 100644 index 0000000..a2aecde Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal8.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal8.mp3 new file mode 100644 index 0000000..b26476f Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal8.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal9.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal9.mp3 new file mode 100644 index 0000000..5d9e90b Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_metal9.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic1.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic1.mp3 new file mode 100644 index 0000000..a100d70 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic10.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic10.mp3 new file mode 100644 index 0000000..406c71b Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic10.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic11.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic11.mp3 new file mode 100644 index 0000000..a49e877 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic11.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic12.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic12.mp3 new file mode 100644 index 0000000..19bb720 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic12.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic13.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic13.mp3 new file mode 100644 index 0000000..5611701 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic13.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic14.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic14.mp3 new file mode 100644 index 0000000..706b2fd Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic14.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic15.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic15.mp3 new file mode 100644 index 0000000..f0cf3a4 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic15.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic2.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic2.mp3 new file mode 100644 index 0000000..9d4eb27 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic3.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic3.mp3 new file mode 100644 index 0000000..ad00956 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic4.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic4.mp3 new file mode 100644 index 0000000..3359d86 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic5.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic5.mp3 new file mode 100644 index 0000000..f0da2f1 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic6.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic6.mp3 new file mode 100644 index 0000000..a812b8a Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic7.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic7.mp3 new file mode 100644 index 0000000..2a66e45 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic8.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic8.mp3 new file mode 100644 index 0000000..506f2b9 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic8.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic9.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic9.mp3 new file mode 100644 index 0000000..7992067 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_plastic9.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood1.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood1.mp3 new file mode 100644 index 0000000..9c5bb18 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood10.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood10.mp3 new file mode 100644 index 0000000..46a3746 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood10.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood11.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood11.mp3 new file mode 100644 index 0000000..c2a852e Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood11.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood12.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood12.mp3 new file mode 100644 index 0000000..3d66478 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood12.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood2.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood2.mp3 new file mode 100644 index 0000000..6c402de Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood3.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood3.mp3 new file mode 100644 index 0000000..835db18 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood4.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood4.mp3 new file mode 100644 index 0000000..0f9c7e5 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood5.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood5.mp3 new file mode 100644 index 0000000..5b6f0da Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood6.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood6.mp3 new file mode 100644 index 0000000..921b1ad Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood7.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood7.mp3 new file mode 100644 index 0000000..1029332 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood8.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood8.mp3 new file mode 100644 index 0000000..8456d75 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood8.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood9.mp3 b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood9.mp3 new file mode 100644 index 0000000..601afb0 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/dicehit/dicehit_wood9.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt1.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt1.mp3 new file mode 100644 index 0000000..b5d298d Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt2.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt2.mp3 new file mode 100644 index 0000000..d187453 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt3.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt3.mp3 new file mode 100644 index 0000000..2876c87 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt4.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt4.mp3 new file mode 100644 index 0000000..62ccd10 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt5.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt5.mp3 new file mode 100644 index 0000000..dbb15d1 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt6.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt6.mp3 new file mode 100644 index 0000000..dd97670 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_felt7.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_felt7.mp3 new file mode 100644 index 0000000..2e55a83 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_felt7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal1.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal1.mp3 new file mode 100644 index 0000000..fca44de Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal2.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal2.mp3 new file mode 100644 index 0000000..aacf2c6 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal3.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal3.mp3 new file mode 100644 index 0000000..5d2bafc Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal4.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal4.mp3 new file mode 100644 index 0000000..47b9afd Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal5.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal5.mp3 new file mode 100644 index 0000000..6103002 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal6.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal6.mp3 new file mode 100644 index 0000000..ba60148 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal7.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal7.mp3 new file mode 100644 index 0000000..e4b87b7 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal8.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal8.mp3 new file mode 100644 index 0000000..adc17f8 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal8.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_metal9.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_metal9.mp3 new file mode 100644 index 0000000..fa4643e Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_metal9.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table1.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table1.mp3 new file mode 100644 index 0000000..199baca Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table2.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table2.mp3 new file mode 100644 index 0000000..5c0cf02 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table3.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table3.mp3 new file mode 100644 index 0000000..ef9e8c0 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table4.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table4.mp3 new file mode 100644 index 0000000..fbf6c1b Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table5.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table5.mp3 new file mode 100644 index 0000000..34aee14 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table6.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table6.mp3 new file mode 100644 index 0000000..869d6fd Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table7.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table7.mp3 new file mode 100644 index 0000000..1464785 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_table7.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray1.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray1.mp3 new file mode 100644 index 0000000..e146ed3 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray1.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray2.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray2.mp3 new file mode 100644 index 0000000..556e73f Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray2.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray3.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray3.mp3 new file mode 100644 index 0000000..68da166 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray3.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray4.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray4.mp3 new file mode 100644 index 0000000..3513cf0 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray4.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray5.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray5.mp3 new file mode 100644 index 0000000..77d91eb Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray5.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray6.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray6.mp3 new file mode 100644 index 0000000..fc28534 Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray6.mp3 differ diff --git a/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray7.mp3 b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray7.mp3 new file mode 100644 index 0000000..b76878f Binary files /dev/null and b/app/renderer/public/dice-box/sounds/surfaces/surface_wood_tray7.mp3 differ diff --git a/app/renderer/public/dice-box/textures/astral.webp b/app/renderer/public/dice-box/textures/astral.webp new file mode 100644 index 0000000..83a1c23 Binary files /dev/null and b/app/renderer/public/dice-box/textures/astral.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze01.webp b/app/renderer/public/dice-box/textures/bronze01.webp new file mode 100644 index 0000000..cbc45c3 Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze01.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze02.webp b/app/renderer/public/dice-box/textures/bronze02.webp new file mode 100644 index 0000000..241fcce Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze02.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze03.webp b/app/renderer/public/dice-box/textures/bronze03.webp new file mode 100644 index 0000000..044d211 Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze03.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze03a.webp b/app/renderer/public/dice-box/textures/bronze03a.webp new file mode 100644 index 0000000..05e11b2 Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze03a.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze03b.webp b/app/renderer/public/dice-box/textures/bronze03b.webp new file mode 100644 index 0000000..81779c0 Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze03b.webp differ diff --git a/app/renderer/public/dice-box/textures/bronze04.webp b/app/renderer/public/dice-box/textures/bronze04.webp new file mode 100644 index 0000000..0bfa37c Binary files /dev/null and b/app/renderer/public/dice-box/textures/bronze04.webp differ diff --git a/app/renderer/public/dice-box/textures/cheetah.webp b/app/renderer/public/dice-box/textures/cheetah.webp new file mode 100644 index 0000000..29c625a Binary files /dev/null and b/app/renderer/public/dice-box/textures/cheetah.webp differ diff --git a/app/renderer/public/dice-box/textures/cloudy.alt.webp b/app/renderer/public/dice-box/textures/cloudy.alt.webp new file mode 100644 index 0000000..575ad27 Binary files /dev/null and b/app/renderer/public/dice-box/textures/cloudy.alt.webp differ diff --git a/app/renderer/public/dice-box/textures/cloudy.webp b/app/renderer/public/dice-box/textures/cloudy.webp new file mode 100644 index 0000000..b73592c Binary files /dev/null and b/app/renderer/public/dice-box/textures/cloudy.webp differ diff --git a/app/renderer/public/dice-box/textures/dragon-bump.webp b/app/renderer/public/dice-box/textures/dragon-bump.webp new file mode 100644 index 0000000..81591a5 Binary files /dev/null and b/app/renderer/public/dice-box/textures/dragon-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/dragon.webp b/app/renderer/public/dice-box/textures/dragon.webp new file mode 100644 index 0000000..d5caed7 Binary files /dev/null and b/app/renderer/public/dice-box/textures/dragon.webp differ diff --git a/app/renderer/public/dice-box/textures/feather-bump.webp b/app/renderer/public/dice-box/textures/feather-bump.webp new file mode 100644 index 0000000..781f297 Binary files /dev/null and b/app/renderer/public/dice-box/textures/feather-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/feather.webp b/app/renderer/public/dice-box/textures/feather.webp new file mode 100644 index 0000000..a505e1f Binary files /dev/null and b/app/renderer/public/dice-box/textures/feather.webp differ diff --git a/app/renderer/public/dice-box/textures/fire.webp b/app/renderer/public/dice-box/textures/fire.webp new file mode 100644 index 0000000..653044f Binary files /dev/null and b/app/renderer/public/dice-box/textures/fire.webp differ diff --git a/app/renderer/public/dice-box/textures/glitter-alpha.webp b/app/renderer/public/dice-box/textures/glitter-alpha.webp new file mode 100644 index 0000000..fef86f0 Binary files /dev/null and b/app/renderer/public/dice-box/textures/glitter-alpha.webp differ diff --git a/app/renderer/public/dice-box/textures/glitter-bump.webp b/app/renderer/public/dice-box/textures/glitter-bump.webp new file mode 100644 index 0000000..2070ed9 Binary files /dev/null and b/app/renderer/public/dice-box/textures/glitter-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/glitter.webp b/app/renderer/public/dice-box/textures/glitter.webp new file mode 100644 index 0000000..df7cdbe Binary files /dev/null and b/app/renderer/public/dice-box/textures/glitter.webp differ diff --git a/app/renderer/public/dice-box/textures/ice.webp b/app/renderer/public/dice-box/textures/ice.webp new file mode 100644 index 0000000..47250b5 Binary files /dev/null and b/app/renderer/public/dice-box/textures/ice.webp differ diff --git a/app/renderer/public/dice-box/textures/leopard.webp b/app/renderer/public/dice-box/textures/leopard.webp new file mode 100644 index 0000000..841604a Binary files /dev/null and b/app/renderer/public/dice-box/textures/leopard.webp differ diff --git a/app/renderer/public/dice-box/textures/lizard-bump.webp b/app/renderer/public/dice-box/textures/lizard-bump.webp new file mode 100644 index 0000000..7f5f08c Binary files /dev/null and b/app/renderer/public/dice-box/textures/lizard-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/lizard.webp b/app/renderer/public/dice-box/textures/lizard.webp new file mode 100644 index 0000000..095c464 Binary files /dev/null and b/app/renderer/public/dice-box/textures/lizard.webp differ diff --git a/app/renderer/public/dice-box/textures/marble.webp b/app/renderer/public/dice-box/textures/marble.webp new file mode 100644 index 0000000..1bed836 Binary files /dev/null and b/app/renderer/public/dice-box/textures/marble.webp differ diff --git a/app/renderer/public/dice-box/textures/metal-bump.webp b/app/renderer/public/dice-box/textures/metal-bump.webp new file mode 100644 index 0000000..cef36e0 Binary files /dev/null and b/app/renderer/public/dice-box/textures/metal-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/metal.webp b/app/renderer/public/dice-box/textures/metal.webp new file mode 100644 index 0000000..7c7c1ee Binary files /dev/null and b/app/renderer/public/dice-box/textures/metal.webp differ diff --git a/app/renderer/public/dice-box/textures/noise-thin-film.webp b/app/renderer/public/dice-box/textures/noise-thin-film.webp new file mode 100644 index 0000000..62c78bb Binary files /dev/null and b/app/renderer/public/dice-box/textures/noise-thin-film.webp differ diff --git a/app/renderer/public/dice-box/textures/noise.webp b/app/renderer/public/dice-box/textures/noise.webp new file mode 100644 index 0000000..35a2efd Binary files /dev/null and b/app/renderer/public/dice-box/textures/noise.webp differ diff --git a/app/renderer/public/dice-box/textures/paper-bump.webp b/app/renderer/public/dice-box/textures/paper-bump.webp new file mode 100644 index 0000000..8167a0e Binary files /dev/null and b/app/renderer/public/dice-box/textures/paper-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/paper.webp b/app/renderer/public/dice-box/textures/paper.webp new file mode 100644 index 0000000..1105919 Binary files /dev/null and b/app/renderer/public/dice-box/textures/paper.webp differ diff --git a/app/renderer/public/dice-box/textures/skulls.webp b/app/renderer/public/dice-box/textures/skulls.webp new file mode 100644 index 0000000..8b02099 Binary files /dev/null and b/app/renderer/public/dice-box/textures/skulls.webp differ diff --git a/app/renderer/public/dice-box/textures/speckles.webp b/app/renderer/public/dice-box/textures/speckles.webp new file mode 100644 index 0000000..85b3033 Binary files /dev/null and b/app/renderer/public/dice-box/textures/speckles.webp differ diff --git a/app/renderer/public/dice-box/textures/stainedglass-bump.webp b/app/renderer/public/dice-box/textures/stainedglass-bump.webp new file mode 100644 index 0000000..3aadc85 Binary files /dev/null and b/app/renderer/public/dice-box/textures/stainedglass-bump.webp differ diff --git a/app/renderer/public/dice-box/textures/stainedglass.webp b/app/renderer/public/dice-box/textures/stainedglass.webp new file mode 100644 index 0000000..46f1b53 Binary files /dev/null and b/app/renderer/public/dice-box/textures/stainedglass.webp differ diff --git a/app/renderer/public/dice-box/textures/stars.webp b/app/renderer/public/dice-box/textures/stars.webp new file mode 100644 index 0000000..18967f0 Binary files /dev/null and b/app/renderer/public/dice-box/textures/stars.webp differ diff --git a/app/renderer/public/dice-box/textures/stone.webp b/app/renderer/public/dice-box/textures/stone.webp new file mode 100644 index 0000000..376d205 Binary files /dev/null and b/app/renderer/public/dice-box/textures/stone.webp differ diff --git a/app/renderer/public/dice-box/textures/tiger.webp b/app/renderer/public/dice-box/textures/tiger.webp new file mode 100644 index 0000000..c31658c Binary files /dev/null and b/app/renderer/public/dice-box/textures/tiger.webp differ diff --git a/app/renderer/public/dice-box/textures/water.webp b/app/renderer/public/dice-box/textures/water.webp new file mode 100644 index 0000000..5f85bad Binary files /dev/null and b/app/renderer/public/dice-box/textures/water.webp differ diff --git a/app/renderer/public/dice-box/textures/wood.webp b/app/renderer/public/dice-box/textures/wood.webp new file mode 100644 index 0000000..65727a0 Binary files /dev/null and b/app/renderer/public/dice-box/textures/wood.webp differ diff --git a/app/renderer/shared/PresentationView.tsx b/app/renderer/shared/PresentationView.tsx index d455f19..37bf601 100644 --- a/app/renderer/shared/PresentationView.tsx +++ b/app/renderer/shared/PresentationView.tsx @@ -25,6 +25,7 @@ import { useSceneNpcTokensSession } from './playerToken/useSceneNpcTokensSession import { useScenePlayerTokensSession } from './playerToken/useScenePlayerTokensSession'; import { SceneOverlayHost } from './sceneOverlay/SceneOverlayHost'; import { SessionTimerLayer } from './sessionTimer/SessionTimerLayer'; +import { DiceRollLayer } from './dice/DiceRollLayer'; import { useSceneViewState } from './sceneView/useSceneViewState'; import { SceneTokensOverlay } from './tokens/SceneTokensOverlay'; import { TokenPathsOverlay } from './tokens/TokenPathsOverlay'; @@ -340,6 +341,7 @@ export function PresentationView({ {activeNpcItems.length > 0 ? : null} + {showTitle ? (
diff --git a/app/renderer/shared/dice/DiceRollLayer.module.css b/app/renderer/shared/dice/DiceRollLayer.module.css new file mode 100644 index 0000000..9ea2e95 --- /dev/null +++ b/app/renderer/shared/dice/DiceRollLayer.module.css @@ -0,0 +1,91 @@ +.layer { + position: absolute; + inset: 0; + z-index: 40; + pointer-events: none; + opacity: 0; + transition: opacity 160ms ease; +} + +.layer[data-active='1'] { + opacity: 1; + pointer-events: auto; +} + +.backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.45); +} + +.tray { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: min(920px, 92%); + height: min(520px, 78%); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + overflow: hidden; + background: rgba(10, 14, 20, 0.55); + box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45); +} + +.host { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.host :global(canvas) { + width: 100% !important; + height: 100% !important; + display: block; +} + +.result { + position: absolute; + left: 50%; + bottom: 14px; + transform: translateX(-50%); + display: flex; + align-items: baseline; + gap: 10px; + padding: 8px 14px; + border-radius: 999px; + background: rgba(9, 9, 11, 0.82); + border: 1px solid rgba(255, 255, 255, 0.14); + color: #f8fafc; + font-weight: 700; + pointer-events: none; + white-space: nowrap; +} + +.resultNotation { + color: #93c5fd; + font-size: 13px; +} + +.resultValues { + font-size: 14px; + font-variant-numeric: tabular-nums; +} + +.resultTotal { + font-size: 18px; + font-variant-numeric: tabular-nums; +} + +.error { + position: absolute; + inset: 12px; + display: flex; + align-items: center; + justify-content: center; + color: #fecaca; + font-size: 13px; + text-align: center; + pointer-events: none; +} diff --git a/app/renderer/shared/dice/DiceRollLayer.tsx b/app/renderer/shared/dice/DiceRollLayer.tsx new file mode 100644 index 0000000..c69f99c --- /dev/null +++ b/app/renderer/shared/dice/DiceRollLayer.tsx @@ -0,0 +1,132 @@ +import React, { useEffect, useId, useRef, useState } from 'react'; + +import DiceBox from '@3d-dice/dice-box-threejs'; + +import { + formatPredeterminedDiceNotation, + isDiceSides, + type DiceRollState, +} from '../../../shared/types/diceRoll'; + +import { useDiceRollState } from './useDiceRollState'; +import styles from './DiceRollLayer.module.css'; + +function diceAssetPath(): string { + const base = import.meta.env.BASE_URL || '/'; + const joined = `${base}dice-box/`.replace(/([^:]\/)\/+/g, '$1'); + return joined; +} + +function parseNotationSides(notation: string): number | null { + const m = /^(\d+)d(\d+)$/i.exec(notation.trim()); + if (!m) return null; + const sides = Number(m[2]); + return Number.isFinite(sides) ? sides : null; +} + +function parseNotationCount(notation: string): number { + const m = /^(\d+)d(\d+)$/i.exec(notation.trim()); + if (!m) return 1; + return Math.max(1, Number(m[1]) || 1); +} + +/** Полноэкранный оверлей 3D-броска на превью и презентации. */ +export function DiceRollLayer() { + const [state] = useDiceRollState(); + const reactId = useId().replace(/:/g, ''); + const hostId = `dice-roll-host-${reactId}`; + const boxRef = useRef(null); + const rollingRef = useRef(false); + const lastRevisionRef = useRef(0); + const [ready, setReady] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + const box = new DiceBox(`#${hostId}`, { + assetPath: diceAssetPath(), + sounds: false, + shadows: true, + theme_surface: 'green-felt', + theme_colorset: 'white', + theme_material: 'glass', + strength: 1, + gravity_multiplier: 400, + }); + boxRef.current = box; + void box + .initialize() + .then(() => { + if (!cancelled) setReady(true); + }) + .catch((err: unknown) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : String(err)); + }); + return () => { + cancelled = true; + try { + box.clearDice?.(); + } catch { + /* ignore */ + } + boxRef.current = null; + const el = document.getElementById(hostId); + el?.replaceChildren(); + }; + }, [hostId]); + + useEffect(() => { + if (!ready || !state?.active) return; + if (state.revision === lastRevisionRef.current) return; + if (rollingRef.current) return; + const box = boxRef.current; + if (!box) return; + + const sidesRaw = parseNotationSides(state.notation); + if (sidesRaw == null || !isDiceSides(sidesRaw)) return; + const count = parseNotationCount(state.notation); + const notation = formatPredeterminedDiceNotation(count, sidesRaw, state.values); + const revision = state.revision; + lastRevisionRef.current = revision; + rollingRef.current = true; + + void (async () => { + try { + await box.roll(notation); + } catch (err) { + console.error('[dice] roll failed', err); + } finally { + rollingRef.current = false; + } + })(); + }, [ready, state]); + + const visible = Boolean(state?.active); + return ( +
+
+
+
+ {error ?
{error}
: null} + {visible && state ? : null} +
+
+ ); +} + +function DiceResultBadge({ state }: { state: DiceRollState }) { + if (!state.values.length) return null; + return ( +
+ {state.notation} + {state.values.join(' + ')} + = {state.total} +
+ ); +} diff --git a/app/renderer/shared/dice/dice-box-threejs.d.ts b/app/renderer/shared/dice/dice-box-threejs.d.ts new file mode 100644 index 0000000..15af814 --- /dev/null +++ b/app/renderer/shared/dice/dice-box-threejs.d.ts @@ -0,0 +1,27 @@ +declare module '@3d-dice/dice-box-threejs' { + export type DiceBoxConfig = { + assetPath?: string; + framerate?: number; + sounds?: boolean; + volume?: number; + shadows?: boolean; + theme_surface?: string; + theme_colorset?: string; + theme_texture?: string; + theme_material?: string; + gravity_multiplier?: number; + light_intensity?: number; + baseScale?: number; + strength?: number; + onRollComplete?: (results: unknown) => void; + }; + + export default class DiceBox { + constructor(elementSelector: string, options?: DiceBoxConfig); + initialize(): Promise; + roll(notation: string): Promise; + clearDice(): void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + } +} diff --git a/app/renderer/shared/dice/useDiceRollState.ts b/app/renderer/shared/dice/useDiceRollState.ts new file mode 100644 index 0000000..26120f9 --- /dev/null +++ b/app/renderer/shared/dice/useDiceRollState.ts @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; + +import { ipcChannels } from '../../../shared/ipc/contracts'; +import type { DiceRollEvent, DiceRollState } from '../../../shared/types'; +import { getDndApi } from '../dndApi'; + +export function useDiceRollState(): readonly [ + DiceRollState | null, + { dispatch: (event: DiceRollEvent) => Promise }, +] { + const api = getDndApi(); + const [state, setState] = useState(null); + + useEffect(() => { + void api.invoke(ipcChannels.diceRoll.getState, {}).then((r) => { + setState(r.state); + }); + return api.on(ipcChannels.diceRoll.stateChanged, ({ state: next }) => { + setState(next); + }); + }, [api]); + + return [ + state, + { + dispatch: async (event) => { + const r = await api.invoke(ipcChannels.diceRoll.dispatch, { event }); + setState(r.state); + return r.state; + }, + }, + ] as const; +} diff --git a/app/renderer/shared/ui/secondaryWindows.stability.test.ts b/app/renderer/shared/ui/secondaryWindows.stability.test.ts index 3bf8690..5284546 100644 --- a/app/renderer/shared/ui/secondaryWindows.stability.test.ts +++ b/app/renderer/shared/ui/secondaryWindows.stability.test.ts @@ -15,6 +15,7 @@ const SECONDARY_WINDOW_MAINS = [ 'playlists/main.tsx', 'timer/main.tsx', 'initiative/main.tsx', + 'dice/main.tsx', 'sceneEditor/main.tsx', 'sceneDescription/main.tsx', 'control/main.tsx', diff --git a/app/shared/appBranding.ts b/app/shared/appBranding.ts index b6556e5..5ba8a85 100644 --- a/app/shared/appBranding.ts +++ b/app/shared/appBranding.ts @@ -25,6 +25,7 @@ export type AppWindowKind = | 'playlists' | 'timer' | 'initiative' + | 'dice' | 'npcsEditor' | 'sceneEditor' | 'tokenPathEditor' @@ -40,6 +41,7 @@ const WINDOW_SUFFIX: Record = { playlists: { ru: 'Плейлисты', en: 'Playlists' }, timer: { ru: 'Таймер', en: 'Timer' }, initiative: { ru: 'Инициатива', en: 'Initiative' }, + dice: { ru: 'Кубики', en: 'Dice' }, npcsEditor: { ru: 'НПС', en: 'NPCs' }, sceneEditor: { ru: 'Редактор сцены', en: 'Scene editor' }, tokenPathEditor: { ru: 'Движение токена', en: 'Token path' }, diff --git a/app/shared/ipc/contracts.ts b/app/shared/ipc/contracts.ts index dcb9508..061ec05 100644 --- a/app/shared/ipc/contracts.ts +++ b/app/shared/ipc/contracts.ts @@ -59,6 +59,8 @@ import type { SessionTimerState, InitiativeEvent, InitiativeState, + DiceRollEvent, + DiceRollState, TokenId, TokenPathSessionEvent, TokenPathSessionState, @@ -171,6 +173,8 @@ export const ipcChannels = { closeTimer: 'windows.closeTimer', openInitiative: 'windows.openInitiative', closeInitiative: 'windows.closeInitiative', + openDice: 'windows.openDice', + closeDice: 'windows.closeDice', openNpcsEditor: 'windows.openNpcsEditor', closeNpcsEditor: 'windows.closeNpcsEditor', openNpcs: 'windows.openNpcs', @@ -218,6 +222,11 @@ export const ipcChannels = { dispatch: 'initiative.dispatch', stateChanged: 'initiative.stateChanged', }, + diceRoll: { + getState: 'diceRoll.getState', + dispatch: 'diceRoll.dispatch', + stateChanged: 'diceRoll.stateChanged', + }, sceneDarkness: { getState: 'sceneDarkness.getState', dispatch: 'sceneDarkness.dispatch', @@ -377,6 +386,7 @@ export type IpcEventMap = { [ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState }; [ipcChannels.sessionTimer.stateChanged]: { state: SessionTimerState }; [ipcChannels.initiative.stateChanged]: { state: InitiativeState }; + [ipcChannels.diceRoll.stateChanged]: { state: DiceRollState }; [ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState }; [ipcChannels.sceneMarker.stateChanged]: { state: SceneMarkerState }; [ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState }; @@ -822,6 +832,14 @@ export type IpcInvokeMap = { req: Record; res: { ok: true }; }; + [ipcChannels.windows.openDice]: { + req: Record; + res: { ok: true }; + }; + [ipcChannels.windows.closeDice]: { + req: Record; + res: { ok: true }; + }; [ipcChannels.windows.openNpcsEditor]: { req: Record; res: { ok: true }; @@ -894,6 +912,14 @@ export type IpcInvokeMap = { req: { event: InitiativeEvent }; res: { state: InitiativeState }; }; + [ipcChannels.diceRoll.getState]: { + req: Record; + res: { state: DiceRollState }; + }; + [ipcChannels.diceRoll.dispatch]: { + req: { event: DiceRollEvent }; + res: { state: DiceRollState }; + }; [ipcChannels.effects.getState]: { req: Record; res: { state: EffectsState }; @@ -1148,6 +1174,7 @@ export type LegacyIpcEventMap = { [ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState }; [ipcChannels.sessionTimer.stateChanged]: { state: SessionTimerState }; [ipcChannels.initiative.stateChanged]: { state: InitiativeState }; + [ipcChannels.diceRoll.stateChanged]: { state: DiceRollState }; [ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState }; [ipcChannels.sceneMarker.stateChanged]: { state: SceneMarkerState }; [ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState }; diff --git a/app/shared/types/diceRoll.ts b/app/shared/types/diceRoll.ts new file mode 100644 index 0000000..6f2c049 --- /dev/null +++ b/app/shared/types/diceRoll.ts @@ -0,0 +1,88 @@ +/** Поддерживаемые грани кубиков (как в dice-box-threejs / Major's). */ +export const DICE_SIDES = [4, 6, 8, 10, 12, 20, 100] as const; +export type DiceSides = (typeof DICE_SIDES)[number]; + +export function isDiceSides(n: number): n is DiceSides { + return (DICE_SIDES as readonly number[]).includes(n); +} + +export type DiceRollRequest = { + /** Количество кубиков одного типа. */ + count: number; + sides: DiceSides; +}; + +export type DiceRollState = { + revision: number; + /** Идёт показ броска (оверлей виден). */ + active: boolean; + /** Например "2d20". */ + notation: string; + /** Заранее выбранные исходы — для синхрона пульт/презентация. */ + values: number[]; + total: number; + /** Когда бросок начат (для перезапуска анимации при том же revision нельзя — revision растёт). */ + startedAt: number | null; +}; + +export type DiceRollEvent = + | { kind: 'roll'; count: number; sides: DiceSides } + | { kind: 'hideOverlay' } + | { kind: 'clear' }; + +export function createInitialDiceRollState(): DiceRollState { + return { + revision: 1, + active: false, + notation: '', + values: [], + total: 0, + startedAt: null, + }; +} + +export function clampDiceCount(n: number): number { + if (!Number.isFinite(n)) return 1; + return Math.max(1, Math.min(20, Math.round(n))); +} + +/** Грани d100 в dice-box-threejs: 10,20,…,90,100 (лицо «00»). */ +export const D100_FACE_VALUES = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] as const; + +/** Привести значение к ближайшей грани кубика десятков. */ +export function snapD100Value(value: number): number { + const n = Math.round(value); + if (!Number.isFinite(n)) return 10; + if (n <= 0) return 100; + const tens = Math.ceil(n / 10) * 10; + if (tens <= 0) return 100; + return Math.min(100, Math.max(10, tens)); +} + +export function rollDiceValues(count: number, sides: DiceSides): number[] { + const c = clampDiceCount(count); + const out: number[] = []; + for (let i = 0; i < c; i += 1) { + if (sides === 100) { + out.push(D100_FACE_VALUES[Math.floor(Math.random() * D100_FACE_VALUES.length)]!); + } else { + out.push(1 + Math.floor(Math.random() * sides)); + } + } + return out; +} + +/** Нотация для dice-box-threejs с predetermined outcomes: `2d20@15,7`. */ +export function formatPredeterminedDiceNotation(count: number, sides: DiceSides, values: readonly number[]): string { + const c = clampDiceCount(count); + const vals = + sides === 100 + ? values.slice(0, c).map(snapD100Value) + : values.slice(0, c).map((v) => Math.max(1, Math.min(sides, Math.round(v)))); + while (vals.length < c) vals.push(sides === 100 ? 10 : 1); + return `${c}d${sides}@${vals.join(',')}`; +} + +export function formatDiceNotation(count: number, sides: DiceSides): string { + return `${clampDiceCount(count)}d${sides}`; +} diff --git a/app/shared/types/index.ts b/app/shared/types/index.ts index c1c29c8..b96df54 100644 --- a/app/shared/types/index.ts +++ b/app/shared/types/index.ts @@ -22,4 +22,5 @@ export * from './sceneTraps'; export * from './sceneView'; export * from './sessionTimer'; export * from './initiative'; +export * from './diceRoll'; export * from './videoPlayback'; diff --git a/package-lock.json b/package-lock.json index 70030d1..86bf60f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "ISC", "dependencies": { + "@3d-dice/dice-box-threejs": "^0.0.12", "@fontsource/inter": "^5.2.8", "@tiptap/extension-placeholder": "^3.27.4", "@tiptap/react": "^3.27.4", @@ -59,6 +60,16 @@ "yazl": "^3.3.1" } }, + "node_modules/@3d-dice/dice-box-threejs": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@3d-dice/dice-box-threejs/-/dice-box-threejs-0.0.12.tgz", + "integrity": "sha512-Bau5UizUunZS9+A6zgGeZecEJotSLEI6YvmYTosvvdhRxv92xo89aIvKYQb6xqdx/AqjHdGENJmqBOadlFL9kA==", + "license": "MIT", + "dependencies": { + "cannon-es": "^0.20.0", + "three": "^0.143.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -5936,6 +5947,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cannon-es": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz", + "integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==", + "license": "MIT" + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -14256,6 +14273,12 @@ "node": ">= 10.0.0" } }, + "node_modules/three": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.143.0.tgz", + "integrity": "sha512-oKcAGYHhJ46TGEuHjodo2n6TY2R6lbvrkp+feKZxqsUL/WkH7GKKaeu6RHeyb2Xjfk2dPLRKLsOP0KM2VgT8Zg==", + "license": "MIT" + }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", diff --git a/package.json b/package.json index b38b906..9c8956f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:obfuscate": "node scripts/build.mjs --production --obfuscate", "lint": "eslint . --max-warnings 0", "typecheck": "tsc -p tsconfig.eslint.json --noEmit", - "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/shared/graph/activeStorylineScenes.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/renderer/shared/videoSceneMapParity.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/preferences/appPreferences.test.ts app/main/playlists/playlistsStore.test.ts app/main/sessionTimer/timerSessionStore.test.ts app/main/initiative/initiativeSessionStore.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/effects/sceneMarkerStore.test.ts app/shared/sceneMarkerEraser.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/npcExternalUrl.test.ts app/main/npcs/npcsOverlayStore.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/shared/types/sceneDarkness.test.ts app/shared/types/materialLegend.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/main/tokens/sceneTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts app/shared/types/scenePreviewRotation.test.ts app/shared/types/tokenPath.test.ts app/shared/types/tokenPathPlayback.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs scripts/verify-packaged-sharp.test.mjs app/main/project/sharpRuntime.test.mjs", + "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/shared/graph/activeStorylineScenes.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/renderer/shared/videoSceneMapParity.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/preferences/appPreferences.test.ts app/main/playlists/playlistsStore.test.ts app/main/sessionTimer/timerSessionStore.test.ts app/main/initiative/initiativeSessionStore.test.ts app/main/diceRoll/diceRollSessionStore.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/effects/sceneMarkerStore.test.ts app/shared/sceneMarkerEraser.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/npcExternalUrl.test.ts app/main/npcs/npcsOverlayStore.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/shared/types/sceneDarkness.test.ts app/shared/types/materialLegend.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/main/tokens/sceneTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts app/shared/types/scenePreviewRotation.test.ts app/shared/types/tokenPath.test.ts app/shared/types/tokenPathPlayback.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs scripts/verify-packaged-sharp.test.mjs app/main/project/sharpRuntime.test.mjs", "format": "prettier . --check", "format:write": "prettier . --write", "postinstall": "patch-package", @@ -32,6 +32,7 @@ "license": "ISC", "type": "module", "dependencies": { + "@3d-dice/dice-box-threejs": "^0.0.12", "@fontsource/inter": "^5.2.8", "@tiptap/extension-placeholder": "^3.27.4", "@tiptap/react": "^3.27.4", diff --git a/vite.config.ts b/vite.config.ts index addee29..e7d55a4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -58,6 +58,7 @@ export default defineConfig(({ mode }) => { playlists: path.resolve(__dirname, 'app/renderer/playlists.html'), timer: path.resolve(__dirname, 'app/renderer/timer.html'), initiative: path.resolve(__dirname, 'app/renderer/initiative.html'), + dice: path.resolve(__dirname, 'app/renderer/dice.html'), sceneEditor: path.resolve(__dirname, 'app/renderer/sceneEditor.html'), tokenPathEditor: path.resolve(__dirname, 'app/renderer/tokenPathEditor.html'), npcsEditor: path.resolve(__dirname, 'app/renderer/npcsEditor.html'),