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.
155 lines
4.8 KiB
155 lines
4.8 KiB
function formatResponse(text) {
|
|
return text
|
|
// Escape HTML entities first
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
// 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)
|
|
}
|
|
})
|
|
|
|
const sendToClaude = async (content) => {
|
|
|
|
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
|
|
});
|
|
|
|
}
|
|
|
|
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...');
|
|
});
|