From b3399e3dcdbe11166403cfb2f3066f9f8e53517d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:36:09 -0300 Subject: [PATCH 1/8] chore: update Babel packages to latest versions and remove deprecated entries --- pnpm-lock.yaml | 66 ++++++++------------------------------------------ 1 file changed, 10 insertions(+), 56 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e2d619..10fd215 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -198,10 +198,6 @@ packages: resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.29.8': resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} @@ -272,11 +268,6 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.29.8': resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} @@ -320,18 +311,10 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.8': resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.8': resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} @@ -3858,14 +3841,14 @@ snapshots: '@babel/core@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -3875,14 +3858,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': - dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - '@babel/generator@7.29.8': dependencies: '@babel/parser': 7.29.8 @@ -3927,8 +3902,8 @@ snapshots: '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -3937,7 +3912,7 @@ snapshots: '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8 transitivePeerDependencies: - supports-color @@ -3972,11 +3947,7 @@ snapshots: '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/parser@7.29.7': - dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/parser@7.29.8': dependencies: @@ -4027,20 +3998,8 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 - - '@babel/traverse@7.29.7': - dependencies: - '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/template': 7.29.7 - '@babel/types': 7.29.7 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@babel/traverse@7.29.8': dependencies: @@ -4054,11 +4013,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/types@7.29.7': - dependencies: - '@babel/helper-string-parser': 7.29.7 - '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 From 35ee60aa00f02faeb5b236a22a8b07bf6cde6c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:36:21 -0300 Subject: [PATCH 2/8] fix(dice-roller): rename totalDice to regularDice in schema and form handling --- .../utilities/dice-roller/vtm-dice-roller.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 8cf96e8..e837c7b 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -10,9 +10,9 @@ import { Input } from '~/components/ui/input'; import useIsMobile from '~/hooks/use-mobile'; const VtmDiceRollerSchema = z.object({ - totalDice: z.number().min(1).max(20).default(1), - hungerDice: z.number().min(0).max(20).default(0), - difficulty: z.number().min(2).max(10).default(6).optional(), + regularDice: z.coerce.number().min(1).max(20).default(1), + hungerDice: z.coerce.number().min(0).max(20).default(0), + difficulty: z.coerce.number().min(2).max(10).default(6).optional(), }); type VtmDiceRollerSchemaType = z.infer; @@ -22,21 +22,21 @@ export default function VtmDiceRoller() { const form = useForm({ resolver: zodResolver(VtmDiceRollerSchema) as Resolver, - defaultValues: { totalDice: 1, hungerDice: 0, difficulty: 6 }, + defaultValues: { regularDice: 1, hungerDice: 0, difficulty: 6 }, }); const onSubmitFn = useCallback(async (data: VtmDiceRollerSchemaType, event?: BaseSyntheticEvent) => { - const { totalDice, hungerDice, difficulty } = data; + const { regularDice, hungerDice, difficulty } = data; const buttonClicked = (event?.nativeEvent as SubmitEvent | undefined)?.submitter?.id; if (buttonClicked === 'roll-dice') { console.log('Rolling dice with the following parameters:'); - console.log(`Total Dice: ${totalDice}`); + console.log(`Total Dice: ${regularDice}`); console.log(`Hunger Dice: ${hungerDice}`); console.log(`Difficulty: ${difficulty}`); } else if (buttonClicked === 'rouse-check') { console.log('Performing a rouse check with the following parameters:'); - console.log(`Total Dice: ${totalDice}`); + console.log(`Total Dice: ${regularDice}`); console.log(`Hunger Dice: ${hungerDice}`); } }, []); @@ -57,7 +57,7 @@ export default function VtmDiceRoller() { ( Total Dice @@ -69,7 +69,7 @@ export default function VtmDiceRoller() { step={1} inputMode="decimal" placeholder="e.g. 5" - aria-invalid={!!form.formState.errors.totalDice} + aria-invalid={!!form.formState.errors.regularDice} {...field} /> {fieldState.invalid && ( From a494a29c231d63c6335f308ede3320096448cab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 09:41:01 -0300 Subject: [PATCH 3/8] refactor(dice-roller): adjust button styles for better responsiveness in VtmDiceRoller --- .../pages/utilities/dice-roller/vtm-dice-roller.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index e837c7b..9cca2d9 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -138,14 +138,14 @@ export default function VtmDiceRoller() { /> - - - - From c2cf11d128b631ad15d22b448cf82b24ec6e0ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:13:32 -0300 Subject: [PATCH 4/8] feat(dice-roller): add rollDie utility function for random die rolls --- app/lib/dice-roller.utils.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/lib/dice-roller.utils.ts diff --git a/app/lib/dice-roller.utils.ts b/app/lib/dice-roller.utils.ts new file mode 100644 index 0000000..9f77fe3 --- /dev/null +++ b/app/lib/dice-roller.utils.ts @@ -0,0 +1,11 @@ +import { floor, random } from 'mathjs'; + +function rollDie(sides: number): number { + if (sides < 1) { + throw new Error('Number of sides must be at least 1.'); + } + + return floor(random(1, sides + 1)); +} + +export { rollDie }; From a1543309ef4984a925c00b290ca7fcace5d16b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:16:07 -0300 Subject: [PATCH 5/8] feat(dice-roller): enhance VtmDiceRoller with rollDice function and UI updates --- .../utilities/dice-roller/vtm-dice-roller.tsx | 140 ++++++++++++++---- 1 file changed, 114 insertions(+), 26 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 9cca2d9..5f8777c 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -1,71 +1,140 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { type BaseSyntheticEvent, useCallback } from 'react'; +import { InfoIcon } from 'lucide-react'; +import { type BaseSyntheticEvent, useCallback, useState, useTransition } from 'react'; import { Controller, type Resolver, useForm } from 'react-hook-form'; import { z } from 'zod/v4'; +import { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert'; import { Button } from '~/components/ui/button'; import { ButtonGroup } from '~/components/ui/button-group'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '~/components/ui/card'; import { Field, FieldError, FieldGroup, FieldLabel } from '~/components/ui/field'; import { Input } from '~/components/ui/input'; import useIsMobile from '~/hooks/use-mobile'; +import { rollDie } from '~/lib/dice-roller.utils'; + +type DiceRollData = { + regularDiceRoll: number[]; + hungerDiceRoll: number[]; +}; const VtmDiceRollerSchema = z.object({ - regularDice: z.coerce.number().min(1).max(20).default(1), - hungerDice: z.coerce.number().min(0).max(20).default(0), - difficulty: z.coerce.number().min(2).max(10).default(6).optional(), + regularDice: z.coerce.number().min(1).max(10).default(1), + hungerDice: z.coerce.number().min(0).max(5).default(0), + difficulty: z.coerce.number().min(2).max(10).default(4).optional(), }); type VtmDiceRollerSchemaType = z.infer; export default function VtmDiceRoller() { + const [currentRoll, setCurrentRoll] = useState({ regularDiceRoll: [], hungerDiceRoll: [] }); + const [isPending, startTransition] = useTransition(); const { isMobile } = useIsMobile(); const form = useForm({ resolver: zodResolver(VtmDiceRollerSchema) as Resolver, - defaultValues: { regularDice: 1, hungerDice: 0, difficulty: 6 }, + defaultValues: { regularDice: 1, hungerDice: 0, difficulty: 4 }, }); - const onSubmitFn = useCallback(async (data: VtmDiceRollerSchemaType, event?: BaseSyntheticEvent) => { - const { regularDice, hungerDice, difficulty } = data; - const buttonClicked = (event?.nativeEvent as SubmitEvent | undefined)?.submitter?.id; - - if (buttonClicked === 'roll-dice') { - console.log('Rolling dice with the following parameters:'); - console.log(`Total Dice: ${regularDice}`); - console.log(`Hunger Dice: ${hungerDice}`); - console.log(`Difficulty: ${difficulty}`); - } else if (buttonClicked === 'rouse-check') { - console.log('Performing a rouse check with the following parameters:'); - console.log(`Total Dice: ${regularDice}`); - console.log(`Hunger Dice: ${hungerDice}`); + function rollDice(regularDice: number, hungerDice: number, difficulty: number | undefined) { + if (hungerDice > regularDice) { + alert('Hunger dice cannot exceed total dice.'); + return; + } + + if (difficulty && (difficulty < 2 || difficulty > 10)) { + alert('Difficulty must be between 2 and 10.'); + return; + } + + if (regularDice < 1 || regularDice > 10) { + alert('Total dice must be between 1 and 10.'); + return; + } + + if (hungerDice < 0 || hungerDice > 5) { + alert('Hunger dice must be between 0 and 5.'); + return; + } + + setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [] }); + + for (let i = 0; i < regularDice; i++) { + const roll = rollDie(10); + setCurrentRoll((prev) => ({ ...prev, regularDiceRoll: [...prev.regularDiceRoll, roll] })); + } + + for (let i = 0; i < hungerDice; i++) { + const roll = rollDie(10); + setCurrentRoll((prev) => ({ ...prev, hungerDiceRoll: [...prev.hungerDiceRoll, roll] })); } - }, []); + } + + const onSubmitFn = useCallback( + (data: VtmDiceRollerSchemaType, event?: BaseSyntheticEvent) => { + startTransition(async () => { + const { regularDice, hungerDice, difficulty } = data; + const buttonClicked = (event?.nativeEvent as SubmitEvent | undefined)?.submitter?.id; + + switch (buttonClicked) { + case 'roll-dice': + rollDice(regularDice, hungerDice, difficulty); + break; + case 'rouse-check': + console.log( + `Performing rouse check with ${regularDice} regular dice and ${hungerDice} hunger dice.`, + ); + break; + default: + throw new Error('Something went wrong. Please try again.'); + } + }); + }, + [startTransition], + ); function reset() { form.reset(); + setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [] }); } return ( Vampire: The Masquerade (5th edition) Dice Roller - Roll normal and hunger dice for Vampire: The Masquerade. + + Roll regular and hunger dice for Vampire: The Masquerade (5th edition) using this simple dice + roller. + - -
+ + + + Important! + + Regular dice are summed with hunger dice to determine the total dice rolled. Hunger dice are a + special type of die that can result in critical successes or failures, so read the rules + carefully! + + + + ( - Total Dice + Regular Dice
+ + {currentRoll.regularDiceRoll.length > 0 || currentRoll.hungerDiceRoll.length > 0 ? ( +
+

Current Roll:

+
+ {currentRoll.regularDiceRoll.map((roll, index) => ( + {roll} + ))} +
+ {currentRoll.hungerDiceRoll.length > 0 && ( +
+ {currentRoll.hungerDiceRoll.map((roll, index) => ( + {roll} + ))} +
+ )} +
+ ) : null} +
); } From 7b08e49a7f55c132f1aa2fba1cd076dcc6845753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:24:18 -0300 Subject: [PATCH 6/8] feat(dice-roller): add VtmAlert component for improved user notifications --- .../pages/utilities/dice-roller/vtm-alert.tsx | 26 +++++++++++++++++++ .../utilities/dice-roller/vtm-dice-roller.tsx | 13 ++-------- 2 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 app/components/pages/utilities/dice-roller/vtm-alert.tsx diff --git a/app/components/pages/utilities/dice-roller/vtm-alert.tsx b/app/components/pages/utilities/dice-roller/vtm-alert.tsx new file mode 100644 index 0000000..012304a --- /dev/null +++ b/app/components/pages/utilities/dice-roller/vtm-alert.tsx @@ -0,0 +1,26 @@ +import { InfoIcon, XIcon } from 'lucide-react'; +import { useState } from 'react'; +import { Alert, AlertAction, AlertDescription, AlertTitle } from '~/components/ui/alert'; +import { Button } from '~/components/ui/button'; + +export default function VtmAlert() { + const [isOpen, setIsOpen] = useState(true); + + return ( + isOpen && ( + + + Important! + + Regular dice are summed with hunger dice to determine the total dice rolled. Hunger dice are a + special type of die that can result in critical successes or failures, so read the rules carefully! + + + + + + ) + ); +} diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 5f8777c..24acbae 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -1,9 +1,8 @@ import { zodResolver } from '@hookform/resolvers/zod'; -import { InfoIcon } from 'lucide-react'; import { type BaseSyntheticEvent, useCallback, useState, useTransition } from 'react'; import { Controller, type Resolver, useForm } from 'react-hook-form'; import { z } from 'zod/v4'; -import { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert'; +import VtmAlert from '~/components/pages/utilities/dice-roller/vtm-alert'; import { Button } from '~/components/ui/button'; import { ButtonGroup } from '~/components/ui/button-group'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '~/components/ui/card'; @@ -108,15 +107,7 @@ export default function VtmDiceRoller() { - - - Important! - - Regular dice are summed with hunger dice to determine the total dice rolled. Hunger dice are a - special type of die that can result in critical successes or failures, so read the rules - carefully! - - +
Date: Wed, 5 Aug 2026 10:53:24 -0300 Subject: [PATCH 7/8] feat(dice-roller): add visual representation for dice rolls with images and update disclaimer --- .../utilities/dice-roller/vtm-dice-roller.tsx | 135 +++++++++++++++--- 1 file changed, 117 insertions(+), 18 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index 24acbae..a6fbb6c 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -1,6 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { type BaseSyntheticEvent, useCallback, useState, useTransition } from 'react'; import { Controller, type Resolver, useForm } from 'react-hook-form'; +import { Link } from 'react-router'; import { z } from 'zod/v4'; import VtmAlert from '~/components/pages/utilities/dice-roller/vtm-alert'; import { Button } from '~/components/ui/button'; @@ -11,6 +12,15 @@ import { Input } from '~/components/ui/input'; import useIsMobile from '~/hooks/use-mobile'; import { rollDie } from '~/lib/dice-roller.utils'; +// Dice import +import hungerDieBestialFailure from '~/assets/dice/vtm/Dice_Hunger_BestialFailure.png'; +import hungerDieFailure from '~/assets/dice/vtm/Dice_Hunger_Failure.png'; +import hungerDieMessyCritical from '~/assets/dice/vtm/Dice_Hunger_MessyCritical.png'; +import hungerDieSuccess from '~/assets/dice/vtm/Dice_Hunger_Success.png'; +import regularDieCritical from '~/assets/dice/vtm/Dice_Regular_Critical.png'; +import regularDieFailure from '~/assets/dice/vtm/Dice_Regular_Failure.png'; +import regularDieSuccess from '~/assets/dice/vtm/Dice_Regular_Success.png'; + type DiceRollData = { regularDiceRoll: number[]; hungerDiceRoll: number[]; @@ -210,25 +220,114 @@ export default function VtmDiceRoller() {
-
- - {currentRoll.regularDiceRoll.length > 0 || currentRoll.hungerDiceRoll.length > 0 ? ( -
-

Current Roll:

-
- {currentRoll.regularDiceRoll.map((roll, index) => ( - {roll} - ))} -
- {currentRoll.hungerDiceRoll.length > 0 && ( -
- {currentRoll.hungerDiceRoll.map((roll, index) => ( - {roll} - ))} + +
+ {currentRoll.regularDiceRoll.length > 0 || currentRoll.hungerDiceRoll.length > 0 ? ( +
+
+ {currentRoll.regularDiceRoll.map((roll, index) => { + switch (true) { + case roll < 8: + return ( + {`Regular + ); + case roll >= 8 && roll < 10: + return ( + {`Regular + ); + case roll === 10: + return ( + {`Regular + ); + default: + return null; + } + })} + {currentRoll.hungerDiceRoll.map((roll, index) => { + switch (true) { + case roll > 1 && roll < 8: + return ( + {`Hunger + ); + case roll >= 8 && roll < 10: + return ( + {`Hunger + ); + case roll === 10: + return ( + {`Hunger + ); + case roll === 1: + return ( + {`Hunger + ); + default: + return null; + } + })}
- )} -
- ) : null} +
+ ) : ( +

No dice rolled yet.

+ )} +
+ + + +

+ Disclaimer: Dice Images from the{' '} + + 5th Edition Art Pack + {' '} + from White Wolf. +

); From 58085ffd9a98f834aa924bfafb31cdd33deb7719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:26:45 -0300 Subject: [PATCH 8/8] refactor(dice-roller): enhance rollDice function with difficulty checks and update dice roll logic --- .../utilities/dice-roller/vtm-dice-roller.tsx | 160 +++++++++++++++--- 1 file changed, 140 insertions(+), 20 deletions(-) diff --git a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx index a6fbb6c..f705461 100644 --- a/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx +++ b/app/components/pages/utilities/dice-roller/vtm-dice-roller.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { floor } from 'mathjs'; import { type BaseSyntheticEvent, useCallback, useState, useTransition } from 'react'; import { Controller, type Resolver, useForm } from 'react-hook-form'; import { Link } from 'react-router'; @@ -20,43 +21,49 @@ import hungerDieSuccess from '~/assets/dice/vtm/Dice_Hunger_Success.png'; import regularDieCritical from '~/assets/dice/vtm/Dice_Regular_Critical.png'; import regularDieFailure from '~/assets/dice/vtm/Dice_Regular_Failure.png'; import regularDieSuccess from '~/assets/dice/vtm/Dice_Regular_Success.png'; +import { cn } from '~/lib/utils'; type DiceRollData = { regularDiceRoll: number[]; hungerDiceRoll: number[]; + difficulty: number | undefined; }; const VtmDiceRollerSchema = z.object({ regularDice: z.coerce.number().min(1).max(10).default(1), hungerDice: z.coerce.number().min(0).max(5).default(0), - difficulty: z.coerce.number().min(2).max(10).default(4).optional(), + difficulty: z.coerce.number().min(1).max(10).default(3).optional(), }); type VtmDiceRollerSchemaType = z.infer; export default function VtmDiceRoller() { - const [currentRoll, setCurrentRoll] = useState({ regularDiceRoll: [], hungerDiceRoll: [] }); + const [currentRoll, setCurrentRoll] = useState({ + regularDiceRoll: [], + hungerDiceRoll: [], + difficulty: undefined, + }); const [isPending, startTransition] = useTransition(); const { isMobile } = useIsMobile(); const form = useForm({ resolver: zodResolver(VtmDiceRollerSchema) as Resolver, - defaultValues: { regularDice: 1, hungerDice: 0, difficulty: 4 }, + defaultValues: { regularDice: 1, hungerDice: 0, difficulty: 3 }, }); - function rollDice(regularDice: number, hungerDice: number, difficulty: number | undefined) { - if (hungerDice > regularDice) { + function rollDice(regularDice: number, hungerDice: number, difficulty: number | undefined, isRouseCheck: boolean) { + if (hungerDice > regularDice && !isRouseCheck) { alert('Hunger dice cannot exceed total dice.'); return; } - if (difficulty && (difficulty < 2 || difficulty > 10)) { - alert('Difficulty must be between 2 and 10.'); + if (difficulty && (difficulty < 1 || difficulty > 10) && !isRouseCheck) { + alert('Difficulty must be between 1 and 10.'); return; } - if (regularDice < 1 || regularDice > 10) { - alert('Total dice must be between 1 and 10.'); + if ((regularDice < 1 || regularDice > 10) && !isRouseCheck) { + alert('Regular dice must be between 1 and 10.'); return; } @@ -65,7 +72,17 @@ export default function VtmDiceRoller() { return; } - setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [] }); + if (difficulty && !isRouseCheck) { + const totalAvailableDice = regularDice + hungerDice; + const maximumPotentialSuccesses = totalAvailableDice * 2; + + if (maximumPotentialSuccesses < difficulty) { + alert('Not enough dice to potentially meet the difficulty.'); + return; + } + } + + setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [], difficulty }); for (let i = 0; i < regularDice; i++) { const roll = rollDie(10); @@ -86,12 +103,10 @@ export default function VtmDiceRoller() { switch (buttonClicked) { case 'roll-dice': - rollDice(regularDice, hungerDice, difficulty); + rollDice(regularDice, hungerDice, difficulty, false); break; case 'rouse-check': - console.log( - `Performing rouse check with ${regularDice} regular dice and ${hungerDice} hunger dice.`, - ); + rollDice(0, 1, 1, true); break; default: throw new Error('Something went wrong. Please try again.'); @@ -101,11 +116,102 @@ export default function VtmDiceRoller() { [startTransition], ); + function calculateResults(regularDiceRoll: number[], hungerDiceRoll: number[], difficulty: number | undefined) { + let successCount = 0; + let criticalCount = 0; + let messyCriticalCount = 0; + let bestialFailureCount = 0; + + // Process regular dice rolls + for (const roll of regularDiceRoll) { + if (roll >= 6 && roll < 10) { + successCount++; + } else if (roll === 10) { + criticalCount++; + successCount += 2; // Each critical counts as two successes + } + } + + for (const roll of hungerDiceRoll) { + if (roll >= 6 && roll < 10) { + successCount++; + } else if (roll === 10) { + messyCriticalCount++; + successCount += 2; // Each messy critical counts as two successes + } else if (roll === 1) { + bestialFailureCount++; + } + } + + // Adjust critical successes + const totalCriticals = criticalCount + messyCriticalCount; + const criticalPairs = floor(totalCriticals / 2); + successCount += criticalPairs * 2; // Each pair of criticals counts as two successes + + const hasCritical = criticalPairs >= 1; + const hasMessyCritical = hasCritical && messyCriticalCount > 0; + + // Determine the outcome + let outcome: string; + let outcomeClassName: string; + + const isRouseCheck = regularDiceRoll.length === 0 && hungerDiceRoll.length === 1 && difficulty === 1; + + if (isRouseCheck) { + if (successCount >= 1) { + outcome = 'Rouse Check Success'; + outcomeClassName = cn('bg-green-100 text-green-800'); + } else { + outcome = 'Rouse Check Failure (Increase Hunger by 1)'; + outcomeClassName = cn('bg-red-100 text-red-800'); + if (bestialFailureCount > 0) { + outcome += ' and Bestial Failure (Compulsion Check)'; + outcomeClassName = cn('bg-red-100 text-red-800'); + } + } + + return { outcome, outcomeClassName }; + } + + if (difficulty !== undefined) { + if (successCount >= difficulty) { + if (hasMessyCritical) { + outcome = `Messy Critical Success (${successCount}/${difficulty})`; + outcomeClassName = cn('bg-yellow-100 text-yellow-800'); + } else if (hasCritical) { + outcome = `Critical Success (${successCount}/${difficulty})`; + outcomeClassName = cn('bg-green-100 text-green-800'); + } else { + outcome = `Success (${successCount}/${difficulty})`; + outcomeClassName = cn('bg-green-100 text-green-800'); + } + } else if (bestialFailureCount > 0) { + outcome = `Bestial Failure (${successCount}/${difficulty})`; + outcomeClassName = cn('bg-red-100 text-red-800'); + } else { + outcome = `Failure (${successCount}/${difficulty})`; + outcomeClassName = cn('bg-red-100 text-red-800'); + } + } else { + outcome = `Successes: ${successCount}`; + outcomeClassName = cn('bg-green-100 text-green-800'); + if (hasMessyCritical) { + outcome += `\n Messy Critical Possible`; + } else if (hasCritical) { + outcome += `\n Critical Possible`; + } + } + + return { outcome, outcomeClassName }; + } + function reset() { form.reset(); - setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [] }); + setCurrentRoll({ regularDiceRoll: [], hungerDiceRoll: [], difficulty: undefined }); } + const hasRolledDice = currentRoll.regularDiceRoll.length > 0 || currentRoll.hungerDiceRoll.length > 0; + return ( @@ -222,12 +328,26 @@ export default function VtmDiceRoller() {
- {currentRoll.regularDiceRoll.length > 0 || currentRoll.hungerDiceRoll.length > 0 ? ( + {hasRolledDice && + (() => { + const { outcome, outcomeClassName } = calculateResults( + currentRoll.regularDiceRoll, + currentRoll.hungerDiceRoll, + currentRoll.difficulty, + ); + + return ( +
+ {outcome} +
+ ); + })()} + {hasRolledDice ? (
{currentRoll.regularDiceRoll.map((roll, index) => { switch (true) { - case roll < 8: + case roll < 6: return ( ); - case roll >= 8 && roll < 10: + case roll >= 6 && roll < 10: return ( { switch (true) { - case roll > 1 && roll < 8: + case roll > 1 && roll < 6: return ( ); - case roll >= 8 && roll < 10: + case roll >= 6 && roll < 10: return (