Combat and CombatTracker (#108)

* Added Combat and CombatTracker

* Some cleneaup

* Fixing and cleaning up

* Added categories for combatants

* Style improvements

* Layout change
This commit is contained in:
WBHarry 2025-06-07 00:06:54 +02:00 committed by GitHub
parent 32730b3aac
commit aa8fe6a7a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 730 additions and 684 deletions

View file

@ -3,11 +3,10 @@ import * as applications from './module/applications/_module.mjs';
import * as models from './module/data/_module.mjs'; import * as models from './module/data/_module.mjs';
import * as documents from './module/documents/_module.mjs'; import * as documents from './module/documents/_module.mjs';
import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs'; import RegisterHandlebarsHelpers from './module/helpers/handlebarsHelper.mjs';
import DhpCombatTracker from './module/ui/combatTracker.mjs'; import DhCombatTracker from './module/ui/combatTracker.mjs';
import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs'; import { GMUpdateEvent, handleSocketEvent, socketEvent } from './module/helpers/socket.mjs';
import { registerDHSettings } from './module/applications/settings.mjs'; import { registerDHSettings } from './module/applications/settings.mjs';
import DhpChatLog from './module/ui/chatLog.mjs'; import DhpChatLog from './module/ui/chatLog.mjs';
import DhpPlayers from './module/ui/players.mjs';
import DhpRuler from './module/ui/ruler.mjs'; import DhpRuler from './module/ui/ruler.mjs';
import DhpTokenRuler from './module/ui/tokenRuler.mjs'; import DhpTokenRuler from './module/ui/tokenRuler.mjs';
import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs'; import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs';
@ -74,11 +73,11 @@ Hooks.once('init', () => {
Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true }); Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true });
CONFIG.Combat.dataModels = { CONFIG.Combat.dataModels = {
base: models.DhpCombat base: models.DhCombat
}; };
CONFIG.Combatant.dataModels = { CONFIG.Combatant.dataModels = {
base: models.DhpCombatant base: models.DhCombatant
}; };
CONFIG.ChatMessage.dataModels = { CONFIG.ChatMessage.dataModels = {
@ -91,7 +90,7 @@ Hooks.once('init', () => {
CONFIG.Canvas.rulerClass = DhpRuler; CONFIG.Canvas.rulerClass = DhpRuler;
CONFIG.Combat.documentClass = documents.DhpCombat; CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = DhpCombatTracker; CONFIG.ui.combat = DhCombatTracker;
CONFIG.ui.chat = DhpChatLog; CONFIG.ui.chat = DhpChatLog;
// CONFIG.ui.players = DhpPlayers; // CONFIG.ui.players = DhpPlayers;
CONFIG.Token.rulerClass = DhpTokenRuler; CONFIG.Token.rulerClass = DhpTokenRuler;
@ -111,8 +110,8 @@ Hooks.once('init', () => {
Hooks.on('ready', () => { Hooks.on('ready', () => {
ui.resources = new CONFIG.ui.resources(); ui.resources = new CONFIG.ui.resources();
ui.resources.render({force: true}); ui.resources.render({ force: true });
}) });
Hooks.once('dicesoniceready', () => {}); Hooks.once('dicesoniceready', () => {});
@ -295,6 +294,7 @@ const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs', 'systems/daggerheart/templates/sheets/pc/parts/heritageCard.hbs',
'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs', 'systems/daggerheart/templates/sheets/pc/parts/advancementCard.hbs',
'systems/daggerheart/templates/views/parts/level.hbs', 'systems/daggerheart/templates/views/parts/level.hbs',
'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs' 'systems/daggerheart/templates/sheets/global/partials/feature-section-item.hbs',
'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs'
]); ]);
}; };

View file

@ -61,6 +61,13 @@
"outline": "Outline", "outline": "Outline",
"edge": "Edge" "edge": "Edge"
} }
},
"VariantRules": {
"title": "Variant Rules",
"label": "Variant Rules",
"hint": "Apply variant rules from the Daggerheart system",
"name": "Variant Rules",
"actionTokens": "Action Tokens"
} }
}, },
"Automation": { "Automation": {
@ -101,6 +108,12 @@
"Hint": "Enable measuring of ranges with the ruler according to set distances." "Hint": "Enable measuring of ranges with the ruler according to set distances."
} }
}, },
"VariantRules": {
"ActionTokens": {
"Name": "Action Tokens",
"Hint": "Give each player action tokens to use in combat"
}
},
"DualityRollColor": { "DualityRollColor": {
"Name": "Duality Roll Colour Scheme", "Name": "Duality Roll Colour Scheme",
"Hint": "The display type for Duality Rolls", "Hint": "The display type for Duality Rolls",
@ -150,6 +163,14 @@
"Or": "Or", "Or": "Or",
"Description": "Description", "Description": "Description",
"Features": "Features", "Features": "Features",
"Adversary": {
"Singular": "Adversary",
"Plural": "Adversaries"
},
"Character": {
"Singular": "Character",
"Plural": "Characters"
},
"RefreshType": { "RefreshType": {
"Session": "Session", "Session": "Session",
"Shortrest": "Short Rest", "Shortrest": "Short Rest",
@ -329,6 +350,12 @@
"grimoire": "Grimoire" "grimoire": "Grimoire"
} }
}, },
"Combat": {
"giveSpotlight": "Give The Spotlight",
"requestSpotlight": "Request The Spotlight",
"requestingSpotlight": "Requesting The Spotlight",
"combatStarted": "Active"
},
"LevelUp": { "LevelUp": {
"Tier1": { "Tier1": {
"Label": "Level 2-4", "Label": "Level 2-4",

View file

@ -1,4 +1,3 @@
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
/** /**
@ -10,101 +9,101 @@ const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
*/ */
export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) { export default class Resources extends HandlebarsApplicationMixin(ApplicationV2) {
constructor(options={}) { constructor(options = {}) {
super(options); super(options);
}
/** @inheritDoc */
static DEFAULT_OPTIONS = {
id: "resources",
classes: [],
tag: "div",
window: {
frame: true,
title: "Fear",
positioned: true,
resizable: true,
minimizable: false
},
actions: {
setFear: Resources.setFear,
increaseFear: Resources.increaseFear
},
position: {
width: 222,
height: 222,
// top: "200px",
// left: "120px"
} }
};
/** @override */ /** @inheritDoc */
static PARTS = { static DEFAULT_OPTIONS = {
resources: { id: 'resources',
root: true, classes: [],
template: "systems/daggerheart/templates/views/resources.hbs" tag: 'div',
// template: "templates/ui/players.hbs" window: {
frame: true,
title: 'Fear',
positioned: true,
resizable: true,
minimizable: false
},
actions: {
setFear: Resources.setFear,
increaseFear: Resources.increaseFear
},
position: {
width: 222,
height: 222
// top: "200px",
// left: "120px"
}
};
/** @override */
static PARTS = {
resources: {
root: true,
template: 'systems/daggerheart/templates/views/resources.hbs'
}
};
get currentFear() {
return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
} }
};
get currentFear() { get maxFear() {
return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear); return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear);
} }
get maxFear() { /* -------------------------------------------- */
return game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.MaxFear); /* Rendering */
} /* -------------------------------------------- */
/* -------------------------------------------- */ /** @override */
/* Rendering */ async _prepareContext(_options) {
/* -------------------------------------------- */ const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear),
current = this.currentFear,
max = this.maxFear,
percent = (current / max) * 100,
isGM = game.user.isGM;
// Return the data for rendering
return { display, current, max, percent, isGM };
}
/** @override */ /** @override */
async _prepareContext(_options) { async _preFirstRender(context, options) {
const display = game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.DisplayFear), options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position;
current = this.currentFear, }
max = this.maxFear,
percent = (current / max) * 100,
isGM = game.user.isGM;
// Return the data for rendering
return {display, current, max, percent, isGM};
}
/** @override */ /** @override */
async _preFirstRender(context, options) { async _preRender(context, options) {
options.position = game.user.getFlag(SYSTEM.id, 'app.resources.position') ?? Resources.DEFAULT_OPTIONS.position; if (this.currentFear > this.maxFear)
} await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear);
}
/** @override */ _onPosition(position) {
async _preRender(context, options) { game.user.setFlag(SYSTEM.id, 'app.resources.position', position);
if(this.currentFear > this.maxFear) await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, this.maxFear); }
}
_onPosition(position) { async close(options = {}) {
game.user.setFlag(SYSTEM.id, 'app.resources.position', position); if (!options.allowed) return;
} else super.close(options);
}
async close(options={}) { static async setFear(event, target) {
if(!options.allowed) return; if (!game.user.isGM) return;
else super.close(options); const fearCount = Number(target.dataset.index ?? 0);
} await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1);
}
static async setFear(event, target) { static async increaseFear(event, target) {
if(!game.user.isGM) return; let value = target.dataset.increment ?? 0,
const fearCount = Number(target.dataset.index ?? 0); operator = value.split('')[0] ?? null;
await this.updateFear(this.currentFear === fearCount + 1 ? fearCount : fearCount + 1); value = Number(value);
} await this.updateFear(operator ? this.currentFear + value : value);
}
static async increaseFear(event, target) { async updateFear(value) {
let value = target.dataset.increment ?? 0, if (!game.user.isGM) return;
operator = value.split('')[0] ?? null; value = Math.max(0, Math.min(this.maxFear, value));
value = Number(value); await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value);
await this.updateFear(operator ? this.currentFear + value : value); }
}
async updateFear(value) {
if(!game.user.isGM) return;
value = Math.max(0, Math.min(this.maxFear, value));
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear, value);
}
} }

View file

@ -1,5 +1,7 @@
import DhAppearance from '../data/settings/Appearance.mjs'; import DhAppearance from '../data/settings/Appearance.mjs';
import DHAppearanceSettings from './settings/appearanceSettings.mjs'; import DHAppearanceSettings from './settings/appearanceSettings.mjs';
import DhVariantRules from '../data/settings/VariantRules.mjs';
import DHVariantRuleSettings from './settings/variantRuleSettings.mjs';
class DhpAutomationSettings extends FormApplication { class DhpAutomationSettings extends FormApplication {
constructor(object = {}, options = {}) { constructor(object = {}, options = {}) {
@ -181,7 +183,8 @@ export const registerDHSettings = () => {
type: Number, type: Number,
default: 0, default: 0,
onChange: () => { onChange: () => {
if(ui.resources) ui.resources.render({force: true}); if (ui.resources) ui.resources.render({ force: true });
ui.combat.render({ force: true });
} }
}); });
@ -193,7 +196,7 @@ export const registerDHSettings = () => {
type: Number, type: Number,
default: 12, default: 12,
onChange: () => { onChange: () => {
if(ui.resources) ui.resources.render({force: true}); if (ui.resources) ui.resources.render({ force: true });
} }
}); });
@ -204,15 +207,15 @@ export const registerDHSettings = () => {
config: true, config: true,
type: String, type: String,
choices: { choices: {
'token': 'Tokens', token: 'Tokens',
'bar': 'Bar', bar: 'Bar',
'hide': 'Hide' hide: 'Hide'
}, },
default: 'token', default: 'token',
onChange: value => { onChange: value => {
if(ui.resources) { if (ui.resources) {
if(value === 'hide') ui.resources.close({allowed: true}); if (value === 'hide') ui.resources.close({ allowed: true });
else ui.resources.render({force: true}); else ui.resources.render({ force: true });
} }
} }
}); });
@ -251,6 +254,13 @@ export const registerDHSettings = () => {
} }
}); });
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, {
scope: 'world',
config: false,
type: DhVariantRules,
default: DhVariantRules.defaultSchema
});
game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, { game.settings.register(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.appearance, {
scope: 'client', scope: 'client',
config: false, config: false,
@ -291,4 +301,13 @@ export const registerDHSettings = () => {
type: DHAppearanceSettings, type: DHAppearanceSettings,
restricted: false restricted: false
}); });
game.settings.registerMenu(SYSTEM.id, SYSTEM.SETTINGS.menu.VariantRules.Name, {
name: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.title'),
label: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.label'),
hint: game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.hint'),
icon: SYSTEM.SETTINGS.menu.VariantRules.Icon,
type: DHVariantRuleSettings,
restricted: false
});
}; };

View file

@ -0,0 +1,59 @@
import DhVariantRules from '../../data/settings/VariantRules.mjs';
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
export default class DHVariantRuleSettings extends HandlebarsApplicationMixin(ApplicationV2) {
constructor() {
super({});
this.settings = new DhVariantRules(
game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).toObject()
);
}
get title() {
return game.i18n.localize('DAGGERHEART.Settings.Menu.VariantRules.name');
}
static DEFAULT_OPTIONS = {
tag: 'form',
id: 'daggerheart-appearance-settings',
classes: ['daggerheart', 'setting', 'dh-style'],
position: { width: '600', height: 'auto' },
actions: {
reset: this.reset,
save: this.save
},
form: { handler: this.updateData, submitOnChange: true }
};
static PARTS = {
main: {
template: 'systems/daggerheart/templates/settings/variant-rules.hbs'
}
};
async _prepareContext(_options) {
const context = await super._prepareContext(_options);
context.settingFields = this.settings;
return context;
}
static async updateData(event, element, formData) {
const updatedSettings = foundry.utils.expandObject(formData.object);
await this.settings.updateSource(updatedSettings);
this.render();
}
static async reset() {
this.settings = new DhVariantRules();
this.render();
}
static async save() {
await game.settings.set(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules, this.settings.toObject());
this.close();
}
}

View file

@ -10,6 +10,10 @@ export const menu = {
Range: { Range: {
Name: 'GameSettingsRange', Name: 'GameSettingsRange',
Icon: 'fa-solid fa-ruler' Icon: 'fa-solid fa-ruler'
},
VariantRules: {
Name: 'GameSettingsVariantrules',
Icon: 'fa-solid fa-scale-balanced'
} }
}; };
@ -27,5 +31,6 @@ export const gameSettings = {
AbilityArray: 'AbilityArray', AbilityArray: 'AbilityArray',
RangeMeasurement: 'RangeMeasurement' RangeMeasurement: 'RangeMeasurement'
}, },
appearance: 'Appearance' appearance: 'Appearance',
variantRules: 'VariantRules'
}; };

View file

@ -1,8 +1,8 @@
export { default as DhpPC } from './pc.mjs'; export { default as DhpPC } from './pc.mjs';
export { default as DhpClass } from './class.mjs'; export { default as DhpClass } from './class.mjs';
export { default as DhpSubclass } from './subclass.mjs'; export { default as DhpSubclass } from './subclass.mjs';
export { default as DhpCombat } from './combat.mjs'; export { default as DhCombat } from './combat.mjs';
export { default as DhpCombatant } from './combatant.mjs'; export { default as DhCombatant } from './combatant.mjs';
export { default as DhpAdversary } from './adversary.mjs'; export { default as DhpAdversary } from './adversary.mjs';
export { default as DhpFeature } from './feature.mjs'; export { default as DhpFeature } from './feature.mjs';
export { default as DhpDomainCard } from './domainCard.mjs'; export { default as DhpDomainCard } from './domainCard.mjs';

View file

@ -1,9 +1,6 @@
export default class DhpCombat extends foundry.abstract.TypeDataModel { export default class DhCombat extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {};
actions: new fields.NumberField({ initial: 0, integer: true }),
activeCombatant: new fields.StringField({})
};
} }
} }

View file

@ -1,8 +1,11 @@
export default class DhpCombatant extends foundry.abstract.TypeDataModel { export default class DhCombatant extends foundry.abstract.TypeDataModel {
static defineSchema() { static defineSchema() {
const fields = foundry.data.fields; const fields = foundry.data.fields;
return { return {
active: new fields.BooleanField({ initial: false }) spotlight: new fields.SchemaField({
requesting: new fields.BooleanField({ required: true, initial: false })
}),
actionTokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
}; };
} }
} }

View file

@ -0,0 +1,13 @@
export default class DhVariantRules extends foundry.abstract.DataModel {
static defineSchema() {
const fields = foundry.data.fields;
return {
actionTokens: new fields.SchemaField({
enabled: new fields.BooleanField({ required: true, initial: false }),
tokens: new fields.NumberField({ required: true, integer: true, initial: 3 })
})
};
}
static defaultSchema = {};
}

View file

@ -1,44 +1,19 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
export default class DhpCombat extends Combat { export default class DhpCombat extends Combat {
_sortCombatants(a, b) { async startCombat() {
if (a.isNPC !== b.isNPC) { this._playCombatSound('startEncounter');
const aVal = a.isNPC ? 0 : 1; const updateData = { round: 1, turn: null };
const bVal = b.isNPC ? 0 : 1; Hooks.callAll('combatStart', this, updateData);
await this.update(updateData);
return this;
}
return aVal - bVal; _sortCombatants(a, b) {
const aNPC = Number(a.isNPC);
const bNPC = Number(b.isNPC);
if (aNPC !== bNPC) {
return aNPC - bNPC;
} }
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
} }
async useActionToken(combatantId) {
const automateActionPoints = await game.settings.get(
SYSTEM.id,
SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints
);
if (game.user.isGM) {
if (this.system.actions < 1) return;
const update = automateActionPoints
? { 'system.activeCombatant': combatantId, 'system.actions': Math.max(this.system.actions - 1, 0) }
: { 'system.activeCombatant': combatantId };
await this.update(update);
} else {
const update = automateActionPoints
? { 'system.activeCombatant': combatantId, 'system.actions': this.system.actions + 1 }
: { 'system.activeCombatant': combatantId };
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: {
action: GMUpdateEvent.UpdateDocument,
uuid: this.uuid,
update: update
}
});
}
}
} }

View file

@ -1,199 +1,100 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs'; export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker { actions: {
constructor(data, context) { requestSpotlight: this.requestSpotlight,
super(data, context); toggleSpotlight: this.toggleSpotlight,
setActionTokens: this.setActionTokens
Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate);
}
get template() {
return 'systems/daggerheart/templates/ui/combatTracker.hbs';
}
activateListeners(html) {
super.activateListeners(html);
html.on('click', '.token-action-tokens .use-action-token', this.useActionToken.bind(this));
html.on('click', '.encounter-gm-resources .trade-actions', this.tradeActions.bind(this));
html.on('click', '.encounter-gm-resources .trade-fear', this.tradeFear.bind(this));
html.on('click', '.encounter-gm-resources .icon-button.up', this.increaseResource.bind(this));
html.on('click', '.encounter-gm-resources .icon-button.down', this.decreaseResource.bind(this));
}
async useActionToken(event) {
event.stopPropagation();
const combatant = event.currentTarget.dataset.combatant;
await game.combat.useActionToken(combatant);
}
async tradeActions(event) {
if (event.currentTarget.classList.contains('disabled')) return;
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear + 1;
if (value <= 6) {
Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value }
});
await game.combat.update({ 'system.actions': game.combat.system.actions - 2 });
} }
}
async tradeFear() {
if (event.currentTarget.classList.contains('disabled')) return;
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear - 1;
if (value >= 0) {
Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value }
});
await game.combat.update({ 'system.actions': game.combat.system.actions + 2 });
}
}
async increaseResource(event) {
if (event.currentTarget.dataset.type === 'action') {
await game.combat.update({ 'system.actions': game.combat.system.actions + 1 });
}
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear + 1;
if (event.currentTarget.dataset.type === 'fear' && value <= 6) {
Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value }
});
}
this.render();
}
async decreaseResource(event) {
if (event.currentTarget.dataset.type === 'action' && game.combat.system.actions - 1 >= 0) {
await game.combat.update({ 'system.actions': game.combat.system.actions - 1 });
}
const currentFear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = currentFear - 1;
if (event.currentTarget.dataset.type === 'fear' && value >= 0) {
Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value }
});
}
this.render();
}
async getData(options = {}) {
let context = await super.getData(options);
// Get the combat encounters possible for the viewed Scene
const combat = this.viewed;
const hasCombat = combat !== null;
const combats = this.combats;
const currentIdx = combats.findIndex(c => c === combat);
const previousId = currentIdx > 0 ? combats[currentIdx - 1].id : null;
const nextId = currentIdx < combats.length - 1 ? combats[currentIdx + 1].id : null;
const settings = game.settings.get('core', Combat.CONFIG_SETTING);
// Prepare rendering data
context = foundry.utils.mergeObject(context, {
combats: combats,
currentIndex: currentIdx + 1,
combatCount: combats.length,
hasCombat: hasCombat,
combat,
turns: [],
previousId,
nextId,
started: this.started,
control: false,
settings,
linked: combat?.scene !== null,
labels: {}
});
context.labels.scope = game.i18n.localize(`COMBAT.${context.linked ? 'Linked' : 'Unlinked'}`);
if (!hasCombat) return context;
// Format information about each combatant in the encounter
let hasDecimals = false;
const turns = [];
for (let [i, combatant] of combat.turns.entries()) {
if (!combatant.visible) continue;
// Prepare turn data
const resource =
combatant.permission >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER ? combatant.resource : null;
const turn = {
id: combatant.id,
name: combatant.name,
img: await this._getCombatantThumbnail(combatant),
active: combatant.id === combat.system.activeCombatant,
owner: combatant.isOwner,
defeated: combatant.isDefeated,
hidden: combatant.hidden,
initiative: combatant.initiative,
hasRolled: combatant.initiative !== null,
hasResource: resource !== null,
resource: resource,
canPing: combatant.sceneId === canvas.scene?.id && game.user.hasPermission('PING_CANVAS'),
playerCharacter: game.user?.character?.id === combatant.actor.id,
ownedByPlayer: combatant.hasPlayerOwner
};
if (turn.initiative !== null && !Number.isInteger(turn.initiative)) hasDecimals = true;
turn.css = [turn.active ? 'active' : '', turn.hidden ? 'hidden' : '', turn.defeated ? 'defeated' : '']
.join(' ')
.trim();
// Actor and Token status effects
turn.effects = new Set();
if (combatant.token) {
combatant.token.effects.forEach(e => turn.effects.add(e));
if (combatant.token.overlayEffect) turn.effects.add(combatant.token.overlayEffect);
}
if (combatant.actor) {
for (const effect of combatant.actor.temporaryEffects) {
if (effect.statuses.has(CONFIG.specialStatusEffects.DEFEATED)) turn.defeated = true;
else if (effect.icon) turn.effects.add(effect.icon);
}
}
turns.push(turn);
}
// Format initiative numeric precision
const precision = CONFIG.Combat.initiative.decimals;
turns.forEach(t => {
if (t.initiative !== null) t.initiative = t.initiative.toFixed(hasDecimals ? precision : 0);
});
const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
// Merge update data for rendering
return foundry.utils.mergeObject(context, {
round: combat.round,
turn: combat.turn,
turns: turns,
control: combat.combatant?.players?.includes(game.user),
fear: fear
});
}
onFearUpdate = async () => {
this.render(true);
}; };
async close(options) { static PARTS = {
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate); header: {
template: 'systems/daggerheart/templates/ui/combat/combatTrackerHeader.hbs'
},
tracker: {
template: 'systems/daggerheart/templates/ui/combat/combatTracker.hbs'
},
footer: {
template: 'systems/daggerheart/templates/ui/combat/combatTrackerFooter.hbs'
}
};
return super.close(options); async _prepareCombatContext(context, options) {
await super._prepareCombatContext(context, options);
Object.assign(context, {
fear: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear)
});
}
async _prepareTrackerContext(context, options) {
await super._prepareTrackerContext(context, options);
const adversaries = context.turns.filter(x => x.isNPC);
const characters = context.turns.filter(x => !x.isNPC);
Object.assign(context, {
actionTokens: game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.variantRules).actionTokens,
adversaries,
characters
});
}
async _prepareTurnContext(combat, combatant, index) {
const turn = await super._prepareTurnContext(combat, combatant, index);
return { ...turn, isNPC: combatant.isNPC, system: combatant.system.toObject() };
}
_getCombatContextOptions() {
return [
{
name: 'COMBAT.ClearMovementHistories',
icon: '<i class="fa-solid fa-shoe-prints"></i>',
condition: () => game.user.isGM && this.viewed?.combatants.size > 0,
callback: () => this.viewed.clearMovementHistories()
},
{
name: 'COMBAT.Delete',
icon: '<i class="fa-solid fa-trash"></i>',
condition: () => game.user.isGM && !!this.viewed,
callback: () => this.viewed.endCombat()
}
];
}
static async requestSpotlight(_, target) {
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId);
await combatant.update({
'system.spotlight': {
requesting: !combatant.system.spotlight.requesting
}
});
this.render();
}
static async toggleSpotlight(_, target) {
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId);
const toggleTurn = this.viewed.combatants.contents
.sort(this.viewed._sortCombatants)
.map(x => x.id)
.indexOf(combatantId);
await this.viewed.update({ turn: this.viewed.turn === toggleTurn ? null : toggleTurn });
await combatant.update({ 'system.spotlight.requesting': false });
}
static async setActionTokens(_, target) {
const { combatantId, tokenIndex } = target.closest('[data-combatant-id]')?.dataset ?? {};
const combatant = this.viewed.combatants.get(combatantId);
const changeIndex = Number(tokenIndex);
const newIndex = combatant.system.actionTokens > changeIndex ? changeIndex : changeIndex + 1;
await combatant.update({ 'system.actionTokens': newIndex });
this.render();
} }
} }

View file

@ -1,53 +0,0 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
export default class DhpPlayers extends foundry.applications.ui.Players {
constructor(data, context) {
super(data, context);
Hooks.on(socketEvent.DhpFearUpdate, this.onFearUpdate);
}
get template() {
return 'systems/daggerheart/templates/ui/players.hbs';
}
async getData(options = {}) {
const context = super.getData(options);
context.fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
context.user = game.user;
return context;
}
activateListeners(html) {
// Toggle online/offline
html.find('.players-mode').click(this._onToggleOfflinePlayers.bind(this));
html.find('.fear-control.up').click(async event => await this.updateFear(event, 1));
html.find('.fear-control.down').click(async event => await this.updateFear(event, -1));
// Context menu
const contextOptions = this._getUserContextOptions();
Hooks.call('getUserContextOptions', html, contextOptions);
new ContextMenu(html, '.player', contextOptions);
}
async updateFear(_, change) {
const fear = await game.settings.get(SYSTEM.id, SYSTEM.SETTINGS.gameSettings.Resources.Fear);
const value = Math.max(Math.min(fear + change, 6), 0);
Hooks.callAll(socketEvent.GMUpdate, GMUpdateEvent.UpdateFear, null, value);
await game.socket.emit(`system.${SYSTEM.id}`, {
action: socketEvent.GMUpdate,
data: { action: GMUpdateEvent.UpdateFear, update: value }
});
}
onFearUpdate = async () => {
this.render(true);
};
async close(options) {
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate);
return super.close(options);
}
}

View file

@ -1293,60 +1293,92 @@
.daggerheart.sheet.pc div[data-application-part] .sheet-body .inventory-container .inventory-item-list .inventory-row img { .daggerheart.sheet.pc div[data-application-part] .sheet-body .inventory-container .inventory-item-list .inventory-row img {
width: 32px; width: 32px;
} }
.combat-sidebar .encounter-gm-resources { .combat-sidebar .encounter-controls.combat {
flex: 0; justify-content: space-between;
display: flex;
justify-content: center;
padding: 8px 0;
} }
.combat-sidebar .encounter-gm-resources .gm-resource-controls { .combat-sidebar .encounter-controls.combat .encounter-control-fear-container {
display: flex; display: flex;
flex-direction: column; position: relative;
align-items: center; align-items: center;
padding: 0 4px;
justify-content: center; justify-content: center;
color: black;
} }
.combat-sidebar .encounter-gm-resources .gm-resource-tools { .combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice {
display: flex; height: 24px;
flex-direction: column;
justify-content: center;
padding: 0 5px 0 4px;
} }
.combat-sidebar .encounter-gm-resources .gm-resource-tools i { .combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear {
margin: 0 2px; position: absolute;
font-size: 16px; font-size: 16px;
} }
.combat-sidebar .encounter-gm-resources .gm-resource-tools i.disabled { .combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter {
opacity: 0.6; position: absolute;
right: -10px;
color: var(--color-text-secondary);
} }
.combat-sidebar .encounter-gm-resources .gm-resource-tools i:hover:not(.disabled) { .combat-sidebar .encounter-controls.combat .control-buttons {
cursor: pointer; width: min-content;
filter: drop-shadow(0 0 3px red);
} }
.combat-sidebar .encounter-gm-resources .gm-resource { .combat-sidebar .combatant-controls {
background: rgba(255, 255, 255, 0.1); flex: 0;
padding: 4px;
border-radius: 8px;
border: 2px solid black;
font-size: 20px;
} }
.combat-sidebar .token-action-tokens { .combat-sidebar .token-actions {
flex: 0 0 48px; align-self: stretch;
display: flex;
align-items: top;
justify-content: center;
gap: 16px;
}
.combat-sidebar .token-actions .action-tokens {
display: flex;
gap: 4px;
}
.combat-sidebar .token-actions .action-tokens .action-token {
height: 22px;
width: 22px;
border: 1px solid;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
padding: 8px;
--button-size: 0;
}
.combat-sidebar .token-actions .action-tokens .action-token.used {
opacity: 0.5;
background: transparent;
}
.combat-sidebar .token-actions button {
font-size: 22px;
height: 24px;
width: 24px;
}
.combat-sidebar .token-actions button.main {
background: var(--button-hover-background-color);
color: var(--button-hover-text-color);
border-color: var(--button-hover-border-color);
}
.combat-sidebar .token-actions button.main:hover {
filter: drop-shadow(0 0 3px var(--button-hover-text-color));
}
.combat-sidebar .spotlight-control {
font-size: 26px;
}
.combat-sidebar .spotlight-control:focus {
outline: none;
box-shadow: none;
}
.combat-sidebar .spotlight-control.discrete:hover {
background: inherit;
}
.combat-sidebar .spotlight-control.requesting {
filter: drop-shadow(0 0 3px gold);
color: var(--button-hover-text-color);
}
.combat-sidebar h4 {
margin: 0;
text-align: center; text-align: center;
} }
.combat-sidebar .token-action-tokens .use-action-token.disabled {
opacity: 0.6;
}
.combat-sidebar .icon-button.spaced {
margin-left: 4px;
}
.combat-sidebar .icon-button.disabled {
opacity: 0.6;
}
.combat-sidebar .icon-button:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px red);
}
.chat-message.duality { .chat-message.duality {
border-color: black; border-color: black;
padding: 8px 0 0 0; padding: 8px 0 0 0;
@ -2722,11 +2754,18 @@ div.daggerheart.views.multiclass {
--primary-color-fear: rgba(9, 71, 179, 0.75); --primary-color-fear: rgba(9, 71, 179, 0.75);
--secondary-color-fear: rgba(9, 71, 179, 0.75); --secondary-color-fear: rgba(9, 71, 179, 0.75);
--shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; --shadow-text-stroke: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
--fear-animation: background 0.3s ease, box-shadow .3s ease, border-color .3s ease, opacity .3s ease;
} }
#resources { #resources {
min-height: calc(var(--header-height) + 4rem); min-height: calc(var(--header-height) + 4rem);
min-width: 4rem; min-width: 4rem;
color: #d3d3d3; color: #d3d3d3;
transition: var(--fear-animation);
}
#resources header,
#resources .controls,
#resources .window-resize-handle {
transition: var(--fear-animation);
} }
#resources .window-content { #resources .window-content {
padding: 0.5rem; padding: 0.5rem;
@ -2822,7 +2861,7 @@ div.daggerheart.views.multiclass {
#resources:not(:hover):not(.minimized) header, #resources:not(:hover):not(.minimized) header,
#resources:not(:hover):not(.minimized) .controls, #resources:not(:hover):not(.minimized) .controls,
#resources:not(:hover):not(.minimized) .window-resize-handle { #resources:not(:hover):not(.minimized) .window-resize-handle {
visibility: hidden; opacity: 0;
} }
#resources:has(.fear-bar) { #resources:has(.fear-bar) {
min-width: 200px; min-width: 200px;
@ -2903,7 +2942,7 @@ div.daggerheart.views.multiclass {
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: swap;
src: url(https://fonts.gstatic.com/s/cinzeldecorative/v17/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype'); src: url(https://fonts.gstatic.com/s/cinzeldecorative/v18/daaHSScvJGqLYhG8nNt8KPPswUAPniZoaelD.ttf) format('truetype');
} }
@font-face { @font-face {
font-family: 'Montserrat'; font-family: 'Montserrat';
@ -3186,6 +3225,19 @@ div.daggerheart.views.multiclass {
.application.setting.dh-style footer button { .application.setting.dh-style footer button {
flex: 1; flex: 1;
} }
.application.setting.dh-style .form-group {
display: flex;
justify-content: space-between;
align-items: center;
}
.application.setting.dh-style .form-group label {
font-size: 16px;
}
.application.setting.dh-style .form-group .form-fields {
display: flex;
gap: 4px;
align-items: center;
}
.system-daggerheart .tagify { .system-daggerheart .tagify {
background: light-dark(transparent, transparent); background: light-dark(transparent, transparent);
border: 1px solid light-dark(#222, #efe6d8); border: 1px solid light-dark(#222, #efe6d8);

View file

@ -212,6 +212,22 @@
flex: 1; flex: 1;
} }
} }
.form-group {
display: flex;
justify-content: space-between;
align-items: center;
label {
font-size: 16px;
}
.form-fields {
display: flex;
gap: 4px;
align-items: center;
}
}
} }
.system-daggerheart { .system-daggerheart {

View file

@ -1,71 +1,106 @@
.combat-sidebar { .combat-sidebar {
.encounter-gm-resources { .encounter-controls.combat {
flex: 0; justify-content: space-between;
display: flex;
justify-content: center;
padding: @largePadding 0;
.gm-resource-controls { .encounter-control-fear-container {
display: flex; display: flex;
flex-direction: column; position: relative;
align-items: center; align-items: center;
padding: 0 4px;
justify-content: center; justify-content: center;
} color: black;
.gm-resource-tools { .dice {
display: flex; height: 24px;
flex-direction: column; }
justify-content: center;
padding: 0 5px 0 @fullPadding;
i { .encounter-control-fear {
margin: 0 @tinyMargin; position: absolute;
font-size: 16px; font-size: 16px;
}
&.disabled { .encounter-control-counter {
opacity: 0.6; position: absolute;
} right: -10px;
color: var(--color-text-secondary);
}
}
&:hover:not(.disabled) { .control-buttons {
cursor: pointer; width: min-content;
filter: drop-shadow(0 0 3px @mainShadow); }
}
.combatant-controls {
flex: 0;
}
.token-actions {
align-self: stretch;
display: flex;
align-items: top;
justify-content: center;
gap: 16px;
.action-tokens {
display: flex;
gap: 4px;
.action-token {
height: 22px;
width: 22px;
border: 1px solid;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
padding: 8px;
--button-size: 0;
&.used {
opacity: 0.5;
background: transparent;
} }
} }
} }
.gm-resource { button {
background: rgba(255, 255, 255, 0.1); font-size: 22px;
padding: @fullPadding; height: 24px;
border-radius: 8px; width: 24px;
border: @normalBorder solid black;
font-size: 20px; &.main {
background: var(--button-hover-background-color);
color: var(--button-hover-text-color);
border-color: var(--button-hover-border-color);
&:hover {
filter: drop-shadow(0 0 3px var(--button-hover-text-color));
}
}
} }
} }
.token-action-tokens { .spotlight-control {
flex: 0 0 48px; font-size: 26px;
&:focus {
outline: none;
box-shadow: none;
}
&.discrete:hover {
background: inherit;
}
&.requesting {
filter: drop-shadow(0 0 3px gold);
color: var(--button-hover-text-color);
}
}
h4 {
margin: 0;
text-align: center; text-align: center;
.use-action-token {
&.disabled {
opacity: 0.6;
}
}
}
.icon-button {
&.spaced {
margin-left: @halfMargin;
}
&.disabled {
opacity: 0.6;
}
&:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
}
} }
} }

View file

@ -0,0 +1,22 @@
<div>
<div class="form-group">
<label>{{localize "DAGGERHEART.Settings.Menu.VariantRules.actionTokens"}}</label>
<div class="form-fields">
{{formInput settingFields.schema.fields.actionTokens.fields.enabled value=settingFields._source.actionTokens.enabled}}
{{formInput settingFields.schema.fields.actionTokens.fields.tokens value=settingFields._source.actionTokens.tokens disabled=(not settingFields._source.actionTokens.enabled)}}
</div>
</div>
<footer class="form-footer">
<button data-action="reset">
<i class="fa-solid fa-arrow-rotate-left"></i>
<span>{{localize "Reset"}}</span>
</button>
<button data-action="save" >
<i class="fa-solid fa-floppy-disk"></i>
<span>{{localize "Save Changes"}}</span>
</button>
</footer>
</div>

View file

@ -0,0 +1,4 @@
<div>
{{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Character.Plural") turns=this.characters}}
{{> 'systems/daggerheart/templates/ui/combat/combatTrackerSection.hbs' this title=(localize "DAGGERHEART.General.Adversary.Plural") turns=this.adversaries}}
</div>

View file

@ -0,0 +1,17 @@
<nav class="combat-controls" data-tooltip-direction="UP">
{{~#if hasCombat~}}
{{#if user.isGM}}
{{#if combat.round}}
<button type="button" class="combat-control combat-control-lg" data-action="endCombat">
<i class="fa-solid fa-xmark" inert></i>
<span>{{ localize "COMBAT.End" }}</span>
</button>
{{else}}
<button type="button" class="combat-control combat-control-lg" data-action="startCombat">
<i class="fa-solid fa-swords" inert></i>
<span>{{ localize "COMBAT.Begin" }}</span>
</button>
{{/if}}
{{/if}}
{{/if}}
</nav>

View file

@ -0,0 +1,86 @@
<header class="combat-tracker-header">
{{!-- Encounter Controls --}}
{{#if user.isGM}}
<nav class="encounters {{ css }}" aria-label="{{ localize "COMBAT.NavLabel" }}">
{{!-- Cycle Display --}}
{{#if displayCycle}}
<button type="button" class="inline-control icon fa-solid fa-plus" data-action="createCombat"
data-tooltip aria-label="{{ localize "COMBAT.Create" }}"></button>
<div class="cycle-combats">
<button type="button" class="inline-control icon fa-solid fa-caret-left" data-action="cycleCombat"
{{#if previousId}}data-combat-id="{{ previousId }}" {{else}}disabled{{/if}}
data-tooltip aria-label="{{ localize "COMBAT.EncounterPrevious" }}"></button>
<div class="encounter-count">
<span class="value">{{ currentIndex }}</span>
<span class="separator">&sol;</span>
<span class="max">{{ combats.length }}</span>
</div>
<button type="button" class="inline-control icon fa-solid fa-caret-right" data-action="cycleCombat"
{{#if nextId}}data-combat-id="{{ nextId }}" {{else}}disabled{{/if}}
data-tooltip aria-label="{{ localize "COMBAT.EncounterNext" }}"></button>
</div>
<button type="button" class="inline-control icon fa-solid fa-gear" data-action="trackerSettings"
data-tooltip aria-label="{{ localize "COMBAT.Settings" }}"></button>
{{!-- Tabbed Display --}}
{{else if combats.length}}
<button type="button" class="inline-control icon fa-solid fa-plus" data-action="createCombat"
data-tooltip aria-label="{{ localize "COMBAT.Create" }}"></button>
{{#each combats}}
<button type="button" class="inline-control {{#if active}}active{{/if}}" data-action="cycleCombat"
data-combat-id="{{ id }}">
{{ label }}
</button>
{{/each}}
<button type="button" class="inline-control icon fa-solid fa-gear" data-action="trackerSettings"
data-tooltip aria-label="{{ localize "COMBAT.Settings" }}"></button>
{{!-- No Combats --}}
{{else}}
<button type="button" class="combat-control-lg" data-action="createCombat">
<i class="fa-solid fa-plus" inert></i>
<span>{{ localize "COMBAT.Create" }}</span>
</button>
{{/if}}
</nav>
{{/if}}
<div class="encounter-controls {{#if hasCombat}}combat{{/if}}">
{{#if hasCombat}}
<div class="encounter-control-fear-container">
<img class="dice " src="../icons/svg/d12-grey.svg"/>
<i class="fas fa-skull encounter-control-fear"></i>
<div class="encounter-control-counter">{{fear}}</div>
</div>
{{/if}}
{{!-- Combat Status --}}
<strong class="encounter-title">
{{#if combats.length}}
{{#if combat.round}}
{{ localize "DAGGERHEART.Combat.combatStarted" }}
{{else}}
{{ localize "COMBAT.NotStarted" }}
{{/if}}
{{else}}
{{ localize "COMBAT.None" }}
{{/if}}
</strong>
{{!-- Combat Controls --}}
{{#if hasCombat}}
<div class="control-buttons right flexrow">
<div class="spacer"></div>
<button type="button" class="encounter-context-menu inline-control combat-control icon fa-solid fa-ellipsis-vertical"
{{#unless (and user.isGM hasCombat)}}disabled{{/unless}}></button>
</div>
{{/if}}
</div>
</header>

View file

@ -0,0 +1,64 @@
<div>
<h4 class="divider">{{title}}</h4>
<ol class="combat-tracker plain">
{{#each turns}}
<li class="combatant {{ css }}" data-combatant-id="{{ id }}" data-action="activateCombatant">
{{!-- Image --}}
<img class="token-image" src="{{ img }}" alt="{{ name }}" loading="lazy">
{{!-- Name & Controls --}}
<div class="token-name">
<strong class="name">{{ name }}</strong>
<div class="flexrow">
<div class="combatant-controls flex-0">
{{#if @root.user.isGM}}
<button type="button" class="inline-control combatant-control icon fa-solid fa-eye-slash {{#if hidden}}active{{/if}}"
data-action="toggleHidden" data-tooltip aria-label="{{ localize "COMBAT.ToggleVis" }}"></button>
<button type="button" class="inline-control combatant-control icon fa-solid fa-skull {{#if isDefeated}}active{{/if}}"
data-action="toggleDefeated" data-tooltip
aria-label="{{ localize "COMBAT.ToggleDead" }}"></button>
{{/if}}
{{#if canPing}}
<button type="button" class="inline-control combatant-control icon fa-solid fa-bullseye-arrow"
data-action="pingCombatant" data-tooltip
aria-label="{{ localize "COMBAT.PingCombatant" }}"></button>
{{/if}}
{{#unless @root.user.isGM}}
<button type="button" class="inline-control combatant-control icon fa-solid fa-arrows-to-eye"
data-action="panToCombatant" data-tooltip
aria-label="{{ localize "COMBAT.PanToCombatant" }}"></button>
{{/unless}}
</div>
{{#if ../combat.round}}
<div class="token-actions">
{{#if isOwner}}
{{#if (and (not isNPC) ../actionTokens.enabled)}}
<div class="action-tokens">
{{#times ../actionTokens.tokens}}
<button type="button" class="inline-control main icon fa-solid fa-bolt-lightning action-token {{#if (lte ../system/actionTokens this)}}used{{/if}}" data-action="setActionTokens" data-combatant-id="{{../id}}" data-token-index="{{this}}">
</button>
{{/times}}
</div>
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
</div>
{{#if @root.user.isGM}}
<button
type="button" class="inline-control spotlight-control discrete icon fa-solid {{#if system.spotlight.requesting}}fa-hand-sparkles requesting{{else}}fa-hand{{/if}}"
data-action="toggleSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{localize "DAGGERHEART.Combat.giveSpotlight"}}"
></button>
{{else}}
<button
type="button" class="inline-control spotlight-control discrete icon fa-solid {{#if system.spotlight.requesting}}fa-hand-sparkles requesting{{else}}fa-hand{{/if}}"
data-action="requestSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{#if system.spotlight.requesting}}{{localize "DAGGERHEART.Combat.requestingSpotlight"}}{{else}}{{localize "DAGGERHEART.Combat.requestSpotlight"}}{{/if}}"
></button>
{{/if}}
</li>
{{/each}}
</ol>
</div>

View file

@ -1,160 +0,0 @@
<section class="{{cssClass}} directory flexcol" id="{{cssId}}" data-tab="{{tabName}}">
<header class="combat-tracker-header">
{{#if user.isGM}}
<nav class="encounters flexrow" aria-label="{{localize 'COMBAT.NavLabel'}}">
<a class="combat-button combat-create" aria-label="{{localize 'COMBAT.Create'}}" role="button" data-tooltip="COMBAT.Create">
<i class="fas fa-plus"></i>
</a>
{{#if combatCount}}
<a class="combat-button combat-cycle" aria-label="{{localize 'COMBAT.EncounterPrevious'}}" role="button" data-tooltip="COMBAT.EncounterPrevious"
{{#if previousId}}data-document-id="{{previousId}}"{{else}}disabled{{/if}}>
<i class="fas fa-caret-left"></i>
</a>
<h4 class="encounter">{{localize "COMBAT.Encounter"}} {{currentIndex}} / {{combatCount}}</h4>
<a class="combat-button combat-cycle" aria-label="{{localize 'COMBAT.EncounterNext'}}" role="button" data-tooltip="COMBAT.EncounterNext"
{{#if nextId}}data-document-id="{{nextId}}"{{else}}disabled{{/if}}>
<i class="fas fa-caret-right"></i>
</a>
{{/if}}
<a class="combat-button combat-control" aria-label="{{localize 'COMBAT.Delete'}}" role="button" data-tooltip="COMBAT.Delete" data-control="endCombat" {{#unless combatCount}}disabled{{/unless}}>
<i class="fas fa-trash"></i>
</a>
</nav>
{{/if}}
<div class="encounter-controls flexrow {{#if hasCombat}}combat{{/if}}">
{{#if user.isGM}}
<a class="combat-button combat-control" aria-label="{{localize 'COMBAT.RollAll'}}" role="button" data-tooltip="COMBAT.RollAll" data-control="rollAll" {{#unless turns}}disabled{{/unless}}>
<i class="fas fa-users"></i>
</a>
<a class="combat-button combat-control" aria-label="{{localize 'COMBAT.RollNPC'}}" role="button" data-tooltip="COMBAT.RollNPC" data-control="rollNPC" {{#unless turns}}disabled{{/unless}}>
<i class="fas fa-users-cog"></i>
</a>
{{/if}}
{{#if combatCount}}
{{#if combat.round}}
<h3 class="encounter-title noborder">{{localize 'COMBAT.Round'}} {{combat.round}}</h3>
{{else}}
<h3 class="encounter-title noborder">{{localize 'COMBAT.NotStarted'}}</h3>
{{/if}}
{{else}}
<h3 class="encounter-title noborder">{{localize "COMBAT.None"}}</h3>
{{/if}}
{{#if user.isGM}}
<a class="combat-button combat-control" aria-label="{{localize 'COMBAT.InitiativeReset'}}" role="button" data-tooltip="COMBAT.InitiativeReset" data-control="resetAll"
{{#unless hasCombat}}disabled{{/unless}}>
<i class="fas fa-undo"></i>
</a>
<a class="combat-button combat-control" aria-label="{{localize 'labels.scope'}}" role="button" data-tooltip="{{labels.scope}}"
data-control="toggleSceneLink" {{#unless hasCombat}}disabled{{/unless}}>
<i class="fas fa-{{#unless linked}}un{{/unless}}link"></i>
</a>
{{/if}}
<a class="combat-button combat-settings" aria-label="{{localize 'COMBAT.Settings'}}" role="button" data-tooltip="COMBAT.Settings" data-control="trackerSettings">
<i class="fas fa-cog"></i>
</a>
</div>
</header>
<div class="encounter-gm-resources">
{{#if combat.system}}
<div class="gm-resource-controls">
<i class="fa-solid fa-caret-up icon-button up" data-type="action"></i>
<i class="fa-solid fa-caret-down icon-button down {{#if (lte combat.system.actions 0)}}disabled{{/if}}" data-type="action"></i>
</div>
<div class="gm-resource">
<i class="fa-solid fa-hand-sparkles"></i>
<span>{{combat.system.actions}}</span>
</div>
<div class="gm-resource-tools">
<i class="fa-solid fa-arrow-right-long trade-actions {{#if (lt combat.system.actions 2)}}disabled{{/if}}"></i>
<i class="fa-solid fa-arrow-left-long trade-fear {{#if (lt fear 1)}}disabled{{/if}}"></i>
</div>
<div class="gm-resource">
<i class="fa-solid fa-skull"></i>
<span>{{fear}}</span>
</div>
<div class="gm-resource-controls">
<i class="fa-solid fa-caret-up icon-button up {{#if (gte fear 6)}}disabled{{/if}}" data-type="fear"></i>
<i class="fa-solid fa-caret-down icon-button down {{#if (lte fear 0)}}disabled{{/if}}" data-type="fear"></i>
</div>
{{/if}}
</div>
<ol id="combat-tracker" class="directory-list">
{{#each turns}}
<li class="combatant actor directory-item flexrow {{this.css}}" data-combatant-id="{{this.id}}">
<img class="token-image" data-src="{{this.img}}" alt="{{this.name}}"/>
<div class="token-name flexcol">
<h4>{{this.name}}</h4>
<div class="combatant-controls flexrow">
{{#if ../user.isGM}}
<a class="combatant-control {{#if this.hidden}}active{{/if}}" aria-label="{{localize 'COMBAT.ToggleVis'}}" role="button" data-tooltip="COMBAT.ToggleVis" data-control="toggleHidden">
<i class="fas fa-eye-slash"></i>
</a>
<a class="combatant-control {{#if this.defeated}}active{{/if}}" aria-label="{{localize 'COMBAT.ToggleDead'}}" role="button" data-tooltip="COMBAT.ToggleDead" data-control="toggleDefeated">
<i class="fas fa-skull"></i>
</a>
{{/if}}
{{#if this.canPing}}
<a class="combatant-control" aria-label="{{localize 'COMBAT.PingCombatant'}}" role="button" data-tooltip="COMBAT.PingCombatant" data-control="pingCombatant">
<i class="fa-solid fa-bullseye-arrow"></i>
</a>
{{/if}}
{{#unless ../user.isGM}}
<a class="combatant-control" aria-label="{{localize 'COMBAT.PanToCombatant'}}" role="button" data-tooltip="COMBAT.PanToCombatant" data-control="panToCombatant">
<i class="fa-solid fa-arrows-to-eye"></i>
</a>
{{/unless}}
<div class="token-effects">
{{#each this.effects}}
<img class="token-effect" src="{{this}}"/>
{{/each}}
</div>
</div>
</div>
{{#if this.hasResource}}
<div class="token-resource">
<span class="resource">{{this.resource}}</span>
</div>
{{/if}}
<div class="token-action-tokens">
{{#if this.playerCharacter}}
<i class="fa-solid fa-hand use-action-token" data-combatant="{{this.id}}"></i>
{{else if (and (not this.ownedByPlayer) ../user.isGM)}}
<i class="fa-solid fa-hand use-action-token {{#if (lt ../combat.system.actions 1)}}disabled{{/if}}" data-combatant="{{this.id}}"></i>
{{/if}}
{{!-- {{#if this.hasRolled}}
<span class="initiative">{{this.initiative}}</span>
{{else if this.owner}}
<a class="combatant-control roll" aria-label="{{localize 'COMBAT.InitiativeRoll'}}" role="button" data-tooltip="COMBAT.InitiativeRoll" data-control="rollInitiative"></a>
{{/if}} --}}
</div>
</li>
{{/each}}
</ol>
<nav id="combat-controls" class="directory-footer flexrow" data-tooltip-direction="UP">
{{#if hasCombat}}
{{#if user.isGM}}
{{#if round}}
<a class="combat-control" aria-label="{{localize 'COMBAT.RoundPrev'}}" role="button" data-tooltip="COMBAT.RoundPrev" data-control="previousRound"><i class="fas fa-step-backward"></i></a>
<a class="combat-control" aria-label="{{localize 'COMBAT.TurnPrev'}}" role="button" data-tooltip="COMBAT.TurnPrev" data-control="previousTurn"><i class="fas fa-arrow-left"></i></a>
<a class="combat-control center" aria-label="{{localize 'COMBAT.End'}}" role="button" data-control="endCombat">{{localize 'COMBAT.End'}}</a>
<a class="combat-control" aria-label="{{localize 'COMBAT.TurnNext'}}" role="button" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
<a class="combat-control" aria-label="{{localize 'COMBAT.RoundNext'}}" role="button" data-tooltip="COMBAT.RoundNext" data-control="nextRound"><i class="fas fa-step-forward"></i></a>
{{else}}
<a class="combat-control center" aria-label="{{localize 'COMBAT.Begin'}}" role="button" data-control="startCombat">{{localize 'COMBAT.Begin'}}</a>
{{/if}}
{{else if control}}
<a class="combat-control" aria-label="{{localize 'COMBAT.TurnPrev'}}" role="button" data-tooltip="COMBAT.TurnPrev" data-control="previousTurn"><i class="fas fa-arrow-left"></i></a>
<a class="combat-control center" aria-label="{{localize 'COMBAT.TurnEnd'}}" role="button" data-control="nextTurn">{{localize 'COMBAT.TurnEnd'}}</a>
<a class="combat-control" aria-label="{{localize 'COMBAT.TurnNext'}}" role="button" data-tooltip="COMBAT.TurnNext" data-control="nextTurn"><i class="fas fa-arrow-right"></i></a>
{{/if}}
{{/if}}
</nav>
</section>

View file

@ -1,35 +0,0 @@
<aside id="players" class="app daggerheart {{#if hide}}hidden{{/if}}">
<h3 aria-label="{{localize 'PLAYERS.Title'}}" role="button">
<div class="flex-centered">
<i class="fas fa-users"></i>
{{ localize "PLAYERS.Title" }}
{{#if showOffline}}
<i class="players-mode fas fa-angle-down"></i>
{{else}}
<i class="players-mode fas fa-angle-up"></i>
{{/if}}
</div>
<div class="players-container">
<i class="fa-solid fa-skull"></i>
<div>{{this.fear}}</div>
{{#if user.isGM}}
<div class="flexcol">
<i class="fa-solid fa-chevron-up fear-control up {{#if (gte this.fear 6)}}disabled{{/if}}"></i>
<i class="fa-solid fa-chevron-down fear-control down {{#if (lte this.fear 0)}}disabled{{/if}}"></i>
</div>
{{/if}}
</div>
</h3>
<ol aria-label="{{localize 'PLAYERS.List'}}" tabIndex="0" id="player-list">
{{#each users as |user|}}
<li class="player {{#if user.isGM}}gm{{/if}} flexrow" data-user-id="{{user._id}}">
<span class="player-active {{#if user.active}}active{{else}}inactive{{/if}}"
style="background: {{user.color}}; border: 1px solid {{user.border.css}}"></span>
<span class="player-name {{#if user.isSelf}}self{{/if}}" data-tooltip="{{user.displayName}}">
{{user.displayName}}
</span>
</li>
{{/each}}
</ol>
</aside>