import { i18n } from "./util.js";
import { Avatar, ActorAvatar, TokenAvatar, CombatantAvatar } from "./consts.js";
const rgb2hex = (rgb) =>
`#${rgb
.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
.slice(1)
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
.join("")}`;
// second return value is whether the first value can be styled as pf2e-icon
function getActionGlyph(actionCost) {
if (actionCost === "1 to 3") return ["1 / 2 / 3", true];
if (actionCost === "1 or 2") return ["1 / 2", true];
if (actionCost === "2 or 3") return ["2 / 3", true];
if (actionCost.type === "action") {
return [actionCost.value, true];
} else if (actionCost.type === "reaction") return ["R", true];
else if (actionCost.type === "free") return ["F", true];
else if (actionCost.length === 1) return [actionCost, true];
else return [actionCost, false];
}
// Chat cards
Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
const isNarratorToolsMessage = chatMessage.flags["narrator-tools"];
const isMLDRoundMarker = chatMessage.flags["monks-little-details"]?.roundmarker;
const isMCDRoundMarker = chatMessage.flags["monks-combat-details"]?.roundmarker;
const isRoundMarker = isMLDRoundMarker || isMCDRoundMarker;
if (isNarratorToolsMessage || isRoundMarker) {
return;
}
if (game.settings.get("pf2e-dorako-ux", "moving.restructure-card-info")) {
let uuid = chatMessage?.flags?.pf2e?.origin?.uuid;
if (uuid) {
try {
let origin = fromUuidSync(uuid);
let actionCost = origin?.actionCost;
if (actionCost) injectActionCost(html, actionCost);
if (origin?.type === "spell") injectSpellInfo(html, origin);
} catch (error) {
// An error is thrown if the UUID is a reference to something that is not loaded, like an actor in a compendium.
}
}
}
injectSenderWrapper(html, messageData);
injectMessageTag(html, messageData);
injectWhisperParticipants(html, messageData);
injectAuthorName(html, messageData);
if (
(game.settings.get("pf2e-dorako-ux", "avatar.hide-when-token-hidden") &&
chatMessage.getFlag("pf2e-dorako-ux", "wasTokenHidden")) ||
(game.settings.get("pf2e-dorako-ux", "avatar.hide-gm-avatar-when-secret") && !chatMessage.isContentVisible)
) {
} else {
injectAvatar(html, getAvatar(chatMessage));
}
moveFlavorTextToContents(html);
});
// Is damage roll
Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
if (!game.settings.get("pf2e-dorako-ux", "hiding.remove-attack-info-from-damage-roll-messages")) return;
if (chatMessage?.isDamageRoll && chatMessage?.item?.type !== "spell") {
html[0].classList.add("dorako-damage-roll");
let flavor = html.find(".flavor-text");
flavor.each(function () {
$(this).contents().eq(1).wrap("");
});
}
});
// Is check roll
Hooks.on("renderChatMessage", (chatMessage, html, messageData) => {
if (!game.settings.get("pf2e-dorako-ux", "hiding.remove-attack-info-from-damage-roll-messages")) return;
if (chatMessage?.isCheckRoll) {
html.addClass("dorako-check-roll");
}
});
// "Be last" magic trick. Should ensure that any other modules that modify, say, who spoke the message, have done so before you add the flags.
Hooks.once("ready", () => {
Hooks.on("preCreateChatMessage", (message) => {
addAvatarsToFlags(message);
message.updateSource({
"flags.pf2e-dorako-ux.wasTokenHidden": message?.token?.hidden,
});
});
Hooks.on("updateChatMessage", (message) => {
addAvatarsToFlags(message);
});
Hooks.on("renderChatMessage", (app, html, data) => {
const isKoboldWorksTurnAnnouncerMessage = app.flags["koboldworks-turn-announcer"];
if (!isKoboldWorksTurnAnnouncerMessage) return;
const avatar = html.find(".portrait");
avatar.css("transform", `scale(${app.flags["pf2e-dorako-ux"]?.tokenAvatar.scale})`);
avatar.css("flex", `0px 0px var(--avatar-size)`);
avatar.css("height", `var(--avatar-size)`);
avatar.css("width", `var(--avatar-size)`);
});
});
function moveFlavorTextToContents(html) {
let flavor = html.find(".flavor-text")[0];
let contents = html.find(".message-content")[0];
if (flavor) contents.prepend(flavor);
}
function injectSenderWrapper(html, messageData) {
if (messageData.author === undefined) return;
var target = html.find(".message-sender")[0];
var wrapper = document.createElement("div");
wrapper.classList.add("sender-wrapper");
target.parentNode.insertBefore(wrapper, target);
wrapper.appendChild(target);
}
function injectAvatar(html, avatar) {
if (!avatar) return;
let messageHeader = html.find(".message-header")[0];
let portraitAndName = document.createElement("div");
portraitAndName.classList.add("portrait-and-name");
messageHeader.prepend(portraitAndName);
let wrapper = document.createElement("div");
wrapper.classList.add("portrait-wrapper");
let portrait = document.createElement("img");
portrait.classList.add("avatar");
portrait.classList.add("portrait");
wrapper.append(portrait);
let senderWrapper = html.find(".sender-wrapper")[0];
portraitAndName.append(senderWrapper);
portraitAndName.prepend(wrapper);
}
function injectActionCost(html, actionCost) {
if (!actionCost) return;
const [actionGlyph, shouldBeStyled] = getActionGlyph(actionCost);
if (!actionGlyph) return;
// console.log("Injecting actionGlyph %s", actionGlyph);
let messageHeader = html.find(".card-header")[0];
let actionGlyphText = document.createElement("h3");
html.find(".action-glyph")?.get(0).remove(); // Remove action-glyph added by system
if (shouldBeStyled) actionGlyphText.classList.add("pf2-icon");
actionGlyphText.textContent = actionGlyph;
messageHeader.append(actionGlyphText);
}
function localizeComponent(componentKey) {
if (componentKey === "focus") return i18n("PF2E.SpellComponentF");
if (componentKey === "material") return i18n("PF2E.SpellComponentM");
if (componentKey === "somatic") return i18n("PF2E.SpellComponentS");
if (componentKey === "verbal") return i18n("PF2E.SpellComponentV");
}
function spellComponentsToText(components) {
// console.log(components);
const asArray = Object.entries(components);
// console.log(asArray);
const filtered = asArray.filter(([key, value]) => value);
// console.log(filtered);
const localized = filtered.map(([key, value]) => localizeComponent(key));
// console.log(localized);
const combined = localized.join(", ");
return " " + combined.toLowerCase();
}
function injectSpellInfo(html, spell) {
if (!spell) return;
let messageHeader = html.find(".card-content")[0];
let spellInfo = document.createElement("div");
spellInfo.classList.add("spell-info");
// console.log(spell);
// Cast time + components
let time = spell?.system?.time?.value;
let components = spell?.system?.components;
let elem = document.createElement("p");
let castInfoLabel = document.createElement("strong");
castInfoLabel.textContent = i18n("PF2E.CastLabel") + " ";
let castTime = document.createElement("span");
const [actionCost, shouldBeGlyph] = getActionGlyph(time);
castTime.textContent = actionCost;
if (shouldBeGlyph) castTime.classList.add("pf2-icon");
let castComponents = document.createElement("span");
// castComponents.textContent = spellComponentsToText(components);
elem.append(castInfoLabel);
elem.append(castTime);
// elem.append(castComponents);
spellInfo.append(elem);
// Cost info (note: not cast time, material cost)
let cost = spell?.system?.cost?.value;
if (cost) {
let elem = document.createElement("p");
let label = document.createElement("strong");
label.textContent = i18n("PF2E.SpellCostLabel") + " ";
let value = document.createElement("span");
value.textContent = cost;
elem.append(label);
elem.append(value);
spellInfo.append(elem);
}
let secondarycasters = spell?.system?.secondarycasters?.value;
if (secondarycasters) {
let info = document.createElement("p");
let label = document.createElement("strong");
label.textContent = i18n("PF2E.SpellSecondaryCasters") + " ";
let value = document.createElement("span");
value.textContent = secondarycasters;
info.append(label);
info.append(value);
spellInfo.append(info);
}
let primarycheck = spell?.system?.primarycheck?.value;
if (primarycheck) {
let info = document.createElement("p");
let label = document.createElement("strong");
label.textContent = i18n("PF2E.SpellPrimaryCheckLabel") + " ";
let value = document.createElement("span");
value.textContent = primarycheck;
info.append(label);
info.append(value);
spellInfo.append(info);
}
let secondarycheck = spell?.system?.secondarycheck?.value;
if (secondarycheck) {
let info = document.createElement("p");
let label = document.createElement("strong");
label.textContent = i18n("PF2E.SpellSecondaryChecksLabel") + " ";
let value = document.createElement("span");
value.textContent = secondarycheck;
info.append(label);
info.append(value);
spellInfo.append(info);
}
// Target info
let target = spell?.system?.target?.value;
if (target) {
// console.log(target);
let targetInfo = document.createElement("p");
let targetInfoLabel = document.createElement("strong");
targetInfoLabel.textContent = i18n("PF2E.SpellTargetLabel") + " ";
let targetValue = document.createElement("span");
targetValue.textContent = target;
targetInfo.append(targetInfoLabel);
targetInfo.append(targetValue);
spellInfo.append(targetInfo);
}
// Range info
let range = spell?.system?.range?.value;
if (range) {
// console.log(range);
let rangeInfo = document.createElement("p");
let rangeInfoLabel = document.createElement("strong");
rangeInfoLabel.textContent = i18n("PF2E.SpellRangeLabel") + " ";
let rangeValue = document.createElement("span");
rangeValue.textContent = range;
rangeInfo.append(rangeInfoLabel);
rangeInfo.append(rangeValue);
spellInfo.append(rangeInfo);
}
// Area info
let area = spell?.system?.area?.value;
if (area) {
// console.log(area);
let areaInfo = document.createElement("p");
let areaInfoLabel = document.createElement("strong");
areaInfoLabel.textContent = i18n("PF2E.AreaLabel") + " ";
let areaType = spell?.system?.area?.type;
let areaTypeLabel = areaType
? i18n("PF2E.AreaType" + areaType.charAt(0).toUpperCase() + areaType.slice(1)).toLowerCase()
: "";
let areaValue = document.createElement("span");
areaValue.textContent = area + " " + i18n("PF2E.Foot").toLowerCase() + " " + areaTypeLabel;
areaInfo.append(areaInfoLabel);
areaInfo.append(areaValue);
spellInfo.append(areaInfo);
}
// Duration info
let duration = spell?.system?.duration?.value;
if (duration) {
// console.log(duration);
let durationInfo = document.createElement("p");
let durationInfoLabel = document.createElement("strong");
durationInfoLabel.textContent = i18n("PF2E.SpellDurationLabel") + " ";
let durationValue = document.createElement("span");
durationValue.textContent = duration;
durationInfo.append(durationInfoLabel);
durationInfo.append(durationValue);
spellInfo.append(durationInfo);
}
let hr = document.createElement("hr");
// Heightening info
let spellRightInfo = html.find(".card-header").find("h4")[0];
let originalText = spellRightInfo.textContent;
const [_, spellType, parsedLevel] = originalText.split(/(.*) (\d+)/);
const baseLevel = spell?.baseLevel;
const actualLevel = spell?.level;
if (baseLevel != parsedLevel) {
let heighteningInfo = document.createElement("h4");
let spellTypeSpan = document.createElement("span");
spellTypeSpan.textContent = spellType + " ";
let originalLevel = document.createElement("s");
originalLevel.textContent = baseLevel;
let heightenedLevel = document.createElement("span");
heightenedLevel.classList.add("heightened");
heightenedLevel.textContent = " " + parsedLevel;
heighteningInfo.append(spellTypeSpan);
heighteningInfo.append(originalLevel);
heighteningInfo.append(heightenedLevel);
spellRightInfo.parentNode.replaceChild(heighteningInfo, spellRightInfo);
}
// Footer
let footer = html.find(".card-footer")[0];
if (footer) footer.classList.add("dorako-display-none");
messageHeader.prepend(hr);
messageHeader.prepend(spellInfo);
}
function injectAuthorName(html, messageData) {
if (messageData.author === undefined) return;
if (game.settings.get("pf2e-dorako-ux", "other.enable-player-tags")) {
const messageSenderElem = html.find(".sender-wrapper");
const playerName = messageData.author.name;
const playerNameElem = document.createElement("span");
playerNameElem.appendChild(document.createTextNode(playerName));
playerNameElem.classList.add("player-name");
playerNameElem.classList.add("header-meta");
if (playerName === messageData.alias) {
html.find(".message-sender").addClass("dorako-display-none");
}
messageSenderElem.append(playerNameElem);
}
}
function injectMessageTag(html, messageData) {
const setting = game.settings.get("pf2e-dorako-ux", "other.enable-rolltype-indication");
if (setting == false) {
return;
}
const messageMetadata = html.find(".message-metadata");
const rolltype = $("");
rolltype.addClass("rolltype");
rolltype.addClass("header-meta");
const whisperTargets = messageData.message.whisper;
const isBlind = messageData.message.blind;
if (isBlind) rolltype.addClass("blind");
const isWhisper = whisperTargets?.length > 0;
if (isWhisper && !isBlind) rolltype.addClass("whisper");
const isSelf = isWhisper && whisperTargets.length === 1 && whisperTargets[0] === messageData.message.user;
const isRoll = messageData.message.rolls !== undefined;
if (isBlind) {
rolltype.text(i18n("pf2e-dorako-ux.text.secret"));
messageMetadata.prepend(rolltype);
} else if (isSelf && whisperTargets[0]) {
rolltype.text(i18n("pf2e-dorako-ux.text.self-roll"));
messageMetadata.prepend(rolltype);
} else if (isRoll && isWhisper) {
rolltype.text(i18n("pf2e-dorako-ux.text.gm-only"));
messageMetadata.prepend(rolltype);
} else if (isWhisper) {
rolltype.text(i18n("pf2e-dorako-ux.text.whisper"));
messageMetadata.prepend(rolltype);
}
if (game.settings.get("pf2e-dorako-ux", "moving.animate-messages")) {
// Draw attention to direct whispers from players to GM
const isGmSpeaker = game.users.get(messageData.message.user)?.isGM;
const isGmTarget = game.users.get(whisperTargets?.[0])?.isGM;
if (!(isBlind || isSelf) && isWhisper && !isGmSpeaker && isGmTarget) {
html[0].classList.add("attention");
}
}
}
function injectWhisperParticipants(html, messageData) {
const alias = messageData.alias;
const author = messageData.author;
const whisperTargets = messageData.message.whisper;
const whisperTargetString = messageData.whisperTo;
const whisperTargetIds = messageData.message.whisper;
const isWhisper = whisperTargetIds?.length > 0 || false;
const isRoll = messageData.message.rolls !== undefined;
const isSelf =
(isWhisper && whisperTargets.length === 1 && whisperTargets[0] === messageData.message.user) ||
(isWhisper &&
whisperTargets.length === 2 &&
whisperTargets[0] === "null" &&
whisperTargets[1] === messageData.message.user);
const authorId = messageData.message.user;
const userId = game.user.id;
if (!isWhisper) return;
if (userId !== authorId && !whisperTargetIds.includes(userId)) return;
// remove the old whisper to content, if it exists
html.find(".whisper-to").detach();
// if this is a roll
if (isRoll || isSelf) return;
const messageHeader = html.find(".message-header");
const whisperParticipants = $("");
whisperParticipants.addClass("whisper-to");
const whisperFrom = $("");
const fromText = titleCase(i18n("pf2e-dorako-ux.text.from"));
whisperFrom.text(`${fromText}: ${alias}`);
whisperFrom.addClass("header-meta");
const whisperTo = $("");
const toText = titleCase(i18n("pf2e-dorako-ux.text.to")).toLowerCase();
whisperTo.text(`${toText}: ${whisperTargetString}`);
whisperTo.addClass("header-meta");
whisperParticipants.append(whisperFrom);
whisperParticipants.append(whisperTo);
messageHeader.append(whisperParticipants);
}
function addAvatarsToFlags(message) {
let combatantImg =
game.modules.get("combat-tracker-images")?.active && message.actor
? message.actor.getFlag("combat-tracker-images", "trackerImage")
: null;
let speaker = message.speaker;
const token = game.scenes.get(speaker.scene)?.tokens.get(speaker.token);
let tokenImg = token?.texture.src;
const actor = game.actors.get(speaker.actor);
let actorImg = actor?.img;
let userImg = message.user?.avatar;
let userAvatar = new Avatar(message.speaker.alias, userImg);
let combatantAvatar = combatantImg ? new CombatantAvatar(message.speaker.alias, combatantImg) : null;
let actorAvatar = actorImg ? new ActorAvatar(message.speaker.alias, actorImg) : null;
let tokenAvatar = null;
if (tokenImg) {
tokenAvatar = new TokenAvatar(message.speaker.alias, tokenImg, token.texture.scaleX, actor.size == "sm");
}
message.updateSource({
"flags.pf2e-dorako-ux.userAvatar": userAvatar,
"flags.pf2e-dorako-ux.combatantAvatar": combatantAvatar,
"flags.pf2e-dorako-ux.tokenAvatar": tokenAvatar,
"flags.pf2e-dorako-ux.actorAvatar": actorAvatar,
});
}
function getAvatar(message) {
const source = game.settings.get("pf2e-dorako-ux", "avatar.source");
if (source == "none") {
return null;
}
let combatantAvatar = message.getFlag("pf2e-dorako-ux", "combatantAvatar");
let tokenAvatar = message.getFlag("pf2e-dorako-ux", "tokenAvatar");
let actorAvatar = message.getFlag("pf2e-dorako-ux", "actorAvatar");
let userAvatar = game.settings.get("pf2e-dorako-ux", "avatar.use-user-avatar")
? message.getFlag("pf2e-dorako-ux", "userAvatar")
: null;
if (combatantAvatar) return combatantAvatar;
if (
game.settings.get("pf2e-dorako-ux", "avatar.hide-when-token-hidden") &&
message.getFlag("pf2e-dorako-ux", "wasTokenHidden")
) {
return null;
}
return source == "token" ? tokenAvatar || actorAvatar || userAvatar : actorAvatar || tokenAvatar || userAvatar;
}
// Add avatar if message contains avatar data
Hooks.on("renderChatMessage", (message, b) => {
let avatar = getAvatar(message);
if (!avatar) return;
let html = b[0];
let avatarElem = html.getElementsByClassName("avatar")[0];
if (!avatarElem) return;
avatarElem.src = avatar.image;
if (avatar.type == "token") {
const smallScale = game.settings.get("pf2e-dorako-ux", "avatar.small-creature-token-avatar-size");
let smallCorrection = avatar.isSmall ? 1.25 * smallScale : 1;
avatarElem?.setAttribute("style", "transform: scale(" + Math.abs(avatar.scale) * smallCorrection + ")");
}
const portraitDegreeSetting = game.settings.get("pf2e-dorako-ux", "avatar.reacts-to-degree-of-success");
if (portraitDegreeSetting && message.isContentVisible) {
let outcome = message?.flags?.pf2e?.context?.outcome;
if (outcome === undefined) return;
if (outcome === "criticalFailure") {
let wrapper = html.getElementsByClassName("portrait-wrapper")[0];
wrapper?.setAttribute("style", "filter: saturate(0.2) drop-shadow(0px 0px 6px black)");
} else if (outcome === "criticalSuccess") {
let wrapper = html.getElementsByClassName("portrait-wrapper")[0];
wrapper?.setAttribute("style", "filter: drop-shadow(0px 0px 6px lightgreen)");
}
}
});
// Add .spell to spells
Hooks.on("renderChatMessage", (app, html, data) => {
const item = app?.item;
if (!item) return;
if (!item.constructor.name.includes("SpellPF2e")) return;
html[0].classList.add("spell");
});