Add dice-roll animation and tighten attack-roll prompt rules

Broadcast a "rolling" WS event when the DM invokes roll_dice, and show
a cycling dice-emoji animation with the roll notation in the chat while
waiting for the result, instead of just the generic typing indicator.

Also split the dice-roll prompt rule into skill/save checks (DC shown
to the player) vs. attack rolls (AC stays secret but must still be
labeled as a roll), and made the "always roll yourself, never ask the
player to roll" instruction unconditional.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thorsten
2026-09-01 17:28:37 +02:00
parent 624644619c
commit 2b99451fdf
6 changed files with 62 additions and 11 deletions
+35 -6
View File
@@ -1,8 +1,10 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { Message } from "../api/games";
import MarkdownContent from "./MarkdownContent";
const DICE_FACES = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"];
function senderLabel(message: Message): string {
if (message.sender_type === "dm") return "Dungeon Master";
if (message.sender_type === "system") return "System";
@@ -10,18 +12,41 @@ function senderLabel(message: Message): string {
return message.player_name ?? "Spieler";
}
function DiceRollAnimation({ notation }: { notation: string }) {
const [faceIndex, setFaceIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setFaceIndex((i) => (i + 1) % DICE_FACES.length);
}, 120);
return () => clearInterval(interval);
}, []);
return (
<div className="text-slate-100">
<span className="font-semibold text-amber-400">Dungeon Master würfelt</span>{" "}
<span className="inline-block text-xl" aria-hidden="true">
{DICE_FACES[faceIndex]}
</span>{" "}
<span className="text-sm text-slate-400">{notation && `(${notation})`}</span>
</div>
);
}
export default function TranscriptPane({
messages,
typing,
rolling,
}: {
messages: Message[];
typing?: boolean;
rolling?: string | null;
}) {
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
bottomRef.current?.scrollIntoView({ block: "end" });
}, [messages, typing]);
}, [messages, typing, rolling]);
return (
<div className="flex h-full flex-col gap-3 overflow-y-auto p-4">
@@ -41,10 +66,14 @@ export default function TranscriptPane({
<MarkdownContent content={m.content} />
</div>
))}
{typing && (
<div className="text-slate-100">
<span className="animate-pulse font-semibold text-amber-400">Dungeon Master schreibt …</span>
</div>
{rolling != null ? (
<DiceRollAnimation notation={rolling} />
) : (
typing && (
<div className="text-slate-100">
<span className="animate-pulse font-semibold text-amber-400">Dungeon Master schreibt …</span>
</div>
)
)}
<div ref={bottomRef} />
</div>