function formatResponse(text) { return text // Escape HTML entities first .replace(/&/g, "&") .replace(//g, ">") // Bold: **text** .replace(/\*\*(.+?)\*\*/g, "$1") // Italic: *text* .replace(/\*(.+?)\*/g, "$1") // Dice notation: 🎲 lines get highlighted .replace(/^(🎲.*)$/gm, '$1') // Line breaks .replace(/\n\n/g, "
")
.replace(/\n/g, "
")
// Wrap in paragraph
.replace(/^(.*)$/, "
$1
"); } Hooks.on("createChatMessage", async (chatData, options, userId) => { if (chatData.author.name !== 'Gamemaster') return; if (chatData.speaker.alias === 'AI DM') return; if(chatData.isRoll){ return const parts = []; const speaker = chatData.speaker?.alias || "Someone"; const flavor = chatData.flavor || ""; const pf2eFlags = chatData.flags?.pf2e || {}; const pf2eContext = pf2eFlags.context || {}; // Who rolled parts.push(`${speaker}`); // What type of roll (PF2E-specific context) // pf2eContext.type can be: "skill-check", "attack-roll", "damage-roll", // "saving-throw", "perception-check", "flat-check", "initiative", etc. if (pf2eContext.type) { parts.push(`made a ${pf2eContext.type.replace(/-/g, " ")}`); } else if (flavor) { parts.push(`rolled ${flavor.replace(/<[^>]*>/g, "")}`); // strip HTML tags } else { parts.push("rolled"); } // What action/item triggered the roll (e.g. "Longsword", "Perception", "Fireball") const origin = pf2eFlags.origin; if (origin?.type && origin?.sourceId) { parts.push(`using ${origin.type}`); } // The strike info if it's an attack if (pf2eFlags.strike) { const strike = pf2eFlags.strike; if (strike.name) parts.push(`(${strike.name})`); } // Outcome for checks (PF2E stores degree of success) // pf2eContext.outcome can be: "criticalSuccess", "success", "failure", "criticalFailure" if (pf2eContext.outcome) { const outcomeMap = { criticalSuccess: "Critical Success", success: "Success", failure: "Failure", criticalFailure: "Critical Failure" }; parts.push(`— ${outcomeMap[pf2eContext.outcome] || pf2eContext.outcome}`); } // The actual roll numbers const rollDetails = chatData.rolls.map(r => { let detail = `${r.formula} = ${r.total}`; // For damage rolls, try to extract damage types from the terms if (pf2eContext.type === "damage-roll" && r.options?.damage?.categories) { // Damage categories might be available // TODO: add in damage types } return detail; }).join("; "); parts.push(`[${rollDetails}]`); // DC if present (PF2E often includes the target DC) if (pf2eContext.dc !== undefined && pf2eContext.dc !== null) { const dc = typeof pf2eContext.dc === "object" ? pf2eContext.dc.value : pf2eContext.dc; if (dc) parts.push(`against DC ${dc}`); } // Target info if (pf2eContext.target) { const targetName = pf2eContext.target?.token?.name || pf2eContext.target?.actor?.name || null; if (targetName) parts.push(`targeting ${targetName}`); } parts.push('. ') // Traits (e.g. "fire", "mental", "incapacitation") if (pf2eContext.traits?.length > 0) { parts.push(`Traits: ${pf2eContext.traits.join(", ")}.`); } // Modifiers/notes from the flavor text (often contains the breakdown) // The flavor field in PF2E often has rich HTML with modifier breakdowns // We'll grab a clean text version as supplemental info if (flavor) { parts.push('Additional traits: ') const cleanFlavor = flavor.replace(/<[^>]*>/g, " ").replace(/\s{2,}/g, ', ').trim(); if (cleanFlavor){ parts.push(cleanFlavor); } } const rollString = `[ROLL] ${parts.join(" ")}`; console.log(chatData); console.log(rollString); sendToClaude(rollString, chatData.whisper, chatData.blind) } else { sendToClaude(chatData.content, chatData.whisper, chatData.blind) } }) const sendToClaude = async (content, whisper = [], blind = true) => { const response = await fetch('https://ai-dm-api.artisan.al/prompt', { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: content }) }); const data = await response.json(); await ChatMessage.create({ content: formatResponse(data.result), speaker: { alias: 'AI DM'}, style: CONST.CHAT_MESSAGE_STYLES.IC, whisper:whisper, blind:blind }); } Hooks.once("ready", () => { console.log('Claude listening...'); });