This commit is contained in:
WBHarry 2025-06-06 16:39:17 +02:00
parent 6c147a5c00
commit 0f77697614
14 changed files with 589 additions and 343 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 documents from './module/documents/_module.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 { registerDHSettings } from './module/applications/settings.mjs';
import DhpChatLog from './module/ui/chatLog.mjs';
import DhpPlayers from './module/ui/players.mjs';
import DhpRuler from './module/ui/ruler.mjs';
import DhpTokenRuler from './module/ui/tokenRuler.mjs';
import { dualityRollEnricher } from './module/enrichers/DualityRollEnricher.mjs';
@ -65,11 +64,11 @@ Hooks.once('init', () => {
Actors.registerSheet(SYSTEM.id, applications.DhpEnvironment, { types: ['environment'], makeDefault: true });
CONFIG.Combat.dataModels = {
base: models.DhpCombat
base: models.DhCombat
};
CONFIG.Combatant.dataModels = {
base: models.DhpCombatant
base: models.DhCombatant
};
CONFIG.ChatMessage.dataModels = models.messages.config;
@ -77,7 +76,7 @@ Hooks.once('init', () => {
CONFIG.Canvas.rulerClass = DhpRuler;
CONFIG.Combat.documentClass = documents.DhpCombat;
CONFIG.ui.combat = DhpCombatTracker;
CONFIG.ui.combat = DhCombatTracker;
CONFIG.ui.chat = DhpChatLog;
// CONFIG.ui.players = DhpPlayers;
CONFIG.Token.rulerClass = DhpTokenRuler;
@ -97,8 +96,8 @@ Hooks.once('init', () => {
Hooks.on('ready', () => {
ui.resources = new CONFIG.ui.resources();
ui.resources.render({force: true});
})
ui.resources.render({ force: true });
});
Hooks.once('dicesoniceready', () => {});
@ -201,8 +200,8 @@ Hooks.on('chatMessage', (_, message) => {
const title = attributeValue
? game.i18n.format('DAGGERHEART.Chat.DualityRoll.AbilityCheckTitle', {
ability: game.i18n.localize(abilities[attributeValue].label)
})
ability: game.i18n.localize(abilities[attributeValue].label)
})
: game.i18n.localize('DAGGERHEART.General.Duality');
const hopeAndFearRoll = `1${rollCommand.hope ?? 'd12'}+1${rollCommand.fear ?? 'd12'}`;
@ -221,9 +220,9 @@ Hooks.on('chatMessage', (_, message) => {
roll,
attribute: attribute
? {
value: attribute.data.value,
label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : ``}${attribute.data.value}`
}
value: attribute.data.value,
label: `${game.i18n.localize(abilities[attributeValue].label)} ${attribute.data.value >= 0 ? `+` : ``}${attribute.data.value}`
}
: undefined,
title
});

View file

@ -329,6 +329,10 @@
"grimoire": "Grimoire"
}
},
"Combat": {
"takeSpotlight": "Take The Spotlight",
"combatStarted": "Active"
},
"LevelUp": {
"Tier1": {
"Label": "Level 2-4",

View file

@ -1,8 +1,8 @@
export { default as DhpPC } from './pc.mjs';
export { default as DhpCombat } from './combat.mjs';
export { default as DhpCombatant } from './combatant.mjs';
export { default as DhCombat } from './combat.mjs';
export { default as DhCombatant } from './combatant.mjs';
export { default as DhpAdversary } from './adversary.mjs';
export { default as DhpEnvironment } from './environment.mjs';
export * as items from "./item/_module.mjs";
export * as messages from "./chat-message/_modules.mjs";
export * as items from './item/_module.mjs';
export * as messages from './chat-message/_modules.mjs';

View file

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

View file

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

View file

@ -1,44 +1,53 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
export default class DhpCombat extends Combat {
_sortCombatants(a, b) {
if (a.isNPC !== b.isNPC) {
const aVal = a.isNPC ? 0 : 1;
const bVal = b.isNPC ? 0 : 1;
get combatant() {
return this.combatants.contents.find(x => x.system.active) ?? null;
}
return aVal - bVal;
async startCombat() {
this._playCombatSound('startEncounter');
const updateData = { 'system.started': true };
Hooks.callAll('combatStart', this, updateData);
await this.update(updateData);
return this;
}
_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);
}
async useActionToken(combatantId) {
const automateActionPoints = await game.settings.get(
SYSTEM.id,
SYSTEM.SETTINGS.gameSettings.Automation.ActionPoints
);
// 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;
// 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 };
// 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 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
}
});
}
}
// await game.socket.emit(`system.${SYSTEM.id}`, {
// action: socketEvent.GMUpdate,
// data: {
// action: GMUpdateEvent.UpdateDocument,
// uuid: this.uuid,
// update: update
// }
// });
// }
// }
}

View file

@ -1,199 +1,45 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
constructor(data, context) {
super(data, context);
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 });
export default class DhCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
static DEFAULT_OPTIONS = {
actions: {
takeSpotlight: this.takeSpotlight
}
}
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) {
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate);
static PARTS = {
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);
_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 takeSpotlight(_, target) {
const { combatantId } = target.closest('[data-combatant-id]')?.dataset ?? {};
for (var combatant of this.viewed.combatants) {
await combatant.update({ 'system.active': combatantId === combatant.id ? true : false });
}
this.render();
}
}

View file

@ -0,0 +1,199 @@
import { GMUpdateEvent, socketEvent } from '../helpers/socket.mjs';
export default class DhpCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
constructor(data, context) {
super(data, context);
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) {
Hooks.off(socketEvent.DhpFearUpdate, this.onFearUpdate);
return super.close(options);
}
}

View file

@ -1293,59 +1293,44 @@
.daggerheart.sheet.pc div[data-application-part] .sheet-body .inventory-container .inventory-item-list .inventory-row img {
width: 32px;
}
.combat-sidebar .encounter-gm-resources {
flex: 0;
display: flex;
justify-content: center;
padding: 8px 0;
.combat-sidebar .encounter-controls.combat {
justify-content: space-between;
}
.combat-sidebar .encounter-gm-resources .gm-resource-controls {
.combat-sidebar .encounter-controls.combat .encounter-control-fear-container {
display: flex;
position: relative;
align-items: center;
justify-content: center;
color: black;
}
.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .dice {
height: 24px;
}
.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-fear {
position: absolute;
font-size: 16px;
}
.combat-sidebar .encounter-controls.combat .encounter-control-fear-container .encounter-control-counter {
position: absolute;
right: -10px;
color: var(--color-text-secondary);
}
.combat-sidebar .encounter-controls.combat .control-buttons {
width: min-content;
}
.combat-sidebar .token-actions {
flex: 0 0 var(--input-height);
align-self: stretch;
display: flex;
flex-direction: column;
align-items: center;
padding: 0 4px;
justify-content: center;
}
.combat-sidebar .encounter-gm-resources .gm-resource-tools {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 5px 0 4px;
.combat-sidebar .token-actions button {
font-size: 22px;
}
.combat-sidebar .encounter-gm-resources .gm-resource-tools i {
margin: 0 2px;
font-size: 16px;
}
.combat-sidebar .encounter-gm-resources .gm-resource-tools i.disabled {
opacity: 0.6;
}
.combat-sidebar .encounter-gm-resources .gm-resource-tools i:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px red);
}
.combat-sidebar .encounter-gm-resources .gm-resource {
background: rgba(255, 255, 255, 0.1);
padding: 4px;
border-radius: 8px;
border: 2px solid black;
font-size: 20px;
}
.combat-sidebar .token-action-tokens {
flex: 0 0 48px;
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);
.combat-sidebar .token-actions button:hover {
background: inherit;
}
.chat-message.duality {
border-color: black;
@ -2903,7 +2888,7 @@ div.daggerheart.views.multiclass {
font-style: normal;
font-weight: 700;
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-family: 'Montserrat';

View file

@ -1,71 +1,118 @@
.combat-sidebar {
.encounter-gm-resources {
flex: 0;
display: flex;
justify-content: center;
padding: @largePadding 0;
// .encounter-gm-resources {
// flex: 0;
// display: flex;
// justify-content: center;
// padding: @largePadding 0;
.gm-resource-controls {
// .gm-resource-controls {
// display: flex;
// flex-direction: column;
// align-items: center;
// padding: 0 4px;
// justify-content: center;
// }
// .gm-resource-tools {
// display: flex;
// flex-direction: column;
// justify-content: center;
// padding: 0 5px 0 @fullPadding;
// i {
// margin: 0 @tinyMargin;
// font-size: 16px;
// &.disabled {
// opacity: 0.6;
// }
// &:hover:not(.disabled) {
// cursor: pointer;
// filter: drop-shadow(0 0 3px @mainShadow);
// }
// }
// }
// .gm-resource {
// background: rgba(255, 255, 255, 0.1);
// padding: @fullPadding;
// border-radius: 8px;
// border: @normalBorder solid black;
// font-size: 20px;
// }
// }
// .token-action-tokens {
// flex: 0 0 48px;
// 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);
// }
// }
.encounter-controls.combat {
justify-content: space-between;
.encounter-control-fear-container {
display: flex;
flex-direction: column;
position: relative;
align-items: center;
padding: 0 4px;
justify-content: center;
}
color: black;
.gm-resource-tools {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 5px 0 @fullPadding;
.dice {
height: 24px;
}
i {
margin: 0 @tinyMargin;
.encounter-control-fear {
position: absolute;
font-size: 16px;
}
&.disabled {
opacity: 0.6;
}
&:hover:not(.disabled) {
cursor: pointer;
filter: drop-shadow(0 0 3px @mainShadow);
}
.encounter-control-counter {
position: absolute;
right: -10px;
color: var(--color-text-secondary);
}
}
.gm-resource {
background: rgba(255, 255, 255, 0.1);
padding: @fullPadding;
border-radius: 8px;
border: @normalBorder solid black;
font-size: 20px;
.control-buttons {
width: min-content;
}
}
.token-action-tokens {
flex: 0 0 48px;
text-align: center;
.token-actions {
flex: 0 0 var(--input-height);
align-self: stretch;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.use-action-token {
&.disabled {
opacity: 0.6;
button {
font-size: 22px;
&:hover {
background: inherit;
}
}
}
.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,53 @@
<ol class="combat-tracker plain">
{{#each turns}}
<li class="combatant {{ css }}" data-combatant-id="{{ id }}" data-action="activateCombatant">
{{!-- TODO: Targets --}}
{{!-- Image --}}
<img class="token-image" src="{{ img }}" alt="{{ name }}" loading="lazy">
{{!-- Name & Controls --}}
<div class="token-name">
<strong class="name">{{ name }}</strong>
<div class="combatant-controls">
{{#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}}
{{!-- TODO: Target Control --}}
<div class="token-effects" data-tooltip-html="{{ effects.tooltip }}">
{{#each effects.icons}}
<img class="token-effect" src="{{ img }}" alt="{{ name }}">
{{/each}}
</div>
</div>
</div>
{{!-- Resource --}}
{{#if resource includeZero=true}}
<div class="token-resource">
<span class="resource">{{ resource }}</span>
</div>
{{/if}}
{{#if ../combat.system.started}}
<div class="token-actions">
<button type="button" class="inline-control combatant-control icon fa-solid fa-hand" data-action="takeSpotlight" data-combatant-id="{{id}}" data-tooltip aria-label="{{ localize "DAGGERHEART.Combat.takeSpotlight" }}"></button>
</div>
{{/if}}
</li>
{{/each}}
</ol>

View file

@ -0,0 +1,17 @@
<nav class="combat-controls" data-tooltip-direction="UP">
{{~#if hasCombat~}}
{{#if user.isGM}}
{{#if combat.system.started}}
<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">4</div>
</div>
{{/if}}
{{!-- Combat Status --}}
<strong class="encounter-title">
{{#if combats.length}}
{{#if combat.system.started}}
{{ 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>