This skill should be used when creating chat messages, sending roll results to chat, configuring speakers, implementing whispers and roll modes, or hooking into chat message rendering.
Create and customize Foundry VTT chat messages, including whispers, roll results, and speaker configuration. Use when sending dice rolls, implementing chat commands, or adding custom rendering hooks to messages.
/plugin marketplace add ImproperSubset/hh-agentics/plugin install fvtt-dev@hh-agenticsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Domain: Foundry VTT Module/System Development Status: Production-Ready Last Updated: 2026-01-05
ChatMessage documents display in the chat log and handle rolls, whispers, and player communication. Understanding message creation and hooks is essential for game system features.
{
_id: "documentId",
author: "userId", // Who created the message
content: "<p>HTML</p>", // Message content
flavor: "Roll description", // Flavor text for rolls
speaker: { // Who is "speaking"
scene: "sceneId",
actor: "actorId",
token: "tokenId",
alias: "Display Name"
},
whisper: ["userId"], // Private recipients
blind: false, // GM-only visibility
rolls: [], // Dice roll data
sound: "audio/path.ogg", // Sound to play
flags: {} // Custom data
}
await ChatMessage.create({
content: "Hello, world!"
});
await ChatMessage.create({
content: "I attack the dragon!",
speaker: ChatMessage.getSpeaker({ actor: myActor })
});
await ChatMessage.create({
content: `
<h2>Critical Hit!</h2>
<p>You deal <strong>24</strong> damage.</p>
`,
speaker: ChatMessage.getSpeaker({ token: myToken })
});
await ChatMessage.create({
content: "Attack roll",
flags: {
"my-module": {
rollType: "attack",
targetId: target.id
}
}
});
const roll = new Roll("1d20 + @mod", { mod: 5 });
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Attack Roll"
});
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Stealth Check"
}, {
rollMode: game.settings.get("core", "rollMode")
});
const attackRoll = new Roll("1d20 + 5");
const damageRoll = new Roll("2d6 + 3");
await attackRoll.evaluate();
await damageRoll.evaluate();
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: "Attack and Damage",
rolls: [attackRoll, damageRoll]
});
// From controlled token (default)
const speaker = ChatMessage.getSpeaker();
// From specific actor
const speaker = ChatMessage.getSpeaker({ actor: myActor });
// From specific token
const speaker = ChatMessage.getSpeaker({ token: myToken });
// Custom alias
const speaker = ChatMessage.getSpeaker({ alias: "The Narrator" });
{
scene: "sceneId", // Scene where speaker is
actor: "actorId", // Actor document ID
token: "tokenId", // Token document ID
alias: "Display Name" // Fallback name
}
const actor = ChatMessage.getSpeakerActor(message.speaker);
| Mode | Visibility | Command |
|---|---|---|
| Public | Everyone | /publicroll |
| GM | Roller + GMs | /gmroll |
| Blind | GMs only | /blindroll |
| Self | Roller only | /selfroll |
// Get current setting
const rollMode = game.settings.get("core", "rollMode");
// Apply to roll
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor })
}, {
rollMode: rollMode
});
// Single user
await ChatMessage.create({
content: "Secret message",
whisper: [targetUserId]
});
// Multiple users
await ChatMessage.create({
content: "Group secret",
whisper: [user1Id, user2Id]
});
// All GMs
await ChatMessage.create({
content: "GM only",
whisper: game.users.filter(u => u.isGM).map(u => u.id)
});
// GM sees content, others see "???"
await ChatMessage.create({
content: "Secret roll result: 15",
blind: true,
whisper: game.users.filter(u => u.isGM).map(u => u.id)
});
Hooks.on("renderChatMessageHTML", (message, html, context) => {
// message: ChatMessage document
// html: HTMLElement
// context: Rendering context
// Add custom styling
if (message.flags["my-module"]?.critical) {
html.classList.add("critical-hit");
}
// Add buttons
const button = document.createElement("button");
button.textContent = "Apply Damage";
button.addEventListener("click", () => applyDamage(message));
html.querySelector(".message-content").append(button);
});
Hooks.on("preCreateChatMessage", (message, data, options, userId) => {
// Modify before creation
message.updateSource({
content: data.content + " (modified)"
});
// Return false to cancel
return true;
});
Hooks.on("createChatMessage", (message, options, userId) => {
// After creation, for all clients
console.log("New message:", message.content);
});
Hooks.on("chatMessage", (chatLog, messageText, chatData) => {
// When user sends message via input
// Return false to prevent default handling
if (messageText.startsWith("/custom")) {
handleCustomCommand(messageText);
return false;
}
});
async function attackRoll(actor, target) {
const roll = new Roll("1d20 + @mod", actor.getRollData());
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: `Attack vs ${target.name}`,
flags: {
"my-system": {
type: "attack",
targetId: target.id,
total: roll.total
}
}
});
}
// Handle button clicks
Hooks.on("renderChatMessageHTML", (message, html) => {
const flags = message.flags["my-system"];
if (flags?.type !== "attack") return;
html.querySelector(".apply-damage")?.addEventListener("click", () => {
const target = game.actors.get(flags.targetId);
// Apply damage logic
});
});
await ChatMessage.create({
content: `
<div class="roll-result">
<h3>Attack Roll: 18</h3>
<details>
<summary>Details</summary>
<p>Base: 1d20 = 13</p>
<p>Modifier: +5</p>
</details>
</div>
`
});
await ChatMessage.create({
content: "The bell tolls...",
sound: "sounds/bell.ogg"
});
// WRONG - total is undefined
const roll = new Roll("1d20");
await roll.toMessage(); // roll.total undefined!
// CORRECT
const roll = new Roll("1d20");
await roll.evaluate();
await roll.toMessage();
// WRONG - uses first controlled token
ChatMessage.getSpeaker();
// CORRECT - specify the token
ChatMessage.getSpeaker({ token: specificToken });
// Roll messages override whisper with rollMode
// Use rollMode for roll messages:
await roll.toMessage({}, {
rollMode: "gmroll" // Not whisper: [...]
});
// WRONG - always public
await roll.toMessage();
// CORRECT - respect user setting
await roll.toMessage({}, {
rollMode: game.settings.get("core", "rollMode")
});
// Updating too fast causes UI issues
// Wait for notification to fade (~3 seconds)
const msg = await ChatMessage.create({ content: "Loading..." });
setTimeout(() => {
msg.update({ content: "Done!" });
}, 3500);
// Check if message is visible to current user
if (!message.visible) return;
// Check if content is visible (not just presence)
if (message.isContentVisible) {
// Safe to read content
}
ChatMessage.getSpeaker() for proper speakerawait roll.evaluate() before toMessagegame.settings.get("core", "rollMode")renderChatMessageHTML hook for customizationLast Updated: 2026-01-05 Status: Production-Ready Maintainer: ImproperSubset
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.