feat: add color-coded movement ruler for tactical gameplay
This commit is contained in:
parent
9e4dcee3c3
commit
7faadf6583
8 changed files with 145 additions and 1 deletions
49
scss/components/shared/_ruler.scss
Normal file
49
scss/components/shared/_ruler.scss
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Alexander Minges
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Token Ruler Waypoint Styling with Color-Coded Movement Ranges */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ds4-movement-normal: inherit;
|
||||||
|
--ds4-movement-dash: #ffcc00;
|
||||||
|
--ds4-movement-impossible: #e83031;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-ds4 .waypoint-label {
|
||||||
|
.distance {
|
||||||
|
&.move-range {
|
||||||
|
color: var(--ds4-movement-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dash-range {
|
||||||
|
color: var(--ds4-movement-dash);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.out-of-range {
|
||||||
|
color: var(--ds4-movement-impossible);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delta {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elevation {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--ds4-movement-dash: #ffd700;
|
||||||
|
--ds4-movement-impossible: #ff4444;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
@import "components/shared/embedded_document_list";
|
@import "components/shared/embedded_document_list";
|
||||||
@import "components/shared/form_group";
|
@import "components/shared/form_group";
|
||||||
@import "components/shared/rollable_image";
|
@import "components/shared/rollable_image";
|
||||||
|
@import "components/shared/ruler";
|
||||||
@import "components/shared/sheet_body";
|
@import "components/shared/sheet_body";
|
||||||
@import "components/shared/sheet_form";
|
@import "components/shared/sheet_form";
|
||||||
@import "components/shared/sheet_tab_nav";
|
@import "components/shared/sheet_tab_nav";
|
||||||
|
|
47
src/apps/ruler/token-ruler.js
Normal file
47
src/apps/ruler/token-ruler.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// SPDX-FileCopyrightText: 2025 Alexander Minges
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DS4 Token Ruler implementation with color-coded movement ranges
|
||||||
|
* Based on actor movement combat value
|
||||||
|
*/
|
||||||
|
export class DS4TokenRuler extends foundry.canvas.placeables.tokens.TokenRuler {
|
||||||
|
|
||||||
|
static WAYPOINT_LABEL_TEMPLATE = "systems/ds4/templates/partials/waypoint-label.hbs";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhance waypoint label context with movement range information
|
||||||
|
* @param {object} waypoint - The waypoint data
|
||||||
|
* @param {object} state - The current ruler state
|
||||||
|
* @returns {object} Enhanced context with range class
|
||||||
|
*/
|
||||||
|
_getWaypointLabelContext(waypoint, state) {
|
||||||
|
const context = super._getWaypointLabelContext(waypoint, state);
|
||||||
|
|
||||||
|
// Only apply movement coloring for distance measurements in meters
|
||||||
|
if (context?.cost?.units === "m" || context?.distance?.units === "m") {
|
||||||
|
const movement = this.token?.actor?.system?.combatValues?.movement?.total;
|
||||||
|
|
||||||
|
if (movement) {
|
||||||
|
const total = Number(context.cost?.total || context.distance?.total || 0);
|
||||||
|
|
||||||
|
// DS4 movement rules:
|
||||||
|
// - Normal movement: up to movement value
|
||||||
|
// - Dash: up to 2x movement value (requires action)
|
||||||
|
// - Beyond 2x: impossible in single turn
|
||||||
|
if (total > 2 * movement) {
|
||||||
|
context.rangeClass = "out-of-range";
|
||||||
|
} else if (total <= movement) {
|
||||||
|
context.rangeClass = "move-range";
|
||||||
|
} else {
|
||||||
|
context.rangeClass = "dash-range";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
|
@ -492,7 +492,7 @@ export class DS4Actor extends Actor {
|
||||||
}),
|
}),
|
||||||
ok: {
|
ok: {
|
||||||
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
label: getGame().i18n.localize("DS4.GenericOkButton"),
|
||||||
callback: (_event, button, _dialog) => {
|
callback: (_event, button) => {
|
||||||
const selectedAttribute = button.form.elements[attributeIdentifier].value;
|
const selectedAttribute = button.form.elements[attributeIdentifier].value;
|
||||||
if (!isAttribute(selectedAttribute)) {
|
if (!isAttribute(selectedAttribute)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||||
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
|
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
|
||||||
|
// SPDX-FileCopyrightText: 2025 Alexander Minges
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ export async function registerHandlebarsPartials() {
|
||||||
"systems/ds4/templates/sheets/shared/components/add-button.hbs",
|
"systems/ds4/templates/sheets/shared/components/add-button.hbs",
|
||||||
"systems/ds4/templates/sheets/shared/components/control-button-group.hbs",
|
"systems/ds4/templates/sheets/shared/components/control-button-group.hbs",
|
||||||
"systems/ds4/templates/sheets/shared/components/rollable-image.hbs",
|
"systems/ds4/templates/sheets/shared/components/rollable-image.hbs",
|
||||||
|
"systems/ds4/templates/partials/waypoint-label.hbs",
|
||||||
];
|
];
|
||||||
await foundry.applications.handlebars.loadTemplates(templatePaths);
|
await foundry.applications.handlebars.loadTemplates(templatePaths);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { registerForReadyHook } from "./ready";
|
||||||
import { registerForRenderHooks } from "./render";
|
import { registerForRenderHooks } from "./render";
|
||||||
import { registerForSetupHook } from "./setup";
|
import { registerForSetupHook } from "./setup";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function registerForHooks(): void {
|
export function registerForHooks(): void {
|
||||||
registerForHotbarDropHook();
|
registerForHotbarDropHook();
|
||||||
registerForPreCreateItemHook();
|
registerForPreCreateItemHook();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
// SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||||
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
|
||||||
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
|
// SPDX-FileCopyrightText: 2021 Gesina Schwalbe
|
||||||
|
// SPDX-FileCopyrightText: 2025 Alexander Minges
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
@ -8,6 +9,7 @@ import { DS4ActiveEffectConfig } from "../apps/active-effect-config";
|
||||||
import { DS4CharacterActorSheet } from "../apps/actor/character-sheet";
|
import { DS4CharacterActorSheet } from "../apps/actor/character-sheet";
|
||||||
import { DS4CreatureActorSheet } from "../apps/actor/creature-sheet";
|
import { DS4CreatureActorSheet } from "../apps/actor/creature-sheet";
|
||||||
import { DS4ItemSheet } from "../apps/item-sheet";
|
import { DS4ItemSheet } from "../apps/item-sheet";
|
||||||
|
import { DS4TokenRuler } from "../apps/ruler/token-ruler";
|
||||||
import { DS4 } from "../config";
|
import { DS4 } from "../config";
|
||||||
import { DS4Check } from "../dice/check";
|
import { DS4Check } from "../dice/check";
|
||||||
import { createCheckRoll } from "../dice/check-factory";
|
import { createCheckRoll } from "../dice/check-factory";
|
||||||
|
@ -51,6 +53,8 @@ async function init() {
|
||||||
CONFIG.ChatMessage.documentClass = DS4ChatMessage;
|
CONFIG.ChatMessage.documentClass = DS4ChatMessage;
|
||||||
CONFIG.Token.documentClass = DS4TokenDocument;
|
CONFIG.Token.documentClass = DS4TokenDocument;
|
||||||
|
|
||||||
|
CONFIG.Token.rulerClass = DS4TokenRuler;
|
||||||
|
|
||||||
CONFIG.ActiveEffect.legacyTransferral = false;
|
CONFIG.ActiveEffect.legacyTransferral = false;
|
||||||
|
|
||||||
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
|
CONFIG.Actor.typeLabels = DS4.i18n.actorTypes;
|
||||||
|
|
39
templates/partials/waypoint-label.hbs
Normal file
39
templates/partials/waypoint-label.hbs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{{!--
|
||||||
|
SPDX-FileCopyrightText: 2025 Alexander Minges
|
||||||
|
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
--}}
|
||||||
|
|
||||||
|
{{!--
|
||||||
|
!-- Waypoint label template with color-coded movement ranges
|
||||||
|
!-- Based on DS4 movement combat values
|
||||||
|
--}}
|
||||||
|
<div class="waypoint-label {{rangeClass}}">
|
||||||
|
{{#if action.icon}}
|
||||||
|
<i class="{{action.icon}}"></i>
|
||||||
|
{{else if action.label}}
|
||||||
|
{{localize action.label}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if cost}}
|
||||||
|
<span class="distance {{rangeClass}}">{{cost.total}} {{cost.units}}</span>
|
||||||
|
{{#if cost.delta}}
|
||||||
|
<span class="delta">({{cost.delta}})</span>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<span class="distance {{rangeClass}}">{{distance.total}} {{units}}</span>
|
||||||
|
{{#if distance.delta}}
|
||||||
|
<span class="delta">({{distance.delta}})</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if (and elevation (not elevation.hidden))}}
|
||||||
|
<span class="elevation">
|
||||||
|
{{elevation.total}} {{units}}
|
||||||
|
{{#if elevation.delta}}
|
||||||
|
({{elevation.delta}})
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{#if secret}}
|
||||||
|
<i class="fas fa-eye-slash"></i>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
Loading…
Add table
Add a link
Reference in a new issue