Split attack/damage dice buttons, nat 20 highlighting, crit damage, character colors
This commit is contained in:
parent
9b1a0df8a5
commit
3e2e43ca95
15 changed files with 160 additions and 43 deletions
|
|
@ -66,6 +66,11 @@
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rollButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ interface AttackBlockProps {
|
||||||
campaignId?: number;
|
campaignId?: number;
|
||||||
characterId?: number;
|
characterId?: number;
|
||||||
characterName?: string;
|
characterName?: string;
|
||||||
|
characterColor?: string;
|
||||||
mode?: "view" | "edit";
|
mode?: "view" | "edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,6 +16,7 @@ export default function AttackBlock({
|
||||||
campaignId,
|
campaignId,
|
||||||
characterId,
|
characterId,
|
||||||
characterName,
|
characterName,
|
||||||
|
characterColor,
|
||||||
mode,
|
mode,
|
||||||
}: AttackBlockProps) {
|
}: AttackBlockProps) {
|
||||||
const weapons = attacks.filter((a) => !a.isTalent);
|
const weapons = attacks.filter((a) => !a.isTalent);
|
||||||
|
|
@ -41,16 +43,30 @@ export default function AttackBlock({
|
||||||
<span className={styles.damage}>{atk.damage}</span>
|
<span className={styles.damage}>{atk.damage}</span>
|
||||||
</span>
|
</span>
|
||||||
{mode === "view" && campaignId ? (
|
{mode === "view" && campaignId ? (
|
||||||
|
<span className={styles.rollButtons}>
|
||||||
<DiceButton
|
<DiceButton
|
||||||
campaignId={campaignId}
|
campaignId={campaignId}
|
||||||
characterId={characterId}
|
characterId={characterId}
|
||||||
characterName={characterName}
|
characterName={characterName}
|
||||||
|
characterColor={characterColor}
|
||||||
type="attack"
|
type="attack"
|
||||||
dice={`1d20${atk.modifier >= 0 ? "+" + atk.modifier : String(atk.modifier)}`}
|
dice={`1d20${atk.modifier >= 0 ? "+" + atk.modifier : String(atk.modifier)}`}
|
||||||
label={`${atk.name} attack`}
|
label={`${atk.name} attack`}
|
||||||
damageDice={atk.damage}
|
icon="⚔"
|
||||||
damageLabel={`${atk.name} damage`}
|
title={`Attack roll: 1d20${atk.modifier >= 0 ? "+" + atk.modifier : atk.modifier}`}
|
||||||
/>
|
/>
|
||||||
|
<DiceButton
|
||||||
|
campaignId={campaignId}
|
||||||
|
characterId={characterId}
|
||||||
|
characterName={characterName}
|
||||||
|
characterColor={characterColor}
|
||||||
|
type="attack"
|
||||||
|
dice={atk.damage}
|
||||||
|
label={`${atk.name} damage`}
|
||||||
|
icon="💥"
|
||||||
|
title={`Damage: ${atk.damage} (Shift: crit = double dice)`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.rollSpace}></span>
|
<span className={styles.rollSpace}></span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ export default function CharacterCard({
|
||||||
const totalSlots = character.gear.reduce((sum, g) => sum + g.slot_count, 0);
|
const totalSlots = character.gear.reduce((sum, g) => sum + g.slot_count, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.card} onClick={() => onClick(character.id)}>
|
<div
|
||||||
|
className={styles.card}
|
||||||
|
onClick={() => onClick(character.id)}
|
||||||
|
style={{ borderLeftColor: character.color, borderLeftWidth: "3px" }}
|
||||||
|
>
|
||||||
<div className={styles.cardHeader}>
|
<div className={styles.cardHeader}>
|
||||||
<span className={styles.name}>
|
<span className={styles.name}>
|
||||||
{character.name}
|
{character.name}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,10 @@ export default function CharacterSheet({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* HEADER BANNER */}
|
{/* HEADER BANNER */}
|
||||||
<div className={styles.banner}>
|
<div
|
||||||
|
className={styles.banner}
|
||||||
|
style={{ borderLeft: `3px solid ${character.color}` }}
|
||||||
|
>
|
||||||
<div className={styles.identity}>
|
<div className={styles.identity}>
|
||||||
{mode === "edit" ? (
|
{mode === "edit" ? (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
.btn {
|
.btn {
|
||||||
width: 24px;
|
width: 22px;
|
||||||
height: 24px;
|
height: 22px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #444;
|
border: 1px solid #444;
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
|
|
|
||||||
|
|
@ -5,59 +5,66 @@ interface DiceButtonProps {
|
||||||
campaignId: number;
|
campaignId: number;
|
||||||
characterId?: number;
|
characterId?: number;
|
||||||
characterName?: string;
|
characterName?: string;
|
||||||
|
characterColor?: string;
|
||||||
type: "attack" | "ability-check" | "custom";
|
type: "attack" | "ability-check" | "custom";
|
||||||
dice: string;
|
dice: string;
|
||||||
label: string;
|
label: string;
|
||||||
damageDice?: string;
|
icon?: string;
|
||||||
damageLabel?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DiceButton({
|
export default function DiceButton({
|
||||||
campaignId,
|
campaignId,
|
||||||
characterId,
|
characterId,
|
||||||
characterName,
|
characterName,
|
||||||
|
characterColor,
|
||||||
type,
|
type,
|
||||||
dice,
|
dice,
|
||||||
label,
|
label,
|
||||||
damageDice,
|
icon = "🎲",
|
||||||
damageLabel,
|
title: titleProp,
|
||||||
}: DiceButtonProps) {
|
}: DiceButtonProps) {
|
||||||
function handleClick(e: React.MouseEvent) {
|
function handleClick(e: React.MouseEvent) {
|
||||||
const advantage = e.shiftKey;
|
const advantage = e.shiftKey;
|
||||||
const disadvantage = e.ctrlKey || e.metaKey;
|
const disadvantage = e.ctrlKey || e.metaKey;
|
||||||
|
|
||||||
socket.emit("roll:request", {
|
let actualDice = dice;
|
||||||
campaignId,
|
let actualLabel = label;
|
||||||
characterId,
|
let sendAdvantage = advantage && !disadvantage;
|
||||||
characterName,
|
let sendDisadvantage = disadvantage && !advantage;
|
||||||
type,
|
|
||||||
dice,
|
// For damage rolls (non-d20), shift = crit (double dice) instead of advantage
|
||||||
label,
|
const isD20 = dice.toLowerCase().includes("d20");
|
||||||
advantage: advantage && !disadvantage,
|
if (!isD20 && advantage) {
|
||||||
disadvantage: disadvantage && !advantage,
|
const match = dice.match(/^(\d*)d(\d+)(.*)/i);
|
||||||
});
|
if (match) {
|
||||||
|
const count = match[1] ? parseInt(match[1], 10) : 1;
|
||||||
|
actualDice = `${count * 2}d${match[2]}${match[3] || ""}`;
|
||||||
|
actualLabel = label + " (CRIT)";
|
||||||
|
}
|
||||||
|
sendAdvantage = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (damageDice && damageLabel) {
|
|
||||||
setTimeout(() => {
|
|
||||||
socket.emit("roll:request", {
|
socket.emit("roll:request", {
|
||||||
campaignId,
|
campaignId,
|
||||||
characterId,
|
characterId,
|
||||||
characterName,
|
characterName,
|
||||||
type: "attack",
|
characterColor,
|
||||||
dice: damageDice,
|
type,
|
||||||
label: damageLabel,
|
dice: actualDice,
|
||||||
|
label: actualLabel,
|
||||||
|
advantage: sendAdvantage,
|
||||||
|
disadvantage: sendDisadvantage,
|
||||||
});
|
});
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={styles.btn}
|
className={styles.btn}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
title={`Roll ${dice} (Shift: advantage, Ctrl: disadvantage)`}
|
title={titleProp || `Roll ${dice} (Shift: advantage, Ctrl: disadvantage)`}
|
||||||
>
|
>
|
||||||
🎲
|
{icon}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card.nat20 {
|
||||||
|
border-color: #ffd700;
|
||||||
|
background: linear-gradient(135deg, #1a1a0e, #0f1a30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.nat20 .total {
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
.card.fresh {
|
.card.fresh {
|
||||||
border-color: #c9a84c;
|
border-color: #c9a84c;
|
||||||
animation:
|
animation:
|
||||||
|
|
@ -104,3 +113,28 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critBanner {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffd700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
animation: critPulse 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes critPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,19 @@ export default function RollEntry({ roll, fresh }: RollEntryProps) {
|
||||||
: isDisadvantage
|
: isDisadvantage
|
||||||
? Math.min(...rolls)
|
? Math.min(...rolls)
|
||||||
: null;
|
: null;
|
||||||
|
const isNat20 = roll.nat20;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.card} ${fresh ? styles.fresh : ""}`}>
|
<div
|
||||||
|
className={`${styles.card} ${fresh ? styles.fresh : ""} ${isNat20 ? styles.nat20 : ""}`}
|
||||||
|
>
|
||||||
<div className={styles.topLine}>
|
<div className={styles.topLine}>
|
||||||
<span className={styles.charName}>{roll.character_name}</span>
|
<span
|
||||||
|
className={styles.charName}
|
||||||
|
style={{ color: roll.character_color || "#c9a84c" }}
|
||||||
|
>
|
||||||
|
{roll.character_name}
|
||||||
|
</span>
|
||||||
<span className={styles.timestamp}>{timeAgo(roll.created_at)}</span>
|
<span className={styles.timestamp}>{timeAgo(roll.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.label}>
|
<div className={styles.label}>
|
||||||
|
|
@ -40,6 +48,7 @@ export default function RollEntry({ roll, fresh }: RollEntryProps) {
|
||||||
{isAdvantage && <span className={styles.advantage}> ADV</span>}
|
{isAdvantage && <span className={styles.advantage}> ADV</span>}
|
||||||
{isDisadvantage && <span className={styles.disadvantage}> DIS</span>}
|
{isDisadvantage && <span className={styles.disadvantage}> DIS</span>}
|
||||||
</div>
|
</div>
|
||||||
|
{isNat20 && <div className={styles.critBanner}>NAT 20!</div>}
|
||||||
<div className={styles.breakdown}>
|
<div className={styles.breakdown}>
|
||||||
{dice_expression}: [
|
{dice_expression}: [
|
||||||
{rolls.map((r, i) => (
|
{rolls.map((r, i) => (
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface StatBlockProps {
|
||||||
campaignId?: number;
|
campaignId?: number;
|
||||||
characterId?: number;
|
characterId?: number;
|
||||||
characterName?: string;
|
characterName?: string;
|
||||||
|
characterColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function StatBlock({
|
export default function StatBlock({
|
||||||
|
|
@ -21,6 +22,7 @@ export default function StatBlock({
|
||||||
campaignId,
|
campaignId,
|
||||||
characterId,
|
characterId,
|
||||||
characterName,
|
characterName,
|
||||||
|
characterColor,
|
||||||
}: StatBlockProps) {
|
}: StatBlockProps) {
|
||||||
const statMap = new Map(stats.map((s) => [s.stat_name, s.value]));
|
const statMap = new Map(stats.map((s) => [s.stat_name, s.value]));
|
||||||
|
|
||||||
|
|
@ -47,6 +49,7 @@ export default function StatBlock({
|
||||||
campaignId={campaignId}
|
campaignId={campaignId}
|
||||||
characterId={characterId}
|
characterId={characterId}
|
||||||
characterName={characterName}
|
characterName={characterName}
|
||||||
|
characterColor={characterColor}
|
||||||
type="ability-check"
|
type="ability-check"
|
||||||
dice={`1d20${mod >= 0 ? "+" + mod : String(mod)}`}
|
dice={`1d20${mod >= 0 ? "+" + mod : String(mod)}`}
|
||||||
label={`${name} check`}
|
label={`${name} check`}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export default function StatsPanel({
|
||||||
campaignId={campaignId}
|
campaignId={campaignId}
|
||||||
characterId={character.id}
|
characterId={character.id}
|
||||||
characterName={character.name}
|
characterName={character.name}
|
||||||
|
characterColor={character.color}
|
||||||
/>
|
/>
|
||||||
<hr className={styles.separator} />
|
<hr className={styles.separator} />
|
||||||
<AttackBlock
|
<AttackBlock
|
||||||
|
|
@ -38,6 +39,7 @@ export default function StatsPanel({
|
||||||
campaignId={campaignId}
|
campaignId={campaignId}
|
||||||
characterId={character.id}
|
characterId={character.id}
|
||||||
characterName={character.name}
|
characterName={character.name}
|
||||||
|
characterColor={character.color}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export interface Character {
|
||||||
cp: number;
|
cp: number;
|
||||||
gear_slots_max: number;
|
gear_slots_max: number;
|
||||||
overrides: Record<string, unknown>;
|
overrides: Record<string, unknown>;
|
||||||
|
color: string;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
gear: Gear[];
|
gear: Gear[];
|
||||||
talents: Talent[];
|
talents: Talent[];
|
||||||
|
|
@ -90,6 +91,7 @@ export interface RollResult {
|
||||||
campaign_id: number;
|
campaign_id: number;
|
||||||
character_id: number | null;
|
character_id: number | null;
|
||||||
character_name: string;
|
character_name: string;
|
||||||
|
character_color: string;
|
||||||
type: "attack" | "ability-check" | "custom";
|
type: "attack" | "ability-check" | "custom";
|
||||||
label: string;
|
label: string;
|
||||||
dice_expression: string;
|
dice_expression: string;
|
||||||
|
|
@ -98,5 +100,6 @@ export interface RollResult {
|
||||||
total: number;
|
total: number;
|
||||||
advantage: boolean;
|
advantage: boolean;
|
||||||
disadvantage: boolean;
|
disadvantage: boolean;
|
||||||
|
nat20: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ db.exec(`
|
||||||
total INTEGER NOT NULL DEFAULT 0,
|
total INTEGER NOT NULL DEFAULT 0,
|
||||||
advantage INTEGER NOT NULL DEFAULT 0,
|
advantage INTEGER NOT NULL DEFAULT 0,
|
||||||
disadvantage INTEGER NOT NULL DEFAULT 0,
|
disadvantage INTEGER NOT NULL DEFAULT 0,
|
||||||
|
nat20 INTEGER NOT NULL DEFAULT 0,
|
||||||
|
character_color TEXT DEFAULT '',
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
@ -108,6 +110,9 @@ const v2Columns: Array<[string, string, string]> = [
|
||||||
["character_gear", "game_item_id", "INTEGER"],
|
["character_gear", "game_item_id", "INTEGER"],
|
||||||
["character_gear", "effects", "TEXT DEFAULT '{}'"],
|
["character_gear", "effects", "TEXT DEFAULT '{}'"],
|
||||||
["character_talents", "game_talent_id", "INTEGER"],
|
["character_talents", "game_talent_id", "INTEGER"],
|
||||||
|
["characters", "color", "TEXT DEFAULT ''"],
|
||||||
|
["roll_log", "nat20", "INTEGER DEFAULT 0"],
|
||||||
|
["roll_log", "character_color", "TEXT DEFAULT ''"],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [table, column, definition] of v2Columns) {
|
for (const [table, column, definition] of v2Columns) {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ const router = Router({ mergeParams: true });
|
||||||
|
|
||||||
const DEFAULT_STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
const DEFAULT_STATS = ["STR", "DEX", "CON", "INT", "WIS", "CHA"];
|
||||||
|
|
||||||
|
function generateCharacterColor(): string {
|
||||||
|
const hue = Math.floor(Math.random() * 360);
|
||||||
|
return `hsl(${hue}, 60%, 65%)`;
|
||||||
|
}
|
||||||
|
|
||||||
function parseJson(val: unknown): Record<string, unknown> {
|
function parseJson(val: unknown): Record<string, unknown> {
|
||||||
if (typeof val === "string") {
|
if (typeof val === "string") {
|
||||||
try {
|
try {
|
||||||
|
|
@ -74,8 +79,8 @@ router.post<CampaignParams>("/", (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertChar = db.prepare(`
|
const insertChar = db.prepare(`
|
||||||
INSERT INTO characters (campaign_id, name, class, ancestry, hp_current, hp_max)
|
INSERT INTO characters (campaign_id, name, class, ancestry, hp_current, hp_max, color)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const insertStat = db.prepare(
|
const insertStat = db.prepare(
|
||||||
|
|
@ -89,6 +94,7 @@ router.post<CampaignParams>("/", (req, res) => {
|
||||||
ancestry || "Human",
|
ancestry || "Human",
|
||||||
hp_max || 0,
|
hp_max || 0,
|
||||||
hp_max || 0,
|
hp_max || 0,
|
||||||
|
generateCharacterColor(),
|
||||||
);
|
);
|
||||||
const characterId = result.lastInsertRowid;
|
const characterId = result.lastInsertRowid;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ router.get("/", (req, res) => {
|
||||||
rolls: JSON.parse(r.rolls as string),
|
rolls: JSON.parse(r.rolls as string),
|
||||||
advantage: r.advantage === 1,
|
advantage: r.advantage === 1,
|
||||||
disadvantage: r.disadvantage === 1,
|
disadvantage: r.disadvantage === 1,
|
||||||
|
nat20: r.nat20 === 1,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json(parsed);
|
res.json(parsed);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export function setupSocket(io: Server) {
|
||||||
campaignId: number;
|
campaignId: number;
|
||||||
characterId?: number;
|
characterId?: number;
|
||||||
characterName?: string;
|
characterName?: string;
|
||||||
|
characterColor?: string;
|
||||||
type: string;
|
type: string;
|
||||||
dice: string;
|
dice: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -35,17 +36,33 @@ export function setupSocket(io: Server) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect nat 20
|
||||||
|
const isD20Roll = data.dice.match(/d20/i);
|
||||||
|
let nat20 = false;
|
||||||
|
if (isD20Roll && result.rolls.length > 0) {
|
||||||
|
if (data.advantage) {
|
||||||
|
// With advantage, nat20 if the chosen (higher) die is 20
|
||||||
|
nat20 = Math.max(...result.rolls) === 20;
|
||||||
|
} else if (data.disadvantage) {
|
||||||
|
// With disadvantage, nat20 if the chosen (lower) die is 20
|
||||||
|
nat20 = Math.min(...result.rolls) === 20;
|
||||||
|
} else {
|
||||||
|
nat20 = result.rolls[0] === 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const row = db
|
const row = db
|
||||||
.prepare(
|
.prepare(
|
||||||
`
|
`
|
||||||
INSERT INTO roll_log (campaign_id, character_id, character_name, type, label, dice_expression, rolls, modifier, total, advantage, disadvantage)
|
INSERT INTO roll_log (campaign_id, character_id, character_name, character_color, type, label, dice_expression, rolls, modifier, total, advantage, disadvantage, nat20)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
data.campaignId,
|
data.campaignId,
|
||||||
data.characterId ?? null,
|
data.characterId ?? null,
|
||||||
data.characterName || "Roll",
|
data.characterName || "Roll",
|
||||||
|
data.characterColor || "",
|
||||||
data.type || "custom",
|
data.type || "custom",
|
||||||
data.label,
|
data.label,
|
||||||
data.dice,
|
data.dice,
|
||||||
|
|
@ -54,6 +71,7 @@ export function setupSocket(io: Server) {
|
||||||
result.total,
|
result.total,
|
||||||
data.advantage ? 1 : 0,
|
data.advantage ? 1 : 0,
|
||||||
data.disadvantage ? 1 : 0,
|
data.disadvantage ? 1 : 0,
|
||||||
|
nat20 ? 1 : 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
const saved = db
|
const saved = db
|
||||||
|
|
@ -65,6 +83,7 @@ export function setupSocket(io: Server) {
|
||||||
rolls: result.rolls,
|
rolls: result.rolls,
|
||||||
advantage: data.advantage || false,
|
advantage: data.advantage || false,
|
||||||
disadvantage: data.disadvantage || false,
|
disadvantage: data.disadvantage || false,
|
||||||
|
nat20,
|
||||||
};
|
};
|
||||||
|
|
||||||
io.to(`campaign:${data.campaignId}`).emit("roll:result", broadcast);
|
io.to(`campaign:${data.campaignId}`).emit("roll:result", broadcast);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue