From bf9ad37d245b91d69845282d5fa1f1bd72a39a18 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Tue, 10 Mar 2026 17:56:13 +0100 Subject: [PATCH] Add mana+AP reset buttons and conditions --- css/fvtt-prism-rpg.css | 180 +++++++++++++++++++ lang/en.json | 83 +++++++++ module/applications/_module.mjs | 1 + module/applications/sheets/ability-sheet.mjs | 50 ++++++ module/config/effects.mjs | 89 +++++++++ module/documents/actor.mjs | 3 +- module/models/_module.mjs | 1 + module/models/ability.mjs | 14 ++ module/utils.mjs | 1 + prism-rpg.mjs | 75 ++++++++ styles/ability.less | 26 +++ styles/chat.less | 87 +++++++++ styles/fvtt-prism-rpg.less | 1 + system.json | 1 + templates/ability.hbs | 38 ++++ templates/chat-new-round.hbs | 20 +++ 16 files changed, 668 insertions(+), 2 deletions(-) create mode 100644 module/applications/sheets/ability-sheet.mjs create mode 100644 module/config/effects.mjs create mode 100644 module/models/ability.mjs create mode 100644 styles/ability.less create mode 100644 templates/ability.hbs create mode 100644 templates/chat-new-round.hbs diff --git a/css/fvtt-prism-rpg.css b/css/fvtt-prism-rpg.css index 9436c5b..4993faf 100644 --- a/css/fvtt-prism-rpg.css +++ b/css/fvtt-prism-rpg.css @@ -2163,6 +2163,116 @@ i.prismrpg { .prismrpg .racial-ability-content input[type="checkbox"]:checked::after { color: rgba(0, 0, 0, 0.1); } +.prismrpg .ability-content { + font-family: var(--font-primary); + font-size: calc(var(--font-size-standard) * 1); + color: var(--color-dark-1); + background-image: var(--background-image-base); + background-repeat: no-repeat; + background-size: 100% 100%; + overflow: auto; +} +.prismrpg .ability-content nav.tabs [data-tab] { + color: #636060; +} +.prismrpg .ability-content nav.tabs [data-tab].active { + color: #252424; +} +.prismrpg .ability-content input:disabled, +.prismrpg .ability-content select:disabled { + background-color: rgba(0, 0, 0, 0.2); + border-color: transparent; + color: var(--color-dark-3); +} +.prismrpg .ability-content input, +.prismrpg .ability-content select { + height: 1.5rem; + background-color: rgba(0, 0, 0, 0.1); + border-color: var(--color-dark-6); + color: var(--color-dark-2); +} +.prismrpg .ability-content input[name="name"] { + height: 2.5rem; + margin-right: 4px; + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + border: none; +} +.prismrpg .ability-content fieldset { + margin-bottom: 4px; + border-radius: 4px; +} +.prismrpg .ability-content .form-fields input, +.prismrpg .ability-content .form-fields select { + text-align: center; + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .ability-content .form-fields select { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1); +} +.prismrpg .ability-content legend { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1.2); + font-weight: bold; + letter-spacing: 1px; +} +.prismrpg .ability-content .form-fields { + padding-top: 4px; +} +.prismrpg .ability-content .form-group { + display: flex; + flex: 1; + flex-direction: row; +} +.prismrpg .ability-content .form-group label { + align-content: center; + min-width: 10rem; + max-width: 10rem; +} +.prismrpg .ability-content .form-group select, +.prismrpg .ability-content .form-group input { + text-align: left; + min-width: 12rem; + max-width: 12rem; +} +.prismrpg .ability-content .form-group input[type="checkbox"] { + min-width: 1.2rem; + max-width: 1.2rem; + margin-right: 0.5rem; +} +.prismrpg .ability-content label { + font-family: var(--font-secondary); + font-size: calc(var(--font-size-standard) * 1); + flex: 50%; +} +.prismrpg .ability-content .align-top { + align-self: flex-start; + padding: 0.1rem; + margin-right: 0.2rem; +} +.prismrpg .ability-content .shift-right { + margin-left: 2rem; +} +.prismrpg .ability-content .header { + display: flex; +} +.prismrpg .ability-content .header img { + width: 50px; + height: 50px; +} +.prismrpg .ability-content input[type="checkbox"] { + font-size: var(--font-size-14); + width: 20px; + padding-top: 0; +} +.prismrpg .ability-content input[type="checkbox"]:checked { + background-color: rgba(0, 0, 0, 0.1); +} +.prismrpg .ability-content input[type="checkbox"]:checked::after { + color: rgba(0, 0, 0, 0.1); +} .prismrpg .weapon-content { font-family: var(--font-primary); font-size: calc(var(--font-size-standard) * 1); @@ -4317,6 +4427,76 @@ i.prismrpg { .chat-log .message-content .prismrpg-item-chat-card.equipment-item .item-header { background: linear-gradient(135deg, #f39c12 0%, #b8730f 100%); } +.new-round-message .chat-title .new-round-label { + font-size: var(--font-size-11); + font-style: italic; + color: #8a7a5a; + margin-top: 1px; +} +.new-round-message .new-round-actors { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + padding: 6px 0 2px; +} +.new-round-message .new-round-actors .new-round-restore-btn { + display: flex; + align-items: center; + gap: 6px; + width: calc(50% - 3px); + padding: 4px 8px 4px 6px; + border: 1px solid #7a6a45; + border-radius: 4px; + background: linear-gradient(135deg, #f5e6c8 0%, #e8d5a0 100%); + cursor: pointer; + font-size: var(--font-size-13); + color: #3a2e1a; + overflow: hidden; +} +.new-round-message .new-round-actors .new-round-restore-btn span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} +.new-round-message .new-round-actors .new-round-restore-btn img { + flex-shrink: 0; + width: 24px; + height: 24px; + border: none; + border-radius: 3px; + object-fit: cover; +} +.new-round-message .new-round-actors .new-round-restore-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #fdf3dc 0%, #f0e0b0 100%); + border-color: #a08040; +} +.new-round-message .new-round-actors .new-round-restore-btn:disabled, +.new-round-message .new-round-actors .new-round-restore-btn.restored { + opacity: 0.45; + cursor: default; +} +.new-round-message .new-round-actors .new-round-all-btn { + width: 100%; + background: linear-gradient(135deg, #c8dff5 0%, #a0c0e8 100%); + border-color: #4a6a8a; + color: #1a2e3a; + font-weight: bold; +} +.new-round-message .new-round-actors .new-round-all-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #dcedfc 0%, #b0d0f0 100%); + border-color: #3a5a7a; +} +.palette.status-effects .status-separator { + grid-column: 1 / -1; + width: 100%; + height: 2px; + border: none; + border-top: 2px solid rgba(255, 255, 255, 0.35); + margin: 5px 0; + position: relative; +} .application.dialog.prismrpg { color: var(--color-dark-1); } diff --git a/lang/en.json b/lang/en.json index b438ce2..7cef299 100644 --- a/lang/en.json +++ b/lang/en.json @@ -382,6 +382,88 @@ } } }, + "Ability": { + "FIELDS": { + "description": { + "label": "Description" + } + } + }, + "Combat": { + "newRound": "New Round —", + "restoreAP": "Reset AP & +1 Mana", + "allPC": "All PC" + }, + "Status": { + "Aided": "Aided", + "Alert": "Alert", + "Alkalized": "Alkalized", + "Anchored": "Anchored", + "Banished": "Banished", + "Bestowed": "Bestowed", + "Blessed": "Blessed", + "Bleed": "Bleed", + "Blind": "Blind", + "Burning": "Burning", + "Chilled": "Chilled", + "Comatose": "Comatose", + "Compulsed": "Compulsed", + "Concealed": "Concealed", + "Corroded": "Corroded", + "Cursed": "Cursed", + "Dazed": "Dazed", + "Deaf": "Deaf", + "Diseased": "Diseased", + "Distracted": "Distracted", + "Enchanted": "Enchanted", + "Enhance": "Enhance", + "Exhaustion": "Exhaustion", + "Fatigue": "Fatigue", + "Frightened": "Frightened", + "Fury": "Fury", + "Haste": "Haste", + "Heroism": "Heroism", + "Horror": "Horror", + "Inspired": "Inspired", + "Invisible": "Invisible", + "Keen": "Keen", + "LifeDrain": "Life Drain", + "Locked": "Locked", + "Madness": "Madness", + "ManaDrain": "Mana Drain", + "Marked": "Marked", + "Mute": "Mute", + "Necrosis": "Necrosis (Elemental)", + "Numbed": "Numbed", + "Paralyzed": "Paralyzed", + "Petrified": "Petrified", + "Plagued": "Plagued", + "Poison": "Poison", + "Prepared": "Prepared", + "Prone": "Prone", + "Radiated": "Radiated (Elemental)", + "Rage": "Rage", + "Regeneration": "Regeneration", + "Reinforced": "Reinforced", + "Renewed": "Renewed", + "Saturated": "Saturated", + "Sealed": "Sealed", + "Seep": "Seep", + "Shattered": "Shattered", + "Shocked": "Shocked (Elemental)", + "Sightless": "Sightless", + "Silenced": "Silenced", + "Soundless": "Soundless", + "Staggered": "Staggered", + "Stunned": "Stunned", + "Supplied": "Supplied", + "Surged": "Surged", + "Taunt": "Taunt", + "Trance": "Trance", + "Unconscious": "Unconscious", + "Warded": "Warded", + "Wounded": "Wounded" + }, "Label": { "agility": "Dexterity", "gotoToken": "Go to token", @@ -1412,6 +1494,7 @@ "armor": "Armor", "equipment": "Equipment", "racial-ability": "Racial Ability", + "ability": "Ability", "miracle": "Miracle", "save": "Save", "shield": "Shield", diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 37a3916..8431b9a 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -3,6 +3,7 @@ export { default as PrismRPGMonsterSheet } from "./sheets/monster-sheet.mjs" export { default as PrismRPGWeaponSheet } from "./sheets/weapon-sheet.mjs" export { default as PrismRPGSkillSheet } from "./sheets/skill-sheet.mjs" export { default as PrismRPGRacialAbilitySheet } from "./sheets/racial-ability-sheet.mjs" +export { default as PrismRPGAbilitySheet } from "./sheets/ability-sheet.mjs" export { default as PrismRPGVulnerabilitySheet } from "./sheets/vulnerability-sheet.mjs" export { default as PrismRPGArmorSheet } from "./sheets/armor-sheet.mjs" export { default as PrismRPGSpellSheet } from "./sheets/spell-sheet.mjs" diff --git a/module/applications/sheets/ability-sheet.mjs b/module/applications/sheets/ability-sheet.mjs new file mode 100644 index 0000000..d2015cf --- /dev/null +++ b/module/applications/sheets/ability-sheet.mjs @@ -0,0 +1,50 @@ +import PrismRPGItemSheet from "./base-item-sheet.mjs" + +export default class PrismRPGAbilitySheet extends PrismRPGItemSheet { + /** @override */ + static DEFAULT_OPTIONS = { + classes: ["ability"], + position: { + width: 600, + }, + window: { + contentClasses: ["ability-content"], + }, + } + + /** @override */ + static PARTS = { + main: { + template: "systems/fvtt-prism-rpg/templates/ability.hbs", + }, + } + + /** @override */ + tabGroups = { + primary: "description", + } + + /** + * Prepare an array of form header tabs. + * @returns {Record>} + */ + #getTabs() { + const tabs = { + description: { id: "description", group: "primary", label: "PRISMRPG.Label.description" }, + effects: { id: "effects", group: "primary", label: "PRISMRPG.Label.effects" }, + } + for (const v of Object.values(tabs)) { + v.active = this.tabGroups[v.group] === v.id + v.cssClass = v.active ? "active" : "" + } + return tabs + } + + /** @override */ + async _prepareContext() { + const context = await super._prepareContext() + context.tabs = this.#getTabs() + return context + } + +} diff --git a/module/config/effects.mjs b/module/config/effects.mjs new file mode 100644 index 0000000..7d8e6c1 --- /dev/null +++ b/module/config/effects.mjs @@ -0,0 +1,89 @@ +/** + * Afflictions — negative status effects (Mundane & Magic) + * @type {StatusEffectConfig[]} + */ +export const AFFLICTIONS = [ + // ── Mundane ──────────────────────────────────────────────────────────────── + { id: "aff-alkalized", name: "PRISMRPG.Status.Alkalized", icon: "icons/svg/acid.svg", category: "affliction", typing: "mundane" }, + { id: "aff-bleed", name: "PRISMRPG.Status.Bleed", icon: "icons/svg/blood.svg", category: "affliction", typing: "mundane" }, + { id: "aff-blind", name: "PRISMRPG.Status.Blind", icon: "icons/svg/blind.svg", category: "affliction", typing: "mundane" }, + { id: "aff-deaf", name: "PRISMRPG.Status.Deaf", icon: "icons/svg/deaf.svg", category: "affliction", typing: "mundane" }, + { id: "aff-diseased", name: "PRISMRPG.Status.Diseased", icon: "icons/svg/biohazard.svg", category: "affliction", typing: "mundane" }, + { id: "aff-distracted", name: "PRISMRPG.Status.Distracted", icon: "icons/svg/daze.svg", category: "affliction", typing: "mundane" }, + { id: "aff-exhaustion", name: "PRISMRPG.Status.Exhaustion", icon: "icons/svg/sleep.svg", category: "affliction", typing: "mundane" }, + { id: "aff-frightened", name: "PRISMRPG.Status.Frightened", icon: "icons/svg/terror.svg", category: "affliction", typing: "mundane" }, + { id: "aff-marked", name: "PRISMRPG.Status.Marked", icon: "icons/svg/target.svg", category: "affliction", typing: "both" }, + { id: "aff-mute", name: "PRISMRPG.Status.Mute", icon: "icons/svg/silenced.svg", category: "affliction", typing: "mundane" }, + { id: "aff-paralyzed", name: "PRISMRPG.Status.Paralyzed", icon: "icons/svg/paralysis.svg", category: "affliction", typing: "mundane" }, + { id: "aff-petrified", name: "PRISMRPG.Status.Petrified", icon: "icons/svg/frozen.svg", category: "affliction", typing: "mundane" }, + { id: "aff-poison", name: "PRISMRPG.Status.Poison", icon: "icons/svg/poison.svg", category: "affliction", typing: "mundane" }, + { id: "aff-prone", name: "PRISMRPG.Status.Prone", icon: "icons/svg/falling.svg", category: "affliction", typing: "mundane" }, + { id: "aff-rage", name: "PRISMRPG.Status.Rage", icon: "icons/svg/fire.svg", category: "affliction", typing: "mundane" }, + { id: "aff-sealed", name: "PRISMRPG.Status.Sealed", icon: "icons/svg/net.svg", category: "affliction", typing: "mundane" }, + { id: "aff-staggered", name: "PRISMRPG.Status.Staggered", icon: "icons/svg/daze.svg", category: "affliction", typing: "mundane" }, + { id: "aff-stunned", name: "PRISMRPG.Status.Stunned", icon: "icons/svg/stun.svg", category: "affliction", typing: "mundane" }, + { id: "aff-taunt", name: "PRISMRPG.Status.Taunt", icon: "icons/svg/eye.svg", category: "affliction", typing: "mundane" }, + { id: "aff-unconscious",name: "PRISMRPG.Status.Unconscious",icon: "icons/svg/unconscious.svg", category: "affliction", typing: "mundane" }, + { id: "aff-wounded", name: "PRISMRPG.Status.Wounded", icon: "icons/svg/degen.svg", category: "affliction", typing: "mundane" }, + // ── Magic ────────────────────────────────────────────────────────────────── + { id: "aff-banished", name: "PRISMRPG.Status.Banished", icon: "icons/svg/wing.svg", category: "affliction", typing: "magic" }, + { id: "aff-seep", name: "PRISMRPG.Status.Seep", icon: "icons/svg/acid.svg", category: "affliction", typing: "magic" }, + { id: "aff-sightless", name: "PRISMRPG.Status.Sightless", icon: "icons/svg/blind.svg", category: "affliction", typing: "magic" }, + { id: "aff-cursed", name: "PRISMRPG.Status.Cursed", icon: "icons/svg/sun.svg", category: "affliction", typing: "magic" }, + { id: "aff-soundless", name: "PRISMRPG.Status.Soundless", icon: "icons/svg/deaf.svg", category: "affliction", typing: "magic" }, + { id: "aff-plagued", name: "PRISMRPG.Status.Plagued", icon: "icons/svg/biohazard.svg", category: "affliction", typing: "magic" }, + { id: "aff-compulsed", name: "PRISMRPG.Status.Compulsed", icon: "icons/svg/eye.svg", category: "affliction", typing: "magic" }, + { id: "aff-fatigue", name: "PRISMRPG.Status.Fatigue", icon: "icons/svg/sleep.svg", category: "affliction", typing: "magic" }, + { id: "aff-horror", name: "PRISMRPG.Status.Horror", icon: "icons/svg/terror.svg", category: "affliction", typing: "magic" }, + { id: "aff-madness", name: "PRISMRPG.Status.Madness", icon: "icons/svg/daze.svg", category: "affliction", typing: "magic" }, + { id: "aff-silenced", name: "PRISMRPG.Status.Silenced", icon: "icons/svg/silenced.svg", category: "affliction", typing: "magic" }, + { id: "aff-locked", name: "PRISMRPG.Status.Locked", icon: "icons/svg/net.svg", category: "affliction", typing: "magic" }, + { id: "aff-dazed", name: "PRISMRPG.Status.Dazed", icon: "icons/svg/daze.svg", category: "affliction", typing: "magic" }, + { id: "aff-numbed", name: "PRISMRPG.Status.Numbed", icon: "icons/svg/frozen.svg", category: "affliction", typing: "magic" }, + { id: "aff-comatose", name: "PRISMRPG.Status.Comatose", icon: "icons/svg/unconscious.svg", category: "affliction", typing: "magic" }, + { id: "aff-shattered", name: "PRISMRPG.Status.Shattered", icon: "icons/svg/blood.svg", category: "affliction", typing: "magic" }, + // ── Elemental (Magic) ────────────────────────────────────────────────────── + { id: "aff-burning", name: "PRISMRPG.Status.Burning", icon: "icons/svg/fire.svg", category: "affliction", typing: "magic" }, + { id: "aff-chilled", name: "PRISMRPG.Status.Chilled", icon: "icons/svg/frozen.svg", category: "affliction", typing: "magic" }, + { id: "aff-corroded", name: "PRISMRPG.Status.Corroded", icon: "icons/svg/acid.svg", category: "affliction", typing: "magic" }, + { id: "aff-necrosis", name: "PRISMRPG.Status.Necrosis", icon: "icons/svg/degen.svg", category: "affliction", typing: "magic" }, + { id: "aff-radiated", name: "PRISMRPG.Status.Radiated", icon: "icons/svg/lightning.svg", category: "affliction", typing: "magic" }, + { id: "aff-shocked", name: "PRISMRPG.Status.Shocked", icon: "icons/svg/lightning.svg", category: "affliction", typing: "magic" }, +] + +/** + * Imbuements — positive status effects (Mundane & Magic) + * @type {StatusEffectConfig[]} + */ +export const IMBUEMENTS = [ + // ── Mundane ──────────────────────────────────────────────────────────────── + { id: "imb-aided", name: "PRISMRPG.Status.Aided", icon: "icons/svg/upgrade.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-alert", name: "PRISMRPG.Status.Alert", icon: "icons/svg/eye.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-alkalized", name: "PRISMRPG.Status.Alkalized", icon: "icons/svg/acid.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-bestowed", name: "PRISMRPG.Status.Bestowed", icon: "icons/svg/angel.svg", category: "imbuement", typing: "both" }, + { id: "imb-concealed", name: "PRISMRPG.Status.Concealed", icon: "icons/svg/invisible.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-enhance", name: "PRISMRPG.Status.Enhance", icon: "icons/svg/upgrade.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-inspired", name: "PRISMRPG.Status.Inspired", icon: "icons/svg/regen.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-keen", name: "PRISMRPG.Status.Keen", icon: "icons/svg/eye.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-life-drain", name: "PRISMRPG.Status.LifeDrain", icon: "icons/svg/regen.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-madness", name: "PRISMRPG.Status.Madness", icon: "icons/svg/daze.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-prepared", name: "PRISMRPG.Status.Prepared", icon: "icons/svg/upgrade.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-rage", name: "PRISMRPG.Status.Rage", icon: "icons/svg/fire.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-reinforced", name: "PRISMRPG.Status.Reinforced", icon: "icons/svg/mage-shield.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-renewed", name: "PRISMRPG.Status.Renewed", icon: "icons/svg/regen.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-supplied", name: "PRISMRPG.Status.Supplied", icon: "icons/svg/upgrade.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-surged", name: "PRISMRPG.Status.Surged", icon: "icons/svg/lightning.svg", category: "imbuement", typing: "mundane" }, + { id: "imb-trance", name: "PRISMRPG.Status.Trance", icon: "icons/svg/sleep.svg", category: "imbuement", typing: "mundane" }, + // ── Magic ────────────────────────────────────────────────────────────────── + { id: "imb-blessed", name: "PRISMRPG.Status.Blessed", icon: "icons/svg/angel.svg", category: "imbuement", typing: "magic" }, + { id: "imb-anchored", name: "PRISMRPG.Status.Anchored", icon: "icons/svg/net.svg", category: "imbuement", typing: "magic" }, + { id: "imb-saturated", name: "PRISMRPG.Status.Saturated", icon: "icons/svg/regen.svg", category: "imbuement", typing: "magic" }, + { id: "imb-invisible", name: "PRISMRPG.Status.Invisible", icon: "icons/svg/invisible.svg", category: "imbuement", typing: "magic" }, + { id: "imb-enchanted", name: "PRISMRPG.Status.Enchanted", icon: "icons/svg/mage-shield.svg", category: "imbuement", typing: "magic" }, + { id: "imb-heroism", name: "PRISMRPG.Status.Heroism", icon: "icons/svg/upgrade.svg", category: "imbuement", typing: "magic" }, + { id: "imb-mana-drain", name: "PRISMRPG.Status.ManaDrain", icon: "icons/svg/regen.svg", category: "imbuement", typing: "magic" }, + { id: "imb-fury", name: "PRISMRPG.Status.Fury", icon: "icons/svg/fire.svg", category: "imbuement", typing: "magic" }, + { id: "imb-warded", name: "PRISMRPG.Status.Warded", icon: "icons/svg/holy-shield.svg", category: "imbuement", typing: "magic" }, + { id: "imb-regeneration", name: "PRISMRPG.Status.Regeneration", icon: "icons/svg/regen.svg", category: "imbuement", typing: "magic" }, + { id: "imb-haste", name: "PRISMRPG.Status.Haste", icon: "icons/svg/wingfoot.svg", category: "imbuement", typing: "magic" }, +] diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index 2af5daf..cc22135 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -36,8 +36,7 @@ export default class PrismRPGActor extends Actor { if (this.type === "character") { Object.assign(prototypeToken, { sight: { enabled: true }, - actorLink: true, - disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY, + actorLink: false }) this.updateSource({ prototypeToken }) } diff --git a/module/models/_module.mjs b/module/models/_module.mjs index 9985de9..9d49064 100644 --- a/module/models/_module.mjs +++ b/module/models/_module.mjs @@ -6,6 +6,7 @@ export { default as PrismRPGSkill } from "./skill.mjs" export { default as PrismRPGArmor } from "./armor.mjs" export { default as PrismRPGShield } from "./shield.mjs" export { default as PrismRPGRacialAbility } from "./racial-ability.mjs" +export { default as PrismRPGAbility } from "./ability.mjs" export { default as PrismRPGEquipment } from "./equipment.mjs" export { default as PrismRPGRace } from "./race.mjs" export { default as PrismRPGClass } from "./class.mjs" diff --git a/module/models/ability.mjs b/module/models/ability.mjs new file mode 100644 index 0000000..ea37fc0 --- /dev/null +++ b/module/models/ability.mjs @@ -0,0 +1,14 @@ +export default class PrismRPGAbility extends foundry.abstract.TypeDataModel { + static defineSchema() { + const fields = foundry.data.fields + const schema = {} + + schema.description = new fields.HTMLField({ required: true, textSearch: true }) + + return schema + } + + /** @override */ + static LOCALIZATION_PREFIXES = ["PRISMRPG.Ability"] + +} diff --git a/module/utils.mjs b/module/utils.mjs index ac5abe2..3aca91c 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -265,6 +265,7 @@ export default class PrismRPGUtils { const templatePaths = [ 'systems/fvtt-prism-rpg/templates/partial-item-effects.hbs', 'systems/fvtt-prism-rpg/templates/weapon-types-config.hbs', + 'systems/fvtt-prism-rpg/templates/chat-new-round.hbs', ] return foundry.applications.handlebars.loadTemplates(templatePaths) } diff --git a/prism-rpg.mjs b/prism-rpg.mjs index 1434873..35b914f 100644 --- a/prism-rpg.mjs +++ b/prism-rpg.mjs @@ -4,6 +4,7 @@ */ import { SYSTEM } from "./module/config/system.mjs" +import { AFFLICTIONS, IMBUEMENTS } from "./module/config/effects.mjs" globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope // Import modules @@ -52,6 +53,7 @@ Hooks.once("init", function () { CONFIG.Item.dataModels = { skill: models.PrismRPGSkill, "racial-ability": models.PrismRPGRacialAbility, + ability: models.PrismRPGAbility, weapon: models.PrismRPGWeapon, armor: models.PrismRPGArmor, shield: models.PrismRPGShield, @@ -70,6 +72,7 @@ Hooks.once("init", function () { foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGSkillSheet, { types: ["skill"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGRacialAbilitySheet, { types: ["racial-ability"], makeDefault: true }) + foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGAbilitySheet, { types: ["ability"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGWeaponSheet, { types: ["weapon"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGSpellSheet, { types: ["spell"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGArmorSheet, { types: ["armor"], makeDefault: true }) @@ -79,6 +82,13 @@ Hooks.once("init", function () { foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGClassSheet, { types: ["class"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("prismRPG", applications.PrismRPGCharacterPathSheet, { types: ["character-path"], makeDefault: true }) + // Status Effects — Afflictions & Imbuements + CONFIG.statusEffects = [ + { id: "dead", name: "EFFECT.StatusDead", icon: "icons/svg/skull.svg" }, + ...AFFLICTIONS, + ...IMBUEMENTS, + ] + // Other Document Configuration CONFIG.ChatMessage.documentClass = documents.PrismRPGChatMessage @@ -196,6 +206,32 @@ Hooks.on(hookName, (message, html, data) => { } } + // Handle new round AP/mana restore buttons (GM only) + if (typeMessage === "newRound") { + $html.find(".new-round-restore-btn").click(async (event) => { + const btn = event.currentTarget + const actorId = btn.dataset.actorId + + const targets = actorId === "all" + ? $html.find(".new-round-restore-btn[data-actor-id!='all']").toArray().map(b => game.actors.get(b.dataset.actorId)).filter(Boolean) + : [game.actors.get(actorId)].filter(Boolean) + + for (const actor of targets) { + await actor.update({ + "system.actionPoints.value": actor.system.actionPoints.max, + "system.manaPoints.value": Math.min(actor.system.manaPoints.value + 1, actor.system.manaPoints.max) + }) + } + + if (actorId === "all") { + $html.find(".new-round-restore-btn").prop("disabled", true).addClass("restored") + } else { + btn.disabled = true + btn.classList.add("restored") + } + }) + } + // Handle Roll Damage button click in weapon attack messages $html.find(".roll-damage-button").click(async (event) => { const btn = event.currentTarget @@ -211,6 +247,45 @@ Hooks.on(hookName, (message, html, data) => { await actor.prepareRoll("weapon-damage-medium", weaponId) }) }) +/** + * Send a GM-only chat message with restore buttons at the start of each new round + */ +Hooks.on("updateCombat", async (combat, change, _options, _userId) => { + if (!game.user.isGM) return + if (change.round === undefined || change.round <= 1) return + + // Deduplicated character-type actors from the active combat + const seen = new Set() + const playerActors = combat.combatants.contents + .filter(c => c.actor?.type === "character" && !seen.has(c.actor.id) && seen.add(c.actor.id)) + .map(c => ({ id: c.actor.id, name: c.actor.name, img: c.actor.img })) + + if (playerActors.length === 0) return + + const content = await foundry.applications.handlebars.renderTemplate( + "systems/fvtt-prism-rpg/templates/chat-new-round.hbs", + { actors: playerActors, round: change.round } + ) + await ChatMessage.create({ + content, + whisper: ChatMessage.getWhisperRecipients("GM"), + flags: { prismRPG: { typeMessage: "newRound" } } + }) +}) + +/** + * Inject a visual separator between Afflictions and Imbuements in the Token HUD status tray + */ +Hooks.on("renderTokenHUD", (_app, html) => { + const tray = html.querySelector(".status-effects") + if (!tray) return + const firstImb = tray.querySelector("[data-status-id^='imb-']") + if (!firstImb) return + const sep = document.createElement("div") + sep.className = "status-separator" + firstImb.before(sep) +}) + /** * Create a macro when dropping an entity on the hotbar * Item - open roll dialog diff --git a/styles/ability.less b/styles/ability.less new file mode 100644 index 0000000..3156cce --- /dev/null +++ b/styles/ability.less @@ -0,0 +1,26 @@ +.ability-content { + .sheet-common(); + .item-sheet-common(); + + .header { + display: flex; + img { + width: 50px; + height: 50px; + } + } + + input[type="checkbox"] { + font-size: var(--font-size-14); + width: 20px; + padding-top: 0; + } + + input[type="checkbox"]:checked { + background-color: rgba(0, 0, 0, 0.1); + } + + input[type="checkbox"]:checked::after { + color: rgba(0, 0, 0, 0.1); + } +} diff --git a/styles/chat.less b/styles/chat.less index 02d3e11..0c2972d 100644 --- a/styles/chat.less +++ b/styles/chat.less @@ -548,3 +548,90 @@ } } } + + +// New round message +.new-round-message { + .chat-title .new-round-label { + font-size: var(--font-size-11); + font-style: italic; + color: #8a7a5a; + margin-top: 1px; + } + + .new-round-actors { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + padding: 6px 0 2px; + + .new-round-restore-btn { + display: flex; + align-items: center; + gap: 6px; + width: calc(50% - 3px); + padding: 4px 8px 4px 6px; + border: 1px solid #7a6a45; + border-radius: 4px; + background: linear-gradient(135deg, #f5e6c8 0%, #e8d5a0 100%); + cursor: pointer; + font-size: var(--font-size-13); + color: #3a2e1a; + overflow: hidden; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + } + + img { + flex-shrink: 0; + width: 24px; + height: 24px; + border: none; + border-radius: 3px; + object-fit: cover; + } + + &:hover:not(:disabled) { + background: linear-gradient(135deg, #fdf3dc 0%, #f0e0b0 100%); + border-color: #a08040; + } + + &:disabled, + &.restored { + opacity: 0.45; + cursor: default; + } + } + + .new-round-all-btn { + width: 100%; + background: linear-gradient(135deg, #c8dff5 0%, #a0c0e8 100%); + border-color: #4a6a8a; + color: #1a2e3a; + font-weight: bold; + + &:hover:not(:disabled) { + background: linear-gradient(135deg, #dcedfc 0%, #b0d0f0 100%); + border-color: #3a5a7a; + } + } + } +} + +// Token HUD — separator between Afflictions and Imbuements +.palette.status-effects { + .status-separator { + grid-column: 1 / -1; + width: 100%; + height: 2px; + border: none; + border-top: 2px solid rgba(255, 255, 255, 0.35); + margin: 5px 0; + position: relative; + } +} diff --git a/styles/fvtt-prism-rpg.less b/styles/fvtt-prism-rpg.less index 6546247..272d947 100644 --- a/styles/fvtt-prism-rpg.less +++ b/styles/fvtt-prism-rpg.less @@ -9,6 +9,7 @@ @import "monster.less"; @import "skill.less"; @import "racial-ability.less"; + @import "ability.less"; @import "weapon.less"; @import "armor.less"; @import "spell.less"; diff --git a/system.json b/system.json index a815efe..4ef195a 100644 --- a/system.json +++ b/system.json @@ -34,6 +34,7 @@ "Item": { "skill": { "htmlFields": ["description"] }, "racial-ability": { "htmlFields": ["description"] }, + "ability": { "htmlFields": ["description"] }, "weapon": { "htmlFields": ["description"] }, "armor": { "htmlFields": ["description"] }, "shield": { "htmlFields": ["description"] }, diff --git a/templates/ability.hbs b/templates/ability.hbs new file mode 100644 index 0000000..15754d7 --- /dev/null +++ b/templates/ability.hbs @@ -0,0 +1,38 @@ +
+
+ + {{formInput fields.name value=source.name}} +
+ + {{! Navigation des onglets }} + + + {{! Onglet Description }} +
+
+ {{localize "PRISMRPG.Label.description"}} + {{formInput + systemFields.description + enriched=enrichedDescription + value=system.description + name="system.description" + toggled=true + }} +
+
+ + {{! Onglet Effects }} +
+ {{> systems/fvtt-prism-rpg/templates/partial-item-effects.hbs}} +
+ +
diff --git a/templates/chat-new-round.hbs b/templates/chat-new-round.hbs new file mode 100644 index 0000000..69397ac --- /dev/null +++ b/templates/chat-new-round.hbs @@ -0,0 +1,20 @@ +
+
+
+
{{localize "PRISMRPG.Combat.newRound"}} {{round}}
+
{{localize "PRISMRPG.Combat.restoreAP"}}
+
+
+
+ + {{#each actors}} + + {{/each}} +
+