darkwatch/server/src/socket.ts
Aaron Wood 33032bcd07 Add torch timer, fog atmosphere, DM card redesign, and avatars
- Torch timer: 60min countdown per character, visual warnings at 10m/5m/1m
- Fog overlay: CSS radial gradient layers with seamless infinite drift
- Fog synced across all clients via socket, adapts to light/dark themes
- DM card redesign: compact layout with HP/AC/luck/torch + modifier row
- Grid changed to 3-up (from 4) with larger fonts
- DiceBear avatars on cards and character sheets with style picker
- Campaign name shown in header
- Server: JSON.stringify fix for object fields in PATCH handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 14:41:22 -04:00

115 lines
3.2 KiB
TypeScript

import { Server } from "socket.io";
import db from "./db.js";
import { rollDice } from "./dice.js";
export function setupSocket(io: Server) {
io.on("connection", (socket) => {
socket.on("join-campaign", (campaignId: string) => {
socket.join(`campaign:${campaignId}`);
});
socket.on("leave-campaign", (campaignId: string) => {
socket.leave(`campaign:${campaignId}`);
});
socket.on(
"roll:request",
(data: {
campaignId: number;
characterId?: number;
characterName?: string;
characterColor?: string;
type: string;
dice: string;
label: string;
modifier?: number;
advantage?: boolean;
disadvantage?: boolean;
}) => {
const result = rollDice(data.dice, {
advantage: data.advantage,
disadvantage: data.disadvantage,
});
if (result.error) {
socket.emit("roll:error", { error: result.error });
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
.prepare(
`
INSERT INTO roll_log (campaign_id, character_id, character_name, character_color, type, label, dice_expression, rolls, modifier, total, advantage, disadvantage, nat20)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
)
.run(
data.campaignId,
data.characterId ?? null,
data.characterName || "Roll",
data.characterColor || "",
data.type || "custom",
data.label,
data.dice,
JSON.stringify(result.rolls),
result.modifier,
result.total,
data.advantage ? 1 : 0,
data.disadvantage ? 1 : 0,
nat20 ? 1 : 0,
);
const saved = db
.prepare("SELECT * FROM roll_log WHERE id = ?")
.get(row.lastInsertRowid) as Record<string, unknown>;
const broadcast = {
...saved,
rolls: result.rolls,
advantage: data.advantage || false,
disadvantage: data.disadvantage || false,
nat20,
};
io.to(`campaign:${data.campaignId}`).emit("roll:result", broadcast);
},
);
socket.on(
"atmosphere:update",
(data: { campaignId: number; fog: boolean }) => {
io.to(`campaign:${data.campaignId}`).emit("atmosphere:update", {
fog: data.fog,
});
},
);
socket.on("disconnect", () => {
// Rooms are cleaned up automatically by Socket.IO
});
});
}
export function broadcastToCampaign(
io: Server,
campaignId: number,
event: string,
data: unknown,
) {
io.to(`campaign:${campaignId}`).emit(event, data);
}