You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

157 lines
4.9 KiB

function formatResponse(text) {
return text
// Escape HTML entities first
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
// Bold: **text**
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
// Italic: *text*
.replace(/\*(.+?)\*/g, "<em>$1</em>")
// Dice notation: 🎲 lines get highlighted
.replace(/^(🎲.*)$/gm, '<span style="color: #c9a33e; font-weight: bold;">$1</span>')
// Line breaks
.replace(/\n\n/g, "</p><p>")
.replace(/\n/g, "<br>")
// Wrap in paragraph
.replace(/^(.*)$/, "<p>$1</p>");
}
Hooks.on("createChatMessage", async (chatData, options, userId) => {
if (chatData.speaker?.alias === 'AI DM') return
if(chatData.isRoll){
console.log(chatData);
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)
}
})
const sendToClaude = async (content, whisper = [], blind = false) => {
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.on("chatMessage", async (chatLog, message, chatData) => {
const formattedContent = chatData.speaker.actor ? `${chatData.speaker.alias} says, "${message}"` : message
const response = await fetch('https://ai-dm-api.artisan.al/prompt', {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: formattedContent })
});
const data = await response.json();
await ChatMessage.create({
content: formatResponse(data.result),
speaker: { alias: 'AI DM'},
style: CONST.CHAT_MESSAGE_STYLES.IC
});
})
Hooks.once("ready", () => {
console.log('Claude listening...');
});