diff --git a/dnd5e.mjs b/dnd5e.mjs index 17d1c31fc3..8e34e0934e 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -587,6 +587,8 @@ Hooks.on("renderJournalPageSheet", applications.journal.JournalSheet5e.onRenderJ Hooks.on("targetToken", canvas.Token5e.onTargetToken); +Hooks.on("renderCombatTracker", (app, html, data) => app.renderGroups(html instanceof HTMLElement ? html : html[0])); + Hooks.on("preCreateScene", (doc, createData, options, userId) => { // Set default grid units based on metric length setting const units = utils.defaultUnits("length"); diff --git a/lang/en.json b/lang/en.json index 1512f7d36d..d673b46f16 100644 --- a/lang/en.json +++ b/lang/en.json @@ -818,6 +818,21 @@ "DND5E.ClassOriginal": "Original Class", "DND5E.ClassSaves": "Saving Throws", "DND5E.Confirm": "Confirm", + +"DND5E.COMBAT": { + "Group": { + "ActiveCount": "{current} of {combatants}", + "Title": "{name} Group" + } +}, + +"DND5E.COMBATANT": { + "Counted": { + "one": "{number} combatant", + "other": "{number} combatants" + } +}, + "DND5E.CompendiumBrowser": { "Title": "Compendium Browser", "Action": { diff --git a/less/v2/apps.less b/less/v2/apps.less index 6a3c22eb18..57ef879c11 100644 --- a/less/v2/apps.less +++ b/less/v2/apps.less @@ -231,20 +231,6 @@ } } - .collapsible { - &.collapsed { - .fa-caret-down { transform: rotate(-90deg); } - .collapsible-content { grid-template-rows: 0fr; } - } - .fa-caret-down { transition: transform 250ms ease; } - .collapsible-content { - display: grid; - grid-template-rows: 1fr; - transition: grid-template-rows 250ms ease; - > .wrapper { overflow: hidden; } - } - } - .unlist { list-style: none; padding: 0; @@ -1559,6 +1545,55 @@ dialog.dnd5e2.application { } } +/* ---------------------------------- */ +/* Collapsible Content */ +/* ---------------------------------- */ + +.dnd5e2 .collapsible, .collapsible.dnd5e2-collapsible { + &.collapsed { + .fa-caret-down { transform: rotate(-90deg); } + .collapsible-content { grid-template-rows: 0fr; } + } + .fa-caret-down { transition: transform 250ms ease; } + .collapsible-content { + display: grid; + grid-template-rows: 1fr; + transition: grid-template-rows 250ms ease; + > .wrapper { overflow: hidden; } + } +} + +/* ---------------------------------- */ +/* Combat Tracker */ +/* ---------------------------------- */ + +.combat-sidebar { + .combatant-group { + .fa-chevron-down { transition: transform 250ms ease; } + &.collapsed .fa-chevron-down { transform: rotate(-90deg); } + + &.active::after, &.active::before { transition: border-color 250ms ease; } + &.active:not(.collapsed)::after { border-color: color-mix(in oklab, var(--color-border-highlight), transparent); } + + .group-header { cursor: pointer; } + .group-numbers { + flex: 0 0 20px; + color: var(--color-text-light-5); + font-size: var(--font-size-12); + line-height: 20px; + } + } + + /* V13 Syles */ + .combat-tracker { + .combatant-group { + display: block; + &.active:not(.collapsed)::before { border-color: color-mix(in oklab, var(--color-warm-2), transparent); } + } + .group-children { padding: 0; } + } +} + /* ---------------------------------- */ /* Context Menus */ /* ---------------------------------- */ diff --git a/module/applications/combat/combat-tracker.mjs b/module/applications/combat/combat-tracker.mjs index 6b970761aa..3a5acdade4 100644 --- a/module/applications/combat/combat-tracker.mjs +++ b/module/applications/combat/combat-tracker.mjs @@ -1,10 +1,18 @@ -import { formatNumber } from "../../utils.mjs"; +import ContextMenu5e from "../context-menu.mjs"; +import { formatNumber, getPluralRules } from "../../utils.mjs"; + +/** + * @typedef {object} CombatGroupData + * @property {boolean} expanded + */ /** * An extension of the base CombatTracker class to provide some 5e-specific functionality. * @extends {CombatTracker} */ export default class CombatTracker5e extends CombatTracker { + + /** @inheritDoc */ async getData(options={}) { const context = await super.getData(options); context.turns.forEach(turn => { @@ -23,4 +31,81 @@ export default class CombatTracker5e extends CombatTracker { if ( (btn.dataset.control === "rollInitiative") && combatant?.actor ) return combatant.actor.rollInitiativeDialog(); return super._onCombatantControl(event); } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _contextMenu(html) { + if ( !(html instanceof HTMLElement) ) html = html[0]; + new ContextMenu5e( + html.querySelector(".directory-list"), ".directory-item:not(.combatant-group)", this._getEntryContextOptions() + ); + } + + /* -------------------------------------------- */ + + /** + * Adjust initiative tracker to group combatants. + * @param {HTMLElement} html The combat tracker being rendered. + */ + renderGroups(html) { + if ( !this.viewed ) return; + const groups = this.viewed.createGroups(); + const V13 = game.release.generation >= 13; + const list = html.querySelector(".directory-list, .combat-tracker"); + for ( const [key, { combatants, expanded }] of groups.entries() ) { + const children = list.querySelectorAll(Array.from(combatants).map(c => `[data-combatant-id="${c.id}"]`).join(", ")); + if ( !children.length ) continue; + const groupContainer = document.createElement("li"); + groupContainer.classList.add("combatant", "combatant-group", "collapsible", "dnd5e2-collapsible"); + if ( !V13 ) groupContainer.classlist.add("directory-item"); + if ( !expanded ) groupContainer.classList.add("collapsed"); + + // Determine the count + let activeEntry; + for ( const [index, element] of children.entries() ) { + if ( element.classList.contains("active") ) activeEntry = index; + } + let count = game.i18n.format(`DND5E.COMBATANT.Counted.${getPluralRules().select(children.length)}`, { + number: formatNumber(children.length) + }); + if ( activeEntry !== undefined ) { + groupContainer.classList.add("active"); + count = game.i18n.format("DND5E.COMBAT.Group.ActiveCount", { + combatants: count, current: formatNumber(activeEntry + 1) + }); + } + + const name = combatants[0].token.baseActor.prototypeToken.name; + const img = children[0].querySelector("img"); + groupContainer.innerHTML = ` +