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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user