From abea77906d0e942c1131943420e0eee09c490fd0 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnien Date: Wed, 21 Jan 2026 13:56:09 +0100 Subject: [PATCH] Add spells rolls and enhance CSS styling - Add spell roll functionality to character sheets - Enhance CSS and LESS styling for better visual presentation - Update character templates and models - Remove old backup files (roll-old.mjs, roll.mjs.backup) - Improve character combat and equipment templates - Update utility functions and actor documents Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- css/fvtt-prism-rpg.css | 103 ++ .../applications/sheets/character-sheet.mjs | 191 ++- module/documents/actor.mjs | 28 +- module/documents/roll-old.mjs | 1095 ----------------- module/documents/roll.mjs.backup | 1095 ----------------- module/models/character.mjs | 102 +- module/utils.mjs | 5 + prism-rpg.mjs | 6 +- styles/chat.less | 136 ++ templates/character-combat.hbs | 3 + templates/character-equipment.hbs | 2 +- templates/character-main.hbs | 26 +- templates/character-skills.hbs | 6 +- templates/character-spells.hbs | 2 +- templates/class.hbs | 2 +- 15 files changed, 519 insertions(+), 2283 deletions(-) delete mode 100644 module/documents/roll-old.mjs delete mode 100644 module/documents/roll.mjs.backup diff --git a/css/fvtt-prism-rpg.css b/css/fvtt-prism-rpg.css index 34c2800..110e845 100644 --- a/css/fvtt-prism-rpg.css +++ b/css/fvtt-prism-rpg.css @@ -4102,6 +4102,109 @@ i.prismrpg { font-family: var(--font-secondary); font-size: calc(var(--font-size-standard) * 1.2); } +.chat-log .message-content .prismrpg-item-chat-card { + font-family: var(--font-primary); + border-radius: 4px; + overflow: hidden; + background: linear-gradient(135deg, rgba(0, 0, 0, 0.05) 0%, rgba(0, 0, 0, 0.02) 100%); + border: 1px solid rgba(0, 0, 0, 0.2); + margin: 2px 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} +.chat-log .message-content .prismrpg-item-chat-card .item-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 8px; + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + border-bottom: 1px solid #444; +} +.chat-log .message-content .prismrpg-item-chat-card .item-header h3 { + margin: 0; + font-size: 0.95em; + font-weight: bold; + color: #d4af37; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); + font-family: var(--font-secondary); +} +.chat-log .message-content .prismrpg-item-chat-card .item-header .item-type { + padding: 1px 6px; + border-radius: 10px; + font-size: 0.65em; + font-weight: bold; + text-transform: uppercase; + background: rgba(255, 255, 255, 0.15); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.2); + letter-spacing: 0.3px; +} +.chat-log .message-content .prismrpg-item-chat-card .item-image { + display: flex; + justify-content: center; + align-items: center; + padding: 6px; + background: rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} +.chat-log .message-content .prismrpg-item-chat-card .item-image img { + max-width: 50px; + max-height: 50px; + border-radius: 3px; + border: 1px solid rgba(212, 175, 55, 0.3); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + transition: transform 0.2s ease; +} +.chat-log .message-content .prismrpg-item-chat-card .item-image img:hover { + transform: scale(1.05); +} +.chat-log .message-content .prismrpg-item-chat-card .item-description { + padding: 6px 8px; + color: #000; + font-size: 0.8em; + line-height: 1.3; + background: rgba(255, 255, 255, 0.8); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-style: italic; +} +.chat-log .message-content .prismrpg-item-chat-card .item-details { + padding: 6px 8px; + display: flex; + flex-direction: column; + gap: 3px; +} +.chat-log .message-content .prismrpg-item-chat-card .item-details .item-detail { + display: flex; + align-items: center; + padding: 2px 6px; + background: rgba(255, 255, 255, 0.03); + border-left: 2px solid rgba(212, 175, 55, 0.5); + border-radius: 2px; + font-size: 0.8em; +} +.chat-log .message-content .prismrpg-item-chat-card .item-details .item-detail strong { + color: #d4af37; + margin-right: 6px; + min-width: 90px; + font-weight: bold; +} +.chat-log .message-content .prismrpg-item-chat-card .item-details .item-detail:nth-child(even) { + background: rgba(0, 0, 0, 0.05); +} +.chat-log .message-content .prismrpg-item-chat-card.weapon-item .item-header { + background: linear-gradient(135deg, #c41e3a 0%, #8b0000 100%); +} +.chat-log .message-content .prismrpg-item-chat-card.armor-item .item-header { + background: linear-gradient(135deg, #4a5cf7 0%, #2c3e9e 100%); +} +.chat-log .message-content .prismrpg-item-chat-card.spell-item .item-header { + background: linear-gradient(135deg, #9b59b6 0%, #6c3483 100%); +} +.chat-log .message-content .prismrpg-item-chat-card.skill-item .item-header { + background: linear-gradient(135deg, #16a085 0%, #0e6655 100%); +} +.chat-log .message-content .prismrpg-item-chat-card.equipment-item .item-header { + background: linear-gradient(135deg, #f39c12 0%, #b8730f 100%); +} .application.dialog.prismrpg { color: var(--color-dark-1); } diff --git a/module/applications/sheets/character-sheet.mjs b/module/applications/sheets/character-sheet.mjs index 0992d44..caea6b1 100644 --- a/module/applications/sheets/character-sheet.mjs +++ b/module/applications/sheets/character-sheet.mjs @@ -18,10 +18,15 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { rollInitiative: PrismRPGCharacterSheet.#onRollInitiative, armorHitPointsPlus: PrismRPGCharacterSheet.#onArmorHitPointsPlus, armorHitPointsMinus: PrismRPGCharacterSheet.#onArmorHitPointsMinus, + armorPointsPlus: PrismRPGCharacterSheet.#onArmorPointsPlus, + armorPointsMinus: PrismRPGCharacterSheet.#onArmorPointsMinus, + actionPointsPlus: PrismRPGCharacterSheet.#onActionPointsPlus, + actionPointsMinus: PrismRPGCharacterSheet.#onActionPointsMinus, manaPointsPlus: PrismRPGCharacterSheet.#onManaPointsPlus, manaPointsMinus: PrismRPGCharacterSheet.#onManaPointsMinus, hpPlus: PrismRPGCharacterSheet.#onHpPlus, hpMinus: PrismRPGCharacterSheet.#onHpMinus, + postItemToChat: PrismRPGCharacterSheet.#onPostItemToChat, }, } @@ -158,43 +163,215 @@ export default class PrismRPGCharacterSheet extends PrismRPGActorSheet { await this.document.system.rollInitiative() } - static #onArmorHitPointsPlus(event, target) { + static async #onArmorHitPointsPlus(event, target) { let armorHP = this.actor.system.combat.armorHitPoints armorHP += 1 this.actor.update({ "system.combat.armorHitPoints": armorHP }) } - static #onArmorHitPointsMinus(event, target) { + static async #onArmorHitPointsMinus(event, target) { let armorHP = this.actor.system.combat.armorHitPoints armorHP -= 1 this.actor.update({ "system.combat.armorHitPoints": Math.max(armorHP, 0) }) } - static #onManaPointsPlus(event, target) { + static async #onManaPointsPlus(event, target) { let mana = this.actor.system.manaPoints.value mana += 1 this.actor.update({ "system.manaPoints.value": Math.min(mana, this.actor.system.manaPoints.max) }) } - static #onManaPointsMinus(event, target) { + static async #onManaPointsMinus(event, target) { let mana = this.actor.system.manaPoints.value mana -= 1 this.actor.update({ "system.manaPoints.value": Math.max(mana, 0) }) } - static #onHpPlus(event, target) { + static async #onArmorPointsPlus(event, target) { + let armor = this.actor.system.armorPoints.value + armor += 1 + this.actor.update({ "system.armorPoints.value": Math.min(armor, this.actor.system.armorPoints.max) }) + } + + static async #onArmorPointsMinus(event, target) { + let armor = this.actor.system.armorPoints.value + armor -= 1 + this.actor.update({ "system.armorPoints.value": Math.max(armor, 0) }) + } + + static async#onActionPointsPlus(event, target) { + let actionPoints = this.actor.system.actionPoints.value + actionPoints += 1 + this.actor.update({ "system.actionPoints.value": Math.min(actionPoints, this.actor.system.actionPoints.max) }) + } + + static async#onActionPointsMinus(event, target) { + let actionPoints = this.actor.system.actionPoints.value + actionPoints -= 1 + this.actor.update({ "system.actionPoints.value": Math.max(actionPoints, 0) }) + } + + static async#onHpPlus(event, target) { let hp = this.actor.system.hp.value hp += 1 this.actor.update({ "system.hp.value": Math.min(hp, this.actor.system.hp.max) }) } - static #onHpMinus(event, target) { + static async#onHpMinus(event, target) { let hp = this.actor.system.hp.value hp -= 1 this.actor.update({ "system.hp.value": Math.max(hp, 0) }) } - static #onCreateEquipment(event, target) { + static async #onCreateEquipment(event, target) { + } + + static async #onPostItemToChat(event, target) { + console.log("PRISM RPG | PostItemToChat action triggered", { event: event, target: target }) + + // Try to find the item element from the clicked target or its parents + let itemElement = null + + // First try with the target (the actual clicked element) + if (event.target) { + itemElement = event.target.closest('[data-item-id]') + } + + // If not found, try with currentTarget (the element with the action) + if (!itemElement && event.currentTarget) { + itemElement = event.currentTarget.closest('[data-item-id]') + } + + // If still not found, try with the target parameter + if (!itemElement && target) { + itemElement = target.closest('[data-item-id]') + } + + console.log("PRISM RPG | Found item element", { itemElement: itemElement }) + + if (!itemElement) { + console.warn("PRISM RPG | Could not find item element for posting to chat") + return + } + + const itemId = itemElement.dataset.itemId + + if (!itemId) { + console.warn("PRISM RPG | Item ID not found for posting to chat") + return + } + + const item = this.actor.items.get(itemId) + + if (!item) { + console.warn("PRISM RPG | Item not found for posting to chat", { itemId: itemId }) + return + } + + // Create a chat message with the item data + const speaker = ChatMessage.getSpeaker({ actor: this.actor }) + const content = await this.formatItemForChat(item) + + await ChatMessage.create({ + content: content, + speaker: speaker, + }) + } + + async formatItemForChat(item) { + // Format the item data for chat display + const itemTypeClass = `${item.type}-item` + let htmlContent = ` +
+
+

${item.name}

+ ${game.i18n.localize(`TYPES.Item.${item.type}`) || item.type} +
+ ` + + // Add item image if available + if (item.img && !item.img.includes('icons/svg/mystery-man.svg')) { + htmlContent += `
${item.name}
` + } + + // Add item description if available + if (item.system.description && item.system.description.trim() !== '') { + const enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(item.system.description, { async: true }) + htmlContent += `
${enrichedDescription}
` + } + + // Add specific item data based on item type + htmlContent += `
` + + switch (item.type) { + case 'weapon': + htmlContent += ` +
Type: ${item.system.weaponType || 'Unknown'}
+
Damage: ${item.system.damage || 'N/A'}
+
APC: ${item.system.apc || 0}
+ ` + if (item.system.damageType) { + const damageTypes = [] + if (item.system.damageType.piercing) damageTypes.push('Piercing') + if (item.system.damageType.bludgeoning) damageTypes.push('Bludgeoning') + if (item.system.damageType.slashing) damageTypes.push('Slashing') + if (damageTypes.length > 0) { + htmlContent += `
Damage Type: ${damageTypes.join('/')}
` + } + } + break + + case 'armor': + htmlContent += ` +
Armor Type: ${item.system.armorType || 'Unknown'}
+
Armor Points: ${item.system.armorPoints || 0}
+
APC: ${item.system.apc || 0}
+ ` + break + + case 'shield': + htmlContent += ` +
Shield Type: ${item.system.shieldType || 'Unknown'}
+
Armor Points: ${item.system.armorPoints || 0}
+
APC: ${item.system.apc || 0}
+ ` + break + + case 'skill': + htmlContent += ` +
Modifier: ${item.system.modifier || 0}
+
Core Skill: ${item.system.isCoreSkill ? 'Yes' : 'No'}
+ ` + break + + case 'spell': + htmlContent += ` +
Level: ${item.system.level || 'Unknown'}
+
Mana Cost: ${item.system.manaCost || 0}
+
Casting Time: ${item.system.castingTime || 'N/A'}
+ ` + break + + case 'miracle': + htmlContent += ` +
Prayer Time: ${item.system.prayerTime || 'N/A'}
+ ` + break + + case 'equipment': + htmlContent += ` +
Weight: ${item.system.weight || 'N/A'}
+ ` + break + + default: + // For other item types, just show basic info + htmlContent += `
Item Type: ${item.type}
` + } + + htmlContent += `
` + + return htmlContent } _onRender(context, options) { diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 56dadca..2af5daf 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -159,38 +159,14 @@ export default class PrismRPGActor extends Actor { case "weapon-damage-medium": case "weapon-attack": { let weapon = this.items.find((i) => i.type === "weapon" && i.id === rollKey) - let skill - let skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase() === weapon.name.toLowerCase()) - if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 1.0) - } else { - skills = this.items.filter((i) => i.type === "skill" && i.name.toLowerCase().replace(" skill", "") === weapon.name.toLowerCase()) - if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 1.0) - } else { - skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass === weapon.system.weaponClass) - if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 0.5) - } else { - skills = this.items.filter((i) => i.type === "skill" && i.system.weaponClass.includes(SYSTEM.WEAPON_CATEGORIES[weapon.system.weaponClass])) - if (skills.length > 0) { - skill = this.getBestWeaponClassSkill(skills, rollType, 0.25) - } else { - ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) - return - } - } - } - } - if (!weapon || !skill) { - console.error("Weapon or skill not found", weapon, skill) + if (!weapon) { + console.error("Weapon not found", weapon, skill) ui.notifications.warn(game.i18n.localize("PRISMRPG.Notifications.skillNotFound")) return } // Create a plain object for rollTarget to ensure weapon data is preserved rollTarget = { - ...skill.toObject(), weapon: weapon.toObject(), rollKey: rollKey, combat: foundry.utils.duplicate(this.system.combat), diff --git a/module/documents/roll-old.mjs b/module/documents/roll-old.mjs deleted file mode 100644 index caa2cc5..0000000 --- a/module/documents/roll-old.mjs +++ /dev/null @@ -1,1095 +0,0 @@ -import { SYSTEM } from "../config/system.mjs" -import PrismRPGUtils from "../utils.mjs" - -export default class PrismRPGRoll extends Roll { - /** - * The HTML template path used to render dice checks of this type - * @type {string} - */ - static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" - - get type() { - return this.options.type - } - - get titleFormula() { - return this.options.titleFormula - } - - get rollName() { - return this.options.rollName - } - - get target() { - return this.options.target - } - - get value() { - return this.options.value - } - - get treshold() { - return this.options.treshold - } - - get actorId() { - return this.options.actorId - } - - get actorName() { - return this.options.actorName - } - - get actorImage() { - return this.options.actorImage - } - - get modifier() { - return this.options.modifier - } - - get resultType() { - return this.options.resultType - } - - get isFailure() { - return this.resultType === "failure" - } - - get hasTarget() { - return this.options.hasTarget - } - - get targetName() { - return this.options.targetName - } - - get targetArmor() { - return this.options.targetArmor - } - - get targetMalus() { - return this.options.targetMalus - } - - get realDamage() { - return this.options.realDamage - } - - get rollTotal() { - return this.options.rollTotal - } - - get diceResults() { - return this.options.diceResults - } - - get rollTarget() { - return this.options.rollTarget - } - - get D30result() { - return this.options.D30result - } - - get badResult() { - return this.options.badResult - } - - get rollData() { - return this.options.rollData - } - - /** - * Prompt the user with a dialog to configure and execute a roll (D&D 5e style). - * - * @param {Object} options Configuration options for the roll. - * @param {string} options.rollType The type of roll being performed. - * @param {Object} options.rollTarget The target of the roll. - * @param {string} options.actorId The ID of the actor performing the roll. - * @param {string} options.actorName The name of the actor performing the roll. - * @param {string} options.actorImage The image of the actor performing the roll. - * @param {boolean} options.hasTarget Whether the roll has a target. - * @param {Object} options.data Additional data for the roll. - * - * @returns {Promise} The roll result or null if the dialog was cancelled. - */ - static async prompt(options = {}) { - // D&D 5e style: simple 1d20 + modifier - let dice = "1d20" - let baseFormula = "1d20" - let hasModifier = true - let hasFavor = true // Allow advantage/disadvantage - let isDamageRoll = false - - // Determine roll specifics based on type - if (options.rollType === "characteristic") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.value - options.rollTarget.charModifier = options.rollTarget.value - - } else if (options.rollType === "challenge" || options.rollType === "save") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - // value already set in actor.mjs - - } else if (options.rollType === "skill") { - options.rollName = options.rollTarget.name - options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) - - } else if (options.rollType === "weapon-attack") { - options.rollName = options.rollTarget.name - if (options.rollTarget.weapon.system.weaponType === "melee") { - options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.attackModifier - } else { - options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier - } - - } else if (options.rollType === "weapon-defense") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus - options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier - - } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier - - } else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier - - } else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") { - options.rollName = options.rollTarget.name - if (options.rollType === "monster-attack") { - options.rollTarget.value = options.rollTarget.attackModifier - } else { - options.rollTarget.value = options.rollTarget.defenseModifier - } - options.rollTarget.charModifier = 0 - - } else if (options.rollType === "monster-skill") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - // value already set - - } else if (options.rollType.includes("weapon-damage")) { - isDamageRoll = true - hasFavor = false - options.rollName = options.rollTarget.name - let damageBonus = options.rollTarget.combat.damageModifier - options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus - options.rollTarget.charModifier = damageBonus - - if (options.rollType.includes("small")) { - dice = options.rollTarget.weapon.system.damage.damageS - } else { - dice = options.rollTarget.weapon.system.damage.damageM - } - dice = dice.replace("E", "").replace("e", "") - baseFormula = dice - - } else if (options.rollType.includes("monster-damage")) { - isDamageRoll = true - hasFavor = false - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.damageModifier - options.rollTarget.charModifier = 0 - dice = options.rollTarget.damageDice.replace("E", "").replace("e", "") - baseFormula = dice - - } else if (options.rollType === "granted") { - hasModifier = false - hasFavor = false - options.rollName = `Granted ${options.rollTarget.rollKey}` - dice = options.rollTarget.formula - baseFormula = options.rollTarget.formula - } - - // Setup roll modes - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) - - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - // D&D 5e style: simple modifiers - const choiceModifier = SYSTEM.CHOICE_MODIFIERS - const choiceFavor = SYSTEM.FAVOR_CHOICES - - let dialogContext = { - rollType: options.rollType, - rollTarget: options.rollTarget, - rollName: options.rollName, - actorName: options.actorName, - rollModes, - hasModifier, - hasFavor, - isDamageRoll, - baseValue: options.rollTarget.value || 0, - baseFormula, - dice, - fieldRollMode, - choiceModifier, - choiceFavor, - hasTarget: options.hasTarget, - modifier: "+0", - favor: "none", - targetName: "" - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext) - - let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } - const label = game.i18n.localize("PRISMRPG.Roll.roll") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Roll dialog" }, - classes: ["prismrpg"], - content, - position, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - console.log("Roll context", event, button, dialog) - let position = dialog.position - game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - actions: { - "selectGranted": (event, button, dialog) => { - hasGrantedDice = event.target.checked - }, - "selectBeyondSkill": (event, button, dialog) => { - beyondSkill = button.checked - }, - "selectPointBlank": (event, button, dialog) => { - pointBlank = button.checked - }, - "selectLetItFly": (event, button, dialog) => { - letItFly = button.checked - }, - "saveSpellCheck": (event, button, dialog) => { - saveSpell = button.checked - }, - "gotoToken": (event, button, dialog) => { - let tokenId = $(button).data("tokenId") - let token = canvas.tokens?.get(tokenId) - if (token) { - canvas.animatePan({ x: token.x, y: token.y, duration: 200 }) - canvas.tokens.releaseAll(); - token.control({ releaseOthers: true }); - } - } - }, - rejectClose: false // Click on Close button will not launch an error - }) - - // If the user cancels the dialog, exit - if (rollContext === null) return - console.log("rollContext", rollContext, hasGrantedDice) - rollContext.saveSpell = saveSpell // Update fucking flag - - let fullModifier = 0 - let titleFormula = "" - dice = rollContext.changeDice || dice - if (hasModifier) { - let bonus = Number(options.rollTarget.value) - fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus - fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0 - if (Number(rollContext.attackerAim) > 0) { - fullModifier += Number(rollContext.attackerAim) - } - - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} - 1` - } - if (hasStaticModifier) { - modifierFormula += ` + ${options.rollTarget.staticModifier}` - } - let sign = fullModifier < 0 ? "-" : "+" - if (hasExplode) { - titleFormula = `${dice}E ${sign} ${modifierFormula}` - } else { - titleFormula = `${dice} ${sign} ${modifierFormula}` - } - } else { - modifierFormula = "0" - fullModifier = 0 - baseFormula = `${dice}` - if (hasExplode) { - titleFormula = `${dice}E` - } else { - titleFormula = `${dice}` - } - } - - // Latest addition : favor choice at point blank range - if (pointBlank) { - rollContext.favor = "favor" - } - if (beyondSkill) { - rollContext.favor = "disfavor" - } - - // Specific pain case - if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") { - baseFormula = options.rollTarget.rollDice - titleFormula = `${dice}` - modifierFormula = "0" - fullModifier = 0 - } - - // Specific pain/poison/contagion case - if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) { - hasD30 = false - hasStaticModifier = true - modifierFormula = ` + ${Math.abs(fullModifier)}` - titleFormula = `${dice}E + ${Math.abs(fullModifier)}` - } - - if (letItFly) { - baseFormula = "1D20" - titleFormula = `1D20E` - modifierFormula = "0" - fullModifier = 0 - hasFavor = false - hasExplode = true - rollContext.favor = "none" - } - - maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains - - const rollData = { - type: options.rollType, - rollType: options.rollType, - target: options.rollTarget, - rollName: options.rollName, - actorId: options.actorId, - actorName: options.actorName, - actorImage: options.actorImage, - rollMode: rollContext.visibility, - hasTarget: options.hasTarget, - pointBlank, - beyondSkill, - letItFly, - hasGrantedDice, - titleFormula, - targetName, - ...rollContext, - } - - /** - * A hook event that fires before the roll is made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return - - let rollBase = new this(baseFormula, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - await rollModifier.evaluate() - await rollBase.evaluate() - - let rollFavor - let badResult - if (rollContext.favor === "favor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - console.log("Rolling with favor", rollFavor) - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) > Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } - - if (rollContext.favor === "disfavor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) < Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } - - if (hasD30) { - let rollD30 = await new Roll("1D30").evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollD30, game.user, true) - } - options.D30result = rollD30.total - } - - let rollTotal = 0 - let diceResults = [] - let resultType - let diceSum = 0 - - let singleDice = `1D${maxValue}` - for (let i = 0; i < rollBase.dice.length; i++) { - for (let j = 0; j < rollBase.dice[i].results.length; j++) { - let diceResult = rollBase.dice[i].results[j].result - diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) - diceSum += diceResult - if (hasMaxValue) { - while (diceResult === maxValue) { - let r = await new Roll(baseFormula).evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(r, game.user, true) - } - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - } - } - } - - if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") { - titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}` - let grantedRoll = new Roll(options.rollTarget.grantedDice) - await grantedRoll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(grantedRoll, game.user, true) - } - diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total }) - rollTotal += grantedRoll.total - } - - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal += Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal += diceSum + rollModifier.total - } - } else { - rollTotal += diceSum - } - - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = titleFormula - rollBase.options.D30result = options.D30result - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, rollBase) === false) return - - return rollBase - } - - /* ***********************************************************/ - static async promptInitiative(options = {}) { - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] - } else { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] - } - - let dialogContext = { - actorClass: options.actorClass, - initiativeDiceChoice: options.initiativeDiceChoice, - initiativeDice: "1D20", - maxInit: options.maxInit, - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-initiative-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.initiative") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Initiative Roll" }, - classes: ["prismrpg"], - content, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) - await initRoll.evaluate() - let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (options.combatId && options.combatantId) { - let combat = game.combats.get(options.combatId) - combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); - } - } - - /* ***********************************************************/ - static async promptCombatAction(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) - if (!combatant) { - console.error("No combatant found for this combat") - return - } - let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") - - let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } - - let dialogContext = { - progressionDiceId: "", - fieldRollMode, - rollModes, - currentAction, - ...options - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/combat-action-dialog.hbs", dialogContext) - - let buttons = [] - if (currentAction) { - if (currentAction.type === "weapon") { - buttons.push({ - action: "roll", - label: "Roll progression dice", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - return "rollProgressionDice" - }, - }) - } else if (currentAction.type === "spell" || currentAction.type === "miracle") { - let label = "" - if (currentAction.spellStatus === "castingTime") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Wait casting time" - } - if (currentAction.spellStatus === "toBeCasted") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Cast spell/miracle" - } - if (currentAction.spellStatus === "lethargy") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Roll lethargy dice" - } - buttons.push({ - action: "roll", - label: label, - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return "rollLethargyDice" - }, - }) - } - } else { - buttons.push({ - action: "roll", - label: "Select action", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ) - } - buttons.push({ - action: "cancel", - label: "Other action, not listed here", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return null; - } - }) - - let rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Combat Action Dialog" }, - id: "combat-action-dialog", - classes: ["prismrpg"], - position, - content, - buttons, - rejectClose: false // Click on Close button will not launch an error - }) - - console.log("RollContext", dialogContext, rollContext) - // If action is cancelled, exit - if (rollContext === null || rollContext === "cancel") { - await combatant.setFlag(SYSTEM.id, "currentAction", "") - let message = `${combatant.name} : Other action, progression reset` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - return - } - - // Setup the current action - if (!currentAction || currentAction === "") { - // Get the item from the returned selectedChoice value - let selectedChoice = rollContext.selectedChoice - let rangedMode - if (selectedChoice.match("simpleAim")) { - selectedChoice = selectedChoice.replace("simpleAim", "") - rangedMode = "simpleAim" - } - if (selectedChoice.match("carefulAim")) { - selectedChoice = selectedChoice.replace("carefulAim", "") - rangedMode = "carefulAim" - } - if (selectedChoice.match("focusedAim")) { - selectedChoice = selectedChoice.replace("focusedAim", "") - rangedMode = "focusedAim" - } - let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) - // Setup flag for combat action usage - let actionItem = foundry.utils.duplicate(selectedItem) - actionItem.progressionCount = 1 - actionItem.rangedMode = rangedMode - actionItem.castingTime = 1 - actionItem.spellStatus = "castingTime" - // Set the flag on the combatant - await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) - let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice - currentAction = actionItem - } - - if (currentAction) { - if (rollContext === "rollLethargyDice") { - if (currentAction.spellStatus === "castingTime") { - let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime - if (currentAction.castingTime < time) { - let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } else { - let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime = 1 - currentAction.spellStatus = "toBeCasted" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } - } - if (currentAction.spellStatus === "toBeCasted") { - combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) - if (currentAction.type === "spell") { - currentAction.spellStatus = "lethargy" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - // No lethargy for miracle - await combatant.setFlag(SYSTEM.id, "currentAction", "") - } - return - } - if (currentAction.spellStatus === "lethargy") { - // Roll lethargy dice - let dice = PrismRPGUtils.getLethargyDice(currentAction.system.level) - let roll = new Roll(dice) - await roll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(roll) - } - let max = roll.dice[0].faces - 1 - let toCompare = Math.min(currentAction.progressionCount, max) - if (roll.total <= toCompare) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - // Update the combatant progression count - await combatant.setFlag(SYSTEM.id, "currentAction", "") - // Display the action selection window again - combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - - if (rollContext === "rollProgressionDice") { - let formula = currentAction.system.combatProgressionDice - if (currentAction?.rangedMode) { - let toSplit = currentAction.system.speed[currentAction.rangedMode] - let split = toSplit.split("+") - currentAction.rangedLoad = Number(split[0]) || 0 - formula = split[1] - console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) - } - // Range weapon loading - if (!currentAction.weaponLoaded && currentAction.rangedLoad) { - if (currentAction.progressionCount <= currentAction.rangedLoad) { - let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - let message = `Ranged weapon ${currentAction.name} is loaded !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.weaponLoaded = true - currentAction.progressionCount = 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } - return - } - - // Melee mode - let isMonster = combatant.actor.type === "monster" - // Get the dice and roll it if - let roll = new Roll(formula) - await roll.evaluate() - - let max = roll.dice[0].faces - 1 - max = Math.min(currentAction.progressionCount, max) - let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (roll.total <= max) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - await combatant.setFlag(SYSTEM.id, "currentAction", "") - combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - } - - /* ***********************************************************/ - static async promptRangedDefense(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - let dialogContext = { - movementChoices: SYSTEM.MOVEMENT_CHOICES, - moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES, - sizeChoices: SYSTEM.SIZE_CHOICES, - rangeChoices: SYSTEM.RANGE_CHOICES, - attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, - movement: "none", - moveDirection: "none", - size: "+5", - range: "short", - attackerAim: "simple", - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/range-defense-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.rangeDefenseRoll") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Range Defense" }, - classes: ["prismrpg"], - content, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - // If the user cancels the dialog, exit - if (rollContext === null) return - - console.log("RollContext", rollContext) - // Add disfavor/favor option if point blank range - if (rollContext.range === "pointblank") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range) - rollContext.range = "0" - } - if (rollContext.range === "beyondskill") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range) - rollContext.range = "+11" - } - - // Build the final modifier - let fullModifier = Number(rollContext.moveDirection) + - Number(rollContext.size) + - Number(rollContext.range) + - Number(rollContext?.attackerAim || 0) - - let modifierFormula - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} -1` - } - - let rollData = { ...rollContext } - // Merge rollContext object into options object - options = { ...options, ...rollContext } - options.rollName = "Ranged Defense" - - const rollBase = new this(rollContext.movement, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - rollModifier.evaluate() - await rollBase.evaluate() - let rollD30 = await new Roll("1D30").evaluate() - options.D30result = rollD30.total - - let badResult = 0 - if (rollContext.movement.includes("kh")) { - rollData.favor = "favor" - badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20) - } - if (rollContext.movement.includes("kl")) { - rollData.favor = "disfavor" - badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1) - } - let dice = rollContext.movement - let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0]) - let rollTotal = -1 - let diceResults = [] - let resultType - - let diceResult = rollBase.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult }) - let diceSum = diceResult - while (diceResult === maxValue) { - let r = await new Roll(dice).evaluate() - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal = Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal = diceSum + rollModifier.total - } - } else { - rollTotal = diceSum - } - rollBase.options = { ...rollBase.options, ...options } - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = `1D20E + ${modifierFormula}` - rollBase.options.D30result = options.D30result - rollBase.options.rollName = "Ranged Defense" - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - - return rollBase - } - - /** - * Creates a title based on the given type. - * - * @param {string} type The type of the roll. - * @param {string} target The target of the roll. - * @returns {string} The generated title. - */ - static createTitle(type, target) { - switch (type) { - case "challenge": - return `${game.i18n.localize("PRISMRPG.Label.titleChallenge")}` - case "save": - return `${game.i18n.localize("PRISMRPG.Label.titleSave")}` - case "monster-skill": - case "skill": - return `${game.i18n.localize("PRISMRPG.Label.titleSkill")}` - case "weapon-attack": - return `${game.i18n.localize("PRISMRPG.Label.weapon-attack")}` - case "weapon-defense": - return `${game.i18n.localize("PRISMRPG.Label.weapon-defense")}` - case "weapon-damage-small": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-small")}` - case "weapon-damage-medium": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-medium")}` - case "spell": - case "spell-attack": - case "spell-power": - return `${game.i18n.localize("PRISMRPG.Label.spell")}` - case "miracle": - case "miracle-attack": - case "miracle-power": - return `${game.i18n.localize("PRISMRPG.Label.miracle")}` - default: - return game.i18n.localize("PRISMRPG.Label.titleStandard") - } - } - - /** @override */ - async render(chatOptions = {}) { - let chatData = await this._getChatCardData(chatOptions.isPrivate) - console.log("ChatData", chatData) - return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) - } - - /* - * Generates the data required for rendering a roll chat card. - */ - async _getChatCardData(isPrivate) { - const cardData = { - css: [SYSTEM.id, "dice-roll"], - data: this.data, - diceTotal: this.dice.reduce((t, d) => t + d.total, 0), - isGM: game.user.isGM, - formula: this.formula, - titleFormula: this.titleFormula, - rollName: this.rollName, - rollType: this.type, - rollTarget: this.rollTarget, - total: this.rollTotal, - isFailure: this.isFailure, - actorId: this.actorId, - diceResults: this.diceResults, - actingCharName: this.actorName, - actingCharImg: this.actorImage, - resultType: this.resultType, - hasTarget: this.hasTarget, - targetName: this.targetName, - targetArmor: this.targetArmor, - D30result: this.D30result, - badResult: this.badResult, - rollData: this.rollData, - isPrivate: isPrivate - } - cardData.cssClass = cardData.css.join(" ") - cardData.tooltip = isPrivate ? "" : await this.getTooltip() - return cardData - } - - /** - * Converts the roll result to a chat message. - * - * @param {Object} [messageData={}] Additional data to include in the message. - * @param {Object} options Options for message creation. - * @param {string} options.rollMode The mode of the roll (e.g., public, private). - * @param {boolean} [options.create=true] Whether to create the message. - * @returns {Promise} - A promise that resolves when the message is created. - */ - async toMessage(messageData = {}, { rollMode, create = true } = {}) { - super.toMessage( - { - isSave: this.isSave, - isChallenge: this.isChallenge, - isFailure: this.resultType === "failure", - rollType: this.type, - rollTarget: this.rollTarget, - actingCharName: this.actorName, - actingCharImg: this.actorImage, - hasTarget: this.hasTarget, - targetName: this.targetName, - targetArmor: this.targetArmor, - targetMalus: this.targetMalus, - realDamage: this.realDamage, - rollData: this.rollData, - ...messageData, - }, - { rollMode: rollMode }, - ) - } - -} diff --git a/module/documents/roll.mjs.backup b/module/documents/roll.mjs.backup deleted file mode 100644 index caa2cc5..0000000 --- a/module/documents/roll.mjs.backup +++ /dev/null @@ -1,1095 +0,0 @@ -import { SYSTEM } from "../config/system.mjs" -import PrismRPGUtils from "../utils.mjs" - -export default class PrismRPGRoll extends Roll { - /** - * The HTML template path used to render dice checks of this type - * @type {string} - */ - static CHAT_TEMPLATE = "systems/fvtt-prism-rpg/templates/chat-message.hbs" - - get type() { - return this.options.type - } - - get titleFormula() { - return this.options.titleFormula - } - - get rollName() { - return this.options.rollName - } - - get target() { - return this.options.target - } - - get value() { - return this.options.value - } - - get treshold() { - return this.options.treshold - } - - get actorId() { - return this.options.actorId - } - - get actorName() { - return this.options.actorName - } - - get actorImage() { - return this.options.actorImage - } - - get modifier() { - return this.options.modifier - } - - get resultType() { - return this.options.resultType - } - - get isFailure() { - return this.resultType === "failure" - } - - get hasTarget() { - return this.options.hasTarget - } - - get targetName() { - return this.options.targetName - } - - get targetArmor() { - return this.options.targetArmor - } - - get targetMalus() { - return this.options.targetMalus - } - - get realDamage() { - return this.options.realDamage - } - - get rollTotal() { - return this.options.rollTotal - } - - get diceResults() { - return this.options.diceResults - } - - get rollTarget() { - return this.options.rollTarget - } - - get D30result() { - return this.options.D30result - } - - get badResult() { - return this.options.badResult - } - - get rollData() { - return this.options.rollData - } - - /** - * Prompt the user with a dialog to configure and execute a roll (D&D 5e style). - * - * @param {Object} options Configuration options for the roll. - * @param {string} options.rollType The type of roll being performed. - * @param {Object} options.rollTarget The target of the roll. - * @param {string} options.actorId The ID of the actor performing the roll. - * @param {string} options.actorName The name of the actor performing the roll. - * @param {string} options.actorImage The image of the actor performing the roll. - * @param {boolean} options.hasTarget Whether the roll has a target. - * @param {Object} options.data Additional data for the roll. - * - * @returns {Promise} The roll result or null if the dialog was cancelled. - */ - static async prompt(options = {}) { - // D&D 5e style: simple 1d20 + modifier - let dice = "1d20" - let baseFormula = "1d20" - let hasModifier = true - let hasFavor = true // Allow advantage/disadvantage - let isDamageRoll = false - - // Determine roll specifics based on type - if (options.rollType === "characteristic") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.value - options.rollTarget.charModifier = options.rollTarget.value - - } else if (options.rollType === "challenge" || options.rollType === "save") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - // value already set in actor.mjs - - } else if (options.rollType === "skill") { - options.rollName = options.rollTarget.name - options.rollTarget.value = Math.floor(options.rollTarget.system.skillTotal / 10) - - } else if (options.rollType === "weapon-attack") { - options.rollName = options.rollTarget.name - if (options.rollTarget.weapon.system.weaponType === "melee") { - options.rollTarget.value = options.rollTarget.combat.attackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.attackModifier - } else { - options.rollTarget.value = options.rollTarget.combat.rangedAttackModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.attackBonus - options.rollTarget.charModifier = options.rollTarget.combat.rangedAttackModifier - } - - } else if (options.rollType === "weapon-defense") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.combat.defenseModifier + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.defenseBonus - options.rollTarget.charModifier = options.rollTarget.combat.defenseModifier - - } else if (options.rollType === "spell" || options.rollType === "spell-attack" || options.rollType === "spell-power") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.actorModifiers.levelSpellModifier + options.rollTarget.actorModifiers.intSpellModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.intSpellModifier - - } else if (options.rollType === "miracle" || options.rollType === "miracle-attack" || options.rollType === "miracle-power") { - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.actorModifiers.levelMiracleModifier + options.rollTarget.actorModifiers.chaMiracleModifier - options.rollTarget.charModifier = options.rollTarget.actorModifiers.chaMiracleModifier - - } else if (options.rollType === "monster-attack" || options.rollType === "monster-defense") { - options.rollName = options.rollTarget.name - if (options.rollType === "monster-attack") { - options.rollTarget.value = options.rollTarget.attackModifier - } else { - options.rollTarget.value = options.rollTarget.defenseModifier - } - options.rollTarget.charModifier = 0 - - } else if (options.rollType === "monster-skill") { - options.rollName = game.i18n.localize(`PRISMRPG.Label.${options.rollTarget.rollKey}`) - // value already set - - } else if (options.rollType.includes("weapon-damage")) { - isDamageRoll = true - hasFavor = false - options.rollName = options.rollTarget.name - let damageBonus = options.rollTarget.combat.damageModifier - options.rollTarget.value = damageBonus + options.rollTarget.weaponSkillModifier + options.rollTarget.weapon.system.bonuses.damageBonus - options.rollTarget.charModifier = damageBonus - - if (options.rollType.includes("small")) { - dice = options.rollTarget.weapon.system.damage.damageS - } else { - dice = options.rollTarget.weapon.system.damage.damageM - } - dice = dice.replace("E", "").replace("e", "") - baseFormula = dice - - } else if (options.rollType.includes("monster-damage")) { - isDamageRoll = true - hasFavor = false - options.rollName = options.rollTarget.name - options.rollTarget.value = options.rollTarget.damageModifier - options.rollTarget.charModifier = 0 - dice = options.rollTarget.damageDice.replace("E", "").replace("e", "") - baseFormula = dice - - } else if (options.rollType === "granted") { - hasModifier = false - hasFavor = false - options.rollName = `Granted ${options.rollTarget.rollKey}` - dice = options.rollTarget.formula - baseFormula = options.rollTarget.formula - } - - // Setup roll modes - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) - - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - // D&D 5e style: simple modifiers - const choiceModifier = SYSTEM.CHOICE_MODIFIERS - const choiceFavor = SYSTEM.FAVOR_CHOICES - - let dialogContext = { - rollType: options.rollType, - rollTarget: options.rollTarget, - rollName: options.rollName, - actorName: options.actorName, - rollModes, - hasModifier, - hasFavor, - isDamageRoll, - baseValue: options.rollTarget.value || 0, - baseFormula, - dice, - fieldRollMode, - choiceModifier, - choiceFavor, - hasTarget: options.hasTarget, - modifier: "+0", - favor: "none", - targetName: "" - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-dialog.hbs", dialogContext) - - let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 } - const label = game.i18n.localize("PRISMRPG.Roll.roll") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Roll dialog" }, - classes: ["prismrpg"], - content, - position, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - console.log("Roll context", event, button, dialog) - let position = dialog.position - game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - actions: { - "selectGranted": (event, button, dialog) => { - hasGrantedDice = event.target.checked - }, - "selectBeyondSkill": (event, button, dialog) => { - beyondSkill = button.checked - }, - "selectPointBlank": (event, button, dialog) => { - pointBlank = button.checked - }, - "selectLetItFly": (event, button, dialog) => { - letItFly = button.checked - }, - "saveSpellCheck": (event, button, dialog) => { - saveSpell = button.checked - }, - "gotoToken": (event, button, dialog) => { - let tokenId = $(button).data("tokenId") - let token = canvas.tokens?.get(tokenId) - if (token) { - canvas.animatePan({ x: token.x, y: token.y, duration: 200 }) - canvas.tokens.releaseAll(); - token.control({ releaseOthers: true }); - } - } - }, - rejectClose: false // Click on Close button will not launch an error - }) - - // If the user cancels the dialog, exit - if (rollContext === null) return - console.log("rollContext", rollContext, hasGrantedDice) - rollContext.saveSpell = saveSpell // Update fucking flag - - let fullModifier = 0 - let titleFormula = "" - dice = rollContext.changeDice || dice - if (hasModifier) { - let bonus = Number(options.rollTarget.value) - fullModifier = rollContext.modifier === "" ? 0 : parseInt(rollContext.modifier, 10) + bonus - fullModifier += (rollContext.saveSpell) ? options.rollTarget.actorModifiers.saveModifier : 0 - if (Number(rollContext.attackerAim) > 0) { - fullModifier += Number(rollContext.attackerAim) - } - - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} - 1` - } - if (hasStaticModifier) { - modifierFormula += ` + ${options.rollTarget.staticModifier}` - } - let sign = fullModifier < 0 ? "-" : "+" - if (hasExplode) { - titleFormula = `${dice}E ${sign} ${modifierFormula}` - } else { - titleFormula = `${dice} ${sign} ${modifierFormula}` - } - } else { - modifierFormula = "0" - fullModifier = 0 - baseFormula = `${dice}` - if (hasExplode) { - titleFormula = `${dice}E` - } else { - titleFormula = `${dice}` - } - } - - // Latest addition : favor choice at point blank range - if (pointBlank) { - rollContext.favor = "favor" - } - if (beyondSkill) { - rollContext.favor = "disfavor" - } - - // Specific pain case - if (options.rollType === "save" && options.rollTarget.rollKey === "pain" || options.rollTarget.rollKey === "paincourage") { - baseFormula = options.rollTarget.rollDice - titleFormula = `${dice}` - modifierFormula = "0" - fullModifier = 0 - } - - // Specific pain/poison/contagion case - if (options.rollType === "save" && (options.rollTarget.rollKey === "poison" || options.rollTarget.rollKey === "contagion")) { - hasD30 = false - hasStaticModifier = true - modifierFormula = ` + ${Math.abs(fullModifier)}` - titleFormula = `${dice}E + ${Math.abs(fullModifier)}` - } - - if (letItFly) { - baseFormula = "1D20" - titleFormula = `1D20E` - modifierFormula = "0" - fullModifier = 0 - hasFavor = false - hasExplode = true - rollContext.favor = "none" - } - - maxValue = Number(baseFormula.match(/\d+$/)[0]) // Update the max value agains - - const rollData = { - type: options.rollType, - rollType: options.rollType, - target: options.rollTarget, - rollName: options.rollName, - actorId: options.actorId, - actorName: options.actorName, - actorImage: options.actorImage, - rollMode: rollContext.visibility, - hasTarget: options.hasTarget, - pointBlank, - beyondSkill, - letItFly, - hasGrantedDice, - titleFormula, - targetName, - ...rollContext, - } - - /** - * A hook event that fires before the roll is made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - if (Hooks.call("fvtt-prism-rpg.preRoll", options, rollData) === false) return - - let rollBase = new this(baseFormula, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - await rollModifier.evaluate() - await rollBase.evaluate() - - let rollFavor - let badResult - if (rollContext.favor === "favor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - console.log("Rolling with favor", rollFavor) - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) > Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } - - if (rollContext.favor === "disfavor") { - rollFavor = new this(baseFormula, options.data, rollData) - await rollFavor.evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollFavor, game.user, true) - } - if (Number(rollFavor.result) < Number(rollBase.result)) { - badResult = rollBase.result - rollBase = rollFavor - } else { - badResult = rollFavor.result - } - rollFavor = null - } - - if (hasD30) { - let rollD30 = await new Roll("1D30").evaluate() - if (game?.dice3d) { - game.dice3d.showForRoll(rollD30, game.user, true) - } - options.D30result = rollD30.total - } - - let rollTotal = 0 - let diceResults = [] - let resultType - let diceSum = 0 - - let singleDice = `1D${maxValue}` - for (let i = 0; i < rollBase.dice.length; i++) { - for (let j = 0; j < rollBase.dice[i].results.length; j++) { - let diceResult = rollBase.dice[i].results[j].result - diceResults.push({ dice: `${singleDice.toUpperCase()}`, value: diceResult }) - diceSum += diceResult - if (hasMaxValue) { - while (diceResult === maxValue) { - let r = await new Roll(baseFormula).evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(r, game.user, true) - } - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${singleDice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - } - } - } - - if (hasGrantedDice && options.rollTarget.grantedDice && options.rollTarget.grantedDice !== "") { - titleFormula += ` + ${options.rollTarget.grantedDice.toUpperCase()}` - let grantedRoll = new Roll(options.rollTarget.grantedDice) - await grantedRoll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(grantedRoll, game.user, true) - } - diceResults.push({ dice: `${options.rollTarget.grantedDice.toUpperCase()}`, value: grantedRoll.total }) - rollTotal += grantedRoll.total - } - - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal += Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal += diceSum + rollModifier.total - } - } else { - rollTotal += diceSum - } - - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = titleFormula - rollBase.options.D30result = options.D30result - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - if (Hooks.call("fvtt-prism-rpg.Roll", options, rollData, rollBase) === false) return - - return rollBase - } - - /* ***********************************************************/ - static async promptInitiative(options = {}) { - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - if (SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass]) { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS[options.actorClass] - } else { - options.initiativeDiceChoice = SYSTEM.INITIATIVE_DICE_CHOICES_PER_CLASS["untrained"] - } - - let dialogContext = { - actorClass: options.actorClass, - initiativeDiceChoice: options.initiativeDiceChoice, - initiativeDice: "1D20", - maxInit: options.maxInit, - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/roll-initiative-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.initiative") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Initiative Roll" }, - classes: ["prismrpg"], - content, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) - await initRoll.evaluate() - let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (options.combatId && options.combatantId) { - let combat = game.combats.get(options.combatId) - combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); - } - } - - /* ***********************************************************/ - static async promptCombatAction(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); // v12 : Object.fromEntries(Object.entries(CONFIG.Dice.rollModes).map(([key, value]) => [key, game.i18n.localize(value)])) - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - let combatant = game.combats.get(options.combatId)?.combatants?.get(options.combatantId) - if (!combatant) { - console.error("No combatant found for this combat") - return - } - let currentAction = combatant.getFlag(SYSTEM.id, "currentAction") - - let position = game.user.getFlag(SYSTEM.id, "combat-action-dialog-pos") || { top: -1, left: -1 } - - let dialogContext = { - progressionDiceId: "", - fieldRollMode, - rollModes, - currentAction, - ...options - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/combat-action-dialog.hbs", dialogContext) - - let buttons = [] - if (currentAction) { - if (currentAction.type === "weapon") { - buttons.push({ - action: "roll", - label: "Roll progression dice", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - return "rollProgressionDice" - }, - }) - } else if (currentAction.type === "spell" || currentAction.type === "miracle") { - let label = "" - if (currentAction.spellStatus === "castingTime") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Wait casting time" - } - if (currentAction.spellStatus === "toBeCasted") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Cast spell/miracle" - } - if (currentAction.spellStatus === "lethargy") { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", pos) - label = "Roll lethargy dice" - } - buttons.push({ - action: "roll", - label: label, - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return "rollLethargyDice" - }, - }) - } - } else { - buttons.push({ - action: "roll", - label: "Select action", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ) - } - buttons.push({ - action: "cancel", - label: "Other action, not listed here", - callback: (event, button, dialog) => { - let pos = $('#combat-action-dialog').position() - game.user.setFlag(SYSTEM.id, "combat-action-dialog-pos", foundry.utils.duplicate(pos)) - return null; - } - }) - - let rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Combat Action Dialog" }, - id: "combat-action-dialog", - classes: ["prismrpg"], - position, - content, - buttons, - rejectClose: false // Click on Close button will not launch an error - }) - - console.log("RollContext", dialogContext, rollContext) - // If action is cancelled, exit - if (rollContext === null || rollContext === "cancel") { - await combatant.setFlag(SYSTEM.id, "currentAction", "") - let message = `${combatant.name} : Other action, progression reset` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - return - } - - // Setup the current action - if (!currentAction || currentAction === "") { - // Get the item from the returned selectedChoice value - let selectedChoice = rollContext.selectedChoice - let rangedMode - if (selectedChoice.match("simpleAim")) { - selectedChoice = selectedChoice.replace("simpleAim", "") - rangedMode = "simpleAim" - } - if (selectedChoice.match("carefulAim")) { - selectedChoice = selectedChoice.replace("carefulAim", "") - rangedMode = "carefulAim" - } - if (selectedChoice.match("focusedAim")) { - selectedChoice = selectedChoice.replace("focusedAim", "") - rangedMode = "focusedAim" - } - let selectedItem = combatant.actor.items.find(i => i.id === selectedChoice) - // Setup flag for combat action usage - let actionItem = foundry.utils.duplicate(selectedItem) - actionItem.progressionCount = 1 - actionItem.rangedMode = rangedMode - actionItem.castingTime = 1 - actionItem.spellStatus = "castingTime" - // Set the flag on the combatant - await combatant.setFlag(SYSTEM.id, "currentAction", actionItem) - let message = `${combatant.name} action : ${selectedItem.name}, start rolling progression dice or casting time` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - rollContext = (actionItem.type === "weapon") ? "rollProgressionDice" : "rollLethargyDice" // Set the roll context to rollProgressionDice - currentAction = actionItem - } - - if (currentAction) { - if (rollContext === "rollLethargyDice") { - if (currentAction.spellStatus === "castingTime") { - let time = currentAction.type === "spell" ? currentAction.system.castingTime : currentAction.system.prayerTime - if (currentAction.castingTime < time) { - let message = `Casting time : ${currentAction.name}, count : ${currentAction.castingTime}/${time}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } else { - let message = `Spell/Miracle ${currentAction.name} ready to be cast on next second !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.castingTime = 1 - currentAction.spellStatus = "toBeCasted" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - return - } - } - if (currentAction.spellStatus === "toBeCasted") { - combatant.actor.prepareRoll((currentAction.type === "spell") ? "spell-attack" : "miracle-attack", currentAction._id) - if (currentAction.type === "spell") { - currentAction.spellStatus = "lethargy" - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - // No lethargy for miracle - await combatant.setFlag(SYSTEM.id, "currentAction", "") - } - return - } - if (currentAction.spellStatus === "lethargy") { - // Roll lethargy dice - let dice = PrismRPGUtils.getLethargyDice(currentAction.system.level) - let roll = new Roll(dice) - await roll.evaluate() - if (game?.dice3d) { - await game.dice3d.showForRoll(roll) - } - let max = roll.dice[0].faces - 1 - let toCompare = Math.min(currentAction.progressionCount, max) - if (roll.total <= toCompare) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyOK", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - // Update the combatant progression count - await combatant.setFlag(SYSTEM.id, "currentAction", "") - // Display the action selection window again - combatant.actor.system.rollProgressionDice(options.combatId, options.combatantId) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageLethargyKO", { name: combatant.actor.name, spellName: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - - if (rollContext === "rollProgressionDice") { - let formula = currentAction.system.combatProgressionDice - if (currentAction?.rangedMode) { - let toSplit = currentAction.system.speed[currentAction.rangedMode] - let split = toSplit.split("+") - currentAction.rangedLoad = Number(split[0]) || 0 - formula = split[1] - console.log("Ranged Mode", currentAction.rangedMode, currentAction.rangedLoad, formula) - } - // Range weapon loading - if (!currentAction.weaponLoaded && currentAction.rangedLoad) { - if (currentAction.progressionCount <= currentAction.rangedLoad) { - let message = `Ranged weapon ${currentAction.name} is loading, loading count : ${currentAction.progressionCount}/${currentAction.rangedLoad}` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.progressionCount += 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } else { - let message = `Ranged weapon ${currentAction.name} is loaded !` - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - currentAction.weaponLoaded = true - currentAction.progressionCount = 1 - await combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - } - return - } - - // Melee mode - let isMonster = combatant.actor.type === "monster" - // Get the dice and roll it if - let roll = new Roll(formula) - await roll.evaluate() - - let max = roll.dice[0].faces - 1 - max = Math.min(currentAction.progressionCount, max) - let msg = await roll.toMessage({ flavor: `Progression Roll for ${currentAction.name}, progression count : ${currentAction.progressionCount}/${max}` }, { rollMode: rollContext.visibility }) - if (game?.dice3d) { - await game.dice3d.waitFor3DAnimationByMessageID(msg.id) - } - - if (roll.total <= max) { - // Notify that the player can act now with a chat message - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionOK", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - await combatant.setFlag(SYSTEM.id, "currentAction", "") - combatant.actor.prepareRoll(currentAction.type === "weapon" ? "weapon-attack" : "spell-attack", currentAction._id) - } else { - // Notify that the player cannot act now with a chat message - currentAction.progressionCount += 1 - combatant.setFlag(SYSTEM.id, "currentAction", foundry.utils.duplicate(currentAction)) - let message = game.i18n.format("PRISMRPG.Notifications.messageProgressionKO", { isMonster, name: combatant.actor.name, weapon: currentAction.name, roll: roll.total }) - ChatMessage.create({ content: message, speaker: ChatMessage.getSpeaker({ actor: combatant.actor }) }) - } - } - } - } - - /* ***********************************************************/ - static async promptRangedDefense(options = {}) { - - const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes); - const fieldRollMode = new foundry.data.fields.StringField({ - choices: rollModes, - blank: false, - default: "public", - }) - - let dialogContext = { - movementChoices: SYSTEM.MOVEMENT_CHOICES, - moveDirectionChoices: SYSTEM.MOVE_DIRECTION_CHOICES, - sizeChoices: SYSTEM.SIZE_CHOICES, - rangeChoices: SYSTEM.RANGE_CHOICES, - attackerAimChoices: SYSTEM.ATTACKER_AIM_CHOICES, - movement: "none", - moveDirection: "none", - size: "+5", - range: "short", - attackerAim: "simple", - fieldRollMode, - rollModes - } - - const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-prism-rpg/templates/range-defense-dialog.hbs", dialogContext) - - const label = game.i18n.localize("PRISMRPG.Label.rangeDefenseRoll") - const rollContext = await foundry.applications.api.DialogV2.wait({ - window: { title: "Range Defense" }, - classes: ["prismrpg"], - content, - buttons: [ - { - label: label, - callback: (event, button, dialog) => { - const output = Array.from(button.form.elements).reduce((obj, input) => { - if (input.name) obj[input.name] = input.value - return obj - }, {}) - return output - }, - }, - ], - rejectClose: false // Click on Close button will not launch an error - }) - - // If the user cancels the dialog, exit - if (rollContext === null) return - - console.log("RollContext", rollContext) - // Add disfavor/favor option if point blank range - if (rollContext.range === "pointblank") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kl" // Add the kl to the movement (disfavor for point blank range) - rollContext.range = "0" - } - if (rollContext.range === "beyondskill") { - rollContext.movement = rollContext.movement.replace("kh", "") - rollContext.movement = rollContext.movement.replace("kl", "") - rollContext.movement += "kh" // Add the kl to the movement (favor for point blank range) - rollContext.range = "+11" - } - - // Build the final modifier - let fullModifier = Number(rollContext.moveDirection) + - Number(rollContext.size) + - Number(rollContext.range) + - Number(rollContext?.attackerAim || 0) - - let modifierFormula - if (fullModifier === 0) { - modifierFormula = "0" - } else { - let modAbs = Math.abs(fullModifier) - modifierFormula = `D${modAbs + 1} -1` - } - - let rollData = { ...rollContext } - // Merge rollContext object into options object - options = { ...options, ...rollContext } - options.rollName = "Ranged Defense" - - const rollBase = new this(rollContext.movement, options.data, rollData) - const rollModifier = new Roll(modifierFormula, options.data, rollData) - rollModifier.evaluate() - await rollBase.evaluate() - let rollD30 = await new Roll("1D30").evaluate() - options.D30result = rollD30.total - - let badResult = 0 - if (rollContext.movement.includes("kh")) { - rollData.favor = "favor" - badResult = Math.min(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 20) - } - if (rollContext.movement.includes("kl")) { - rollData.favor = "disfavor" - badResult = Math.max(rollBase.terms[0].results[0].result, rollBase.terms[0].results[1]?.result || 1) - } - let dice = rollContext.movement - let maxValue = 20 // As per latest changes (was : Number(dice.match(/\d+$/)[0]) - let rollTotal = -1 - let diceResults = [] - let resultType - - let diceResult = rollBase.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}`, value: diceResult }) - let diceSum = diceResult - while (diceResult === maxValue) { - let r = await new Roll(dice).evaluate() - diceResult = r.dice[0].results[0].result - diceResults.push({ dice: `${dice.toUpperCase()}-1`, value: diceResult - 1 }) - diceSum += (diceResult - 1) - } - if (fullModifier !== 0) { - diceResults.push({ dice: `${rollModifier.formula.toUpperCase()}`, value: rollModifier.total }) - if (fullModifier < 0) { - rollTotal = Math.max(diceSum - rollModifier.total, 0) - } else { - rollTotal = diceSum + rollModifier.total - } - } else { - rollTotal = diceSum - } - rollBase.options = { ...rollBase.options, ...options } - rollBase.options.resultType = resultType - rollBase.options.rollTotal = rollTotal - rollBase.options.diceResults = diceResults - rollBase.options.rollTarget = options.rollTarget - rollBase.options.titleFormula = `1D20E + ${modifierFormula}` - rollBase.options.D30result = options.D30result - rollBase.options.rollName = "Ranged Defense" - rollBase.options.badResult = badResult - rollBase.options.rollData = foundry.utils.duplicate(rollData) - /** - * A hook event that fires after the roll has been made. - * @function - * @memberof hookEvents - * @param {Object} options Options for the roll. - * @param {Object} rollData All data related to the roll. - @param {PrismRPGRoll} roll The resulting roll. - * @returns {boolean} Explicitly return `false` to prevent roll to be made. - */ - - return rollBase - } - - /** - * Creates a title based on the given type. - * - * @param {string} type The type of the roll. - * @param {string} target The target of the roll. - * @returns {string} The generated title. - */ - static createTitle(type, target) { - switch (type) { - case "challenge": - return `${game.i18n.localize("PRISMRPG.Label.titleChallenge")}` - case "save": - return `${game.i18n.localize("PRISMRPG.Label.titleSave")}` - case "monster-skill": - case "skill": - return `${game.i18n.localize("PRISMRPG.Label.titleSkill")}` - case "weapon-attack": - return `${game.i18n.localize("PRISMRPG.Label.weapon-attack")}` - case "weapon-defense": - return `${game.i18n.localize("PRISMRPG.Label.weapon-defense")}` - case "weapon-damage-small": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-small")}` - case "weapon-damage-medium": - return `${game.i18n.localize("PRISMRPG.Label.weapon-damage-medium")}` - case "spell": - case "spell-attack": - case "spell-power": - return `${game.i18n.localize("PRISMRPG.Label.spell")}` - case "miracle": - case "miracle-attack": - case "miracle-power": - return `${game.i18n.localize("PRISMRPG.Label.miracle")}` - default: - return game.i18n.localize("PRISMRPG.Label.titleStandard") - } - } - - /** @override */ - async render(chatOptions = {}) { - let chatData = await this._getChatCardData(chatOptions.isPrivate) - console.log("ChatData", chatData) - return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) - } - - /* - * Generates the data required for rendering a roll chat card. - */ - async _getChatCardData(isPrivate) { - const cardData = { - css: [SYSTEM.id, "dice-roll"], - data: this.data, - diceTotal: this.dice.reduce((t, d) => t + d.total, 0), - isGM: game.user.isGM, - formula: this.formula, - titleFormula: this.titleFormula, - rollName: this.rollName, - rollType: this.type, - rollTarget: this.rollTarget, - total: this.rollTotal, - isFailure: this.isFailure, - actorId: this.actorId, - diceResults: this.diceResults, - actingCharName: this.actorName, - actingCharImg: this.actorImage, - resultType: this.resultType, - hasTarget: this.hasTarget, - targetName: this.targetName, - targetArmor: this.targetArmor, - D30result: this.D30result, - badResult: this.badResult, - rollData: this.rollData, - isPrivate: isPrivate - } - cardData.cssClass = cardData.css.join(" ") - cardData.tooltip = isPrivate ? "" : await this.getTooltip() - return cardData - } - - /** - * Converts the roll result to a chat message. - * - * @param {Object} [messageData={}] Additional data to include in the message. - * @param {Object} options Options for message creation. - * @param {string} options.rollMode The mode of the roll (e.g., public, private). - * @param {boolean} [options.create=true] Whether to create the message. - * @returns {Promise} - A promise that resolves when the message is created. - */ - async toMessage(messageData = {}, { rollMode, create = true } = {}) { - super.toMessage( - { - isSave: this.isSave, - isChallenge: this.isChallenge, - isFailure: this.resultType === "failure", - rollType: this.type, - rollTarget: this.rollTarget, - actingCharName: this.actorName, - actingCharImg: this.actorImage, - hasTarget: this.hasTarget, - targetName: this.targetName, - targetArmor: this.targetArmor, - targetMalus: this.targetMalus, - realDamage: this.realDamage, - rollData: this.rollData, - ...messageData, - }, - { rollMode: rollMode }, - ) - } - -} diff --git a/module/models/character.mjs b/module/models/character.mjs index 7c3b9ff..f93c65f 100644 --- a/module/models/character.mjs +++ b/module/models/character.mjs @@ -89,6 +89,11 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) }) + schema.actionPoints = new fields.SchemaField({ + value: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }), + max: new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) + }) + schema.biodata = new fields.SchemaField({ level: new fields.NumberField({ ...requiredInteger, initial: 1, min: 1 }), alignment: new fields.StringField({ required: true, nullable: false, initial: "" }), @@ -165,6 +170,21 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { prepareDerivedData() { super.prepareDerivedData(); + // Calculate action points max based on level + const level = this.biodata.level + let actionPointsMax = 4 + if (level >= 3 && level <= 5) { + actionPointsMax = 5 + } else if (level >= 6 && level <= 8) { + actionPointsMax = 6 + } else if (level >= 9 && level <= 10) { + actionPointsMax = 7 + } + // Set max action points (but don't override if already set to a higher value) + if (this.actionPoints.max < actionPointsMax) { + this.actionPoints.max = actionPointsMax + } + // Calculate sub-attributes from parent characteristics // Sub-attribute = lowest ability modifier between the two parent characteristics for (let subAttrKey in SYSTEM.SUB_ATTRIBUTES) { @@ -241,66 +261,46 @@ export default class PrismRPGCharacter extends foundry.abstract.TypeDataModel { await roll.toMessage({}, { rollMode: roll.options.rollMode }) } + /** + * Rolls initiative for the character: 1d20 + initiative modifier + * @param {string} combatId - Optional combat ID to update + * @param {string} combatantId - Optional combatant ID to update + * @returns {Promise} The initiative roll or null if cancelled + */ async rollInitiative(combatId = undefined, combatantId = undefined) { - const hasTarget = false - let actorClass = this.biodata.class; + // Get the initiative sub-attribute modifier + const initiativeModifier = this.subAttributes.initiative.value - let wisDef = SYSTEM.CHARACTERISTICS_TABLES.wis.find((c) => c.value === this.characteristics.wis.value) - let maxInit = Number(wisDef.init_cap) || 1000 + // Create the roll formula: 1d20 + initiative modifier + const formula = `1d20 + ${initiativeModifier}` - let roll = await PrismRPGRoll.promptInitiative({ - actorId: this.parent.id, - actorName: this.parent.name, - actorImage: this.parent.img, - combatId, - combatantId, - actorClass, - maxInit, + // Roll the initiative + let initRoll = new Roll(formula) + await initRoll.evaluate() + + // Create the chat message + let msg = await initRoll.toMessage({ + flavor: `${game.i18n.localize("PRISMRPG.Label.initiative")} - ${this.parent.name}`, + speaker: ChatMessage.getSpeaker({ actor: this.parent }) }) - if (!roll) return null - await roll.toMessage({}, { rollMode: roll.options.rollMode }) - } - - async rollProgressionDice(combatId, combatantId, rollProgressionCount) { - - // Get all weapons from the actor - let weapons = this.parent.items.filter(i => i.type === "weapon" && i.system.weaponType === "melee") - let weaponsChoices = weapons.map(w => { return { id: w.id, name: `${w.name} (${w.system.combatProgressionDice.toUpperCase()})`, combatProgressionDice: w.system.combatProgressionDice.toUpperCase() } }) - let rangeWeapons = this.parent.items.filter(i => i.type === "weapon" && i.system.weaponType === "ranged") - for (let w of rangeWeapons) { - weaponsChoices.push({ id: `${w.id}simpleAim`, name: `${w.name} (Simple Aim: ${w.system.speed.simpleAim.toUpperCase()})`, combatProgressionDice: w.system.speed.simpleAim.toUpperCase() }) - weaponsChoices.push({ id: `${w.id}carefulAim`, name: `${w.name} (Careful Aim: ${w.system.speed.carefulAim.toUpperCase()})`, combatProgressionDice: w.system.speed.carefulAim.toUpperCase() }) - weaponsChoices.push({ id: `${w.id}focusedAim`, name: `${w.name} (Focused Aim: ${w.system.speed.focusedAim.toUpperCase()})`, combatProgressionDice: w.system.speed.focusedAim.toUpperCase() }) + // Wait for 3D dice animation if enabled + if (game?.dice3d) { + await game.dice3d.waitFor3DAnimationByMessageID(msg.id) } - if (this.biodata.magicUser || this.biodata.clericUser) { - let spells = this.parent.items.filter(i => i.type === "spell" || i.type === "miracle") - for (let s of spells) { - let title = "" - let formula = "" - if (s.type === "spell") { - let dice = PrismRPGUtils.getLethargyDice(s.system.level) - title = `${s.name} (Casting time: ${s.system.castingTime}, Lethargy: ${dice})` - formula = `${s.system.castingTime}+${dice}` - } else { - title = `${s.name} (Prayer time: ${s.system.prayerTime})` - formula = `${s.system.prayerTime}` - } - weaponsChoices.push({ id: s.id, name: title, combatProgressionDice: formula }) + + // Update the combatant's initiative if in combat + if (combatId && combatantId) { + let combat = game.combats.get(combatId) + if (combat) { + await combat.updateEmbeddedDocuments("Combatant", [{ + _id: combatantId, + initiative: initRoll.total + }]) } } - let roll = await PrismRPGRoll.promptCombatAction({ - actorId: this.parent.id, - actorName: this.parent.name, - actorImage: this.parent.img, - weaponsChoices, - combatId, - combatantId, - rollProgressionCount, - type: "progression", - - }) + return initRoll } } diff --git a/module/utils.mjs b/module/utils.mjs index f42beee..ac12e3d 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -177,6 +177,11 @@ export default class PrismRPGUtils { return str ? str.toUpperCase() : ''; }) + Handlebars.registerHelper('replace', function (str, search, replacement) { + if (!str) return ''; + return str.replace(search, replacement); + }) + Handlebars.registerHelper('isEnabled', function (configKey) { return game.settings.get("bol", configKey); }) diff --git a/prism-rpg.mjs b/prism-rpg.mjs index 29d56cb..c464fe4 100644 --- a/prism-rpg.mjs +++ b/prism-rpg.mjs @@ -37,7 +37,11 @@ Hooks.once("init", function () { } CONFIG.ui.combat = PrismRPGCombatTracker - CONFIG.Combat.documentClass = PrismRPGCombat; + CONFIG.Combat.documentClass = PrismRPGCombat + CONFIG.Combat.initiative = { + formula: "1d20 + @subAttributes.initiative.value", + decimals: 2 + } CONFIG.Actor.documentClass = documents.PrismRPGActor CONFIG.Actor.dataModels = { diff --git a/styles/chat.less b/styles/chat.less index 60429d0..02d3e11 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -411,4 +411,140 @@ font-family: var(--font-secondary); font-size: calc(var(--font-size-standard) * 1.2); } + +// Item Chat Card Styles +.prismrpg-item-chat-card { + font-family: var(--font-primary); + border-radius: 4px; + overflow: hidden; + background: linear-gradient(135deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.02) 100%); + border: 1px solid rgba(0,0,0,0.2); + margin: 2px 0; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + + .item-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 8px; + background: linear-gradient(135deg, #2c2c2c 0%, #1a1a1a 100%); + border-bottom: 1px solid #444; + + h3 { + margin: 0; + font-size: 0.95em; + font-weight: bold; + color: #d4af37; + text-shadow: 1px 1px 2px rgba(0,0,0,0.5); + font-family: var(--font-secondary); + } + + .item-type { + padding: 1px 6px; + border-radius: 10px; + font-size: 0.65em; + font-weight: bold; + text-transform: uppercase; + background: rgba(255, 255, 255, 0.15); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.2); + letter-spacing: 0.3px; + } + } + + .item-image { + display: flex; + justify-content: center; + align-items: center; + padding: 6px; + background: rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + img { + max-width: 50px; + max-height: 50px; + border-radius: 3px; + border: 1px solid rgba(212, 175, 55, 0.3); + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.05); + } + } + } + + .item-description { + padding: 6px 8px; + color: #000; + font-size: 0.8em; + line-height: 1.3; + background: rgba(255, 255, 255, 0.8); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-style: italic; + } + + .item-details { + padding: 6px 8px; + display: flex; + flex-direction: column; + gap: 3px; + + .item-detail { + display: flex; + align-items: center; + padding: 2px 6px; + background: rgba(255, 255, 255, 0.03); + border-left: 2px solid rgba(212, 175, 55, 0.5); + border-radius: 2px; + font-size: 0.8em; + + strong { + color: #d4af37; + margin-right: 6px; + min-width: 90px; + font-weight: bold; + } + + &:nth-child(even) { + background: rgba(0, 0, 0, 0.05); + } + } + } + + // Special styling for weapon items + &.weapon-item { + .item-header { + background: linear-gradient(135deg, #c41e3a 0%, #8b0000 100%); + } + } + + // Special styling for armor items + &.armor-item { + .item-header { + background: linear-gradient(135deg, #4a5cf7 0%, #2c3e9e 100%); + } + } + + // Special styling for spell items + &.spell-item { + .item-header { + background: linear-gradient(135deg, #9b59b6 0%, #6c3483 100%); + } + } + + // Special styling for skill items + &.skill-item { + .item-header { + background: linear-gradient(135deg, #16a085 0%, #0e6655 100%); + } + } + + // Special styling for equipment items + &.equipment-item { + .item-header { + background: linear-gradient(135deg, #f39c12 0%, #b8730f 100%); + } + } +} } diff --git a/templates/character-combat.hbs b/templates/character-combat.hbs index 5dd8e57..eaf4809 100644 --- a/templates/character-combat.hbs +++ b/templates/character-combat.hbs @@ -21,6 +21,7 @@ class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" + data-action="postItemToChat" /> {{/if}}
@@ -88,6 +89,7 @@ class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" + data-action="postItemToChat" />
{{item.name}} @@ -137,6 +139,7 @@ class="item-img" src="{{item.img}}" data-tooltip="{{item.name}}" + data-action="postItemToChat" />
{{item.name}} diff --git a/templates/character-equipment.hbs b/templates/character-equipment.hbs index f869dd6..20cf09b 100644 --- a/templates/character-equipment.hbs +++ b/templates/character-equipment.hbs @@ -17,7 +17,7 @@
{{#each equipments as |item|}}
- +
{{item.name}}
diff --git a/templates/character-main.hbs b/templates/character-main.hbs index 4d84bf0..0736897 100644 --- a/templates/character-main.hbs +++ b/templates/character-main.hbs @@ -86,6 +86,7 @@
ARMOR
+
{{formInput systemFields.armorPoints.fields.value @@ -93,6 +94,7 @@ disabled=isPlayMode }}
+
/
{{formInput @@ -102,6 +104,26 @@ }}
+
+
AP
+ +
+ {{formInput + systemFields.actionPoints.fields.value + value=system.actionPoints.value + disabled=isPlayMode + }} +
+ +
/
+
+ {{formInput + systemFields.actionPoints.fields.max + value=system.actionPoints.max + disabled=isPlayMode + }} +
+
@@ -270,7 +292,7 @@