Merge branch 'tactical_range_bands_dev' into 'dev'
Tactical Grid Range Band See merge request teaml5r/l5r5e!49
This commit is contained in:
@@ -837,6 +837,33 @@
|
|||||||
"the_scroll_or_the_blade": "The Scroll or the Blade",
|
"the_scroll_or_the_blade": "The Scroll or the Blade",
|
||||||
"legacies_of_war": "Legacies of War",
|
"legacies_of_war": "Legacies of War",
|
||||||
"children_of_the_five_winds": "Children of the Five Winds"
|
"children_of_the_five_winds": "Children of the Five Winds"
|
||||||
|
},
|
||||||
|
"tactical_grid": {
|
||||||
|
"settings": {
|
||||||
|
"title": "Tactical Grid Settings",
|
||||||
|
"label": "Tactical Grid Settings",
|
||||||
|
"hint":"Configures tactical grid range band distances (GM only) and their visual appearance colors and transparency (all users).",
|
||||||
|
"cells": "spaces",
|
||||||
|
"world": {
|
||||||
|
"enabled": "Enable Tactical Grid",
|
||||||
|
"enabled_hint": "Enables or Disable tactical grid for everyone",
|
||||||
|
"start": "Start"
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"color": "Color",
|
||||||
|
"alpha": "Alpha"
|
||||||
|
},
|
||||||
|
"range": "Range {index}",
|
||||||
|
"validate": {
|
||||||
|
"start-too-small": "Must be greater than Range Band {previousRangeIndex} ({previousStart})",
|
||||||
|
"start-too-large": "Must be lower then Range Band {nextRangeIndex} ({nextStart})"
|
||||||
|
},
|
||||||
|
"reset": "Reset to Default",
|
||||||
|
"submit": "Save"
|
||||||
|
},
|
||||||
|
"range_band": "Range Band {band}",
|
||||||
|
"range": "Range {range}",
|
||||||
|
"range_abbriviation": "RB {range}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ActorL5r5e } from "./actor.js";
|
|||||||
import { CharacterSheetL5r5e } from "./actors/character-sheet.js";
|
import { CharacterSheetL5r5e } from "./actors/character-sheet.js";
|
||||||
import { NpcSheetL5r5e } from "./actors/npc-sheet.js";
|
import { NpcSheetL5r5e } from "./actors/npc-sheet.js";
|
||||||
import { ArmySheetL5r5e } from "./actors/army-sheet.js";
|
import { ArmySheetL5r5e } from "./actors/army-sheet.js";
|
||||||
|
import { RulerL5r5e, TokenRulerL5r5e } from "./tatical-grid-rulers.js";
|
||||||
// Dice and rolls
|
// Dice and rolls
|
||||||
import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js";
|
import { L5rBaseDie } from "./dice/dietype/l5r-base-die.js";
|
||||||
import { AbilityDie } from "./dice/dietype/ability-die.js";
|
import { AbilityDie } from "./dice/dietype/ability-die.js";
|
||||||
@@ -72,6 +73,8 @@ Hooks.once("init", async () => {
|
|||||||
CONFIG.Item.documentClass = ItemL5r5e;
|
CONFIG.Item.documentClass = ItemL5r5e;
|
||||||
CONFIG.JournalEntry.documentClass = JournalL5r5e;
|
CONFIG.JournalEntry.documentClass = JournalL5r5e;
|
||||||
CONFIG.JournalEntry.sheetClass = BaseJournalSheetL5r5e;
|
CONFIG.JournalEntry.sheetClass = BaseJournalSheetL5r5e;
|
||||||
|
CONFIG.Token.rulerClass = TokenRulerL5r5e;
|
||||||
|
CONFIG.Canvas.rulerClass = RulerL5r5e;
|
||||||
|
|
||||||
// Define custom Roll class
|
// Define custom Roll class
|
||||||
CONFIG.Dice.rolls.unshift(RollL5r5e);
|
CONFIG.Dice.rolls.unshift(RollL5r5e);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { L5r5eSetField } from "./data/l5r5e-setfield.js";
|
import { L5r5eSetField } from "./data/l5r5e-setfield.js";
|
||||||
|
import { TacticalGridSettingsL5R5E } from "./settings/tactical-grid-settings.js"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom system settings register
|
* Custom system settings register
|
||||||
@@ -236,4 +237,29 @@ export const RegisterSettings = function () {
|
|||||||
default: [],
|
default: [],
|
||||||
onChange: () => game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor"),
|
onChange: () => game.l5r5e.HelpersL5r5e.refreshLocalAndSocket("l5r5e-gm-monitor"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* -------------------------------------- */
|
||||||
|
/* Grid Settings (GM only) */
|
||||||
|
/* -------------------------------------- */
|
||||||
|
|
||||||
|
// UI Configuration
|
||||||
|
game.settings.register(CONFIG.l5r5e.namespace, "tactical-grid-settings-world", {
|
||||||
|
scope: "world",
|
||||||
|
config: false,
|
||||||
|
type: TacticalGridSettingsL5R5E.worldSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.register(CONFIG.l5r5e.namespace, "tactical-grid-settings-client", {
|
||||||
|
scope: "client",
|
||||||
|
config: false,
|
||||||
|
type: TacticalGridSettingsL5R5E.clientSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.registerMenu(CONFIG.l5r5e.namespace, "tactical-grid-settings", {
|
||||||
|
name: "l5r5e.tactical_grid.settings.title",
|
||||||
|
label: "l5r5e.tactical_grid.settings.label",
|
||||||
|
hint: "l5r5e.tactical_grid.settings.hint",
|
||||||
|
icon: "fa-solid fa-table-layout",
|
||||||
|
type: TacticalGridSettingsL5R5E
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
351
system/scripts/settings/tactical-grid-settings.js
Normal file
351
system/scripts/settings/tactical-grid-settings.js
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
const HandlebarsApplicationMixin = foundry.applications.api.HandlebarsApplicationMixin;
|
||||||
|
const ApplicationV2 = foundry.applications.api.ApplicationV2;
|
||||||
|
const fields = foundry.data.fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @typedef {Object} RangeBand
|
||||||
|
* @property {number} start
|
||||||
|
*
|
||||||
|
* @typedef {Object} ClientRangeBand
|
||||||
|
* @property {string} color
|
||||||
|
* @property {number} alpha
|
||||||
|
*
|
||||||
|
* @typedef {Object} WorldSettings
|
||||||
|
* @property {boolean} enabled
|
||||||
|
* @property {Record<number, RangeBand>} ranges - Indexed 0-6
|
||||||
|
*
|
||||||
|
* @typedef {Object} ClientSettings
|
||||||
|
* @property {Record<number, ClientRangeBand>} ranges - Indexed 0-6
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class TacticalGridSettingsL5R5E extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
|
||||||
|
/** @inheritDoc */
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
id: "tactical-grid-settings",
|
||||||
|
tag: "form",
|
||||||
|
classes: [""], // We could add l5r here but that would add styling that is not matching the default settings menu
|
||||||
|
window: {
|
||||||
|
title: "l5r5e.tactical_grid.settings.title",
|
||||||
|
contentClasses: ["standard-form"]
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
closeOnSubmit: true,
|
||||||
|
handler: TacticalGridSettingsL5R5E.#onSubmit
|
||||||
|
},
|
||||||
|
position: { width: 540 },
|
||||||
|
actions: {
|
||||||
|
reset: TacticalGridSettingsL5R5E.#onReset
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
static PARTS = {
|
||||||
|
form: {
|
||||||
|
template: "systems/l5r5e/templates/" + "settings/tactical-grid-settings.html",
|
||||||
|
scrollable: [""],
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
template: "templates/generic/form-footer.hbs"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SchemaField defining a world range band.
|
||||||
|
* @param {{start: number}} initial - Initial range values.
|
||||||
|
* `start` must be ≥ 0.
|
||||||
|
* * @returns {SchemaField} A schema field containing a 'start' field
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #createWorldRangeBandSchema(initial) {
|
||||||
|
return new fields.SchemaField({
|
||||||
|
start: new fields.NumberField({ initial: initial.start, label: "l5r5e.tactical_grid.settings.world.start", min: 0, max:Infinity, nullable: false, required: true, gmOnly: true})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SchemaField defining a client range band.
|
||||||
|
* @param {{color: string, alpha: number}} initial - Initial range band values.
|
||||||
|
* `color` should be a valid CSS color string and `alpha` a valid alpha value.
|
||||||
|
* @returns {SchemaField} A schema field containing `color` and `alpha` fields.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #createClientRangeBandSchema(initial) {
|
||||||
|
return new fields.SchemaField({
|
||||||
|
color: new fields.ColorField({initial: initial.color, label: "l5r5e.tactical_grid.settings.client.color", required: true}),
|
||||||
|
alpha: new fields.AlphaField({initial: initial.alpha, label: "l5r5e.tactical_grid.settings.client.alpha", required: true}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined Foundry VTT settings schema representing both:
|
||||||
|
* - **World (GM-controlled)** configuration
|
||||||
|
* - **Client (per-user)** visual configuration
|
||||||
|
*
|
||||||
|
* This variable serves as a single source-of-truth definition for the module’s
|
||||||
|
* tactical grid settings structure, including field types, defaults, labels, and
|
||||||
|
* validation rules for world ranges.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #schema = {
|
||||||
|
world: new fields.SchemaField({
|
||||||
|
enabled: new fields.BooleanField({ initial: true, label: "l5r5e.tactical_grid.settings.world.enabled", hint: "l5r5e.tactical_grid.settings.world.enabled_hint"}),
|
||||||
|
ranges: new fields.SchemaField({
|
||||||
|
0: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 0}),
|
||||||
|
1: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 1}),
|
||||||
|
2: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 2}),
|
||||||
|
3: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 3}),
|
||||||
|
4: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 6}),
|
||||||
|
5: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 10}),
|
||||||
|
6: TacticalGridSettingsL5R5E.#createWorldRangeBandSchema({ start: 15})
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
validate: TacticalGridSettingsL5R5E.#validateWorldRangeConfiguration
|
||||||
|
}),
|
||||||
|
client: new fields.SchemaField({
|
||||||
|
ranges: new fields.SchemaField({
|
||||||
|
0: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#00FFFF", alpha: 0.5}),
|
||||||
|
1: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FF00FF", alpha: 0.5}),
|
||||||
|
2: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FFFF00", alpha: 0.5}),
|
||||||
|
3: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#0000FF", alpha: 0.5}),
|
||||||
|
4: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#7FFF00", alpha: 0.5}),
|
||||||
|
5: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#4B0082", alpha: 0.5}),
|
||||||
|
6: TacticalGridSettingsL5R5E.#createClientRangeBandSchema({color: "#FF8800", alpha: 0.5})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the **world (GM-controlled)** portion of the tactical grid settings schema.
|
||||||
|
* @return {SchemaField}
|
||||||
|
*/
|
||||||
|
static get worldSchema() {
|
||||||
|
return TacticalGridSettingsL5R5E.#schema.world;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the **client (per-user visual)** portion of the tactical grid settings schema.
|
||||||
|
* @return {SchemaField}
|
||||||
|
*/
|
||||||
|
static get clientSchema() {
|
||||||
|
return TacticalGridSettingsL5R5E.#schema.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Holds a mutable copy of the tactical grid settings so the form can operate on current values without altering the schema. */
|
||||||
|
static #setting = null;
|
||||||
|
|
||||||
|
|
||||||
|
/** @override ApplicationV2 */
|
||||||
|
async _prepareContext(options) {
|
||||||
|
if (options.isFirstRender) {
|
||||||
|
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||||
|
const world = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||||
|
TacticalGridSettingsL5R5E.#setting = foundry.utils.deepClone({client: client, world: world});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-process range bands for easier template access
|
||||||
|
const rangeBands = Object.entries(this.constructor.worldSchema.fields.ranges.fields).map(([index, field]) => ({
|
||||||
|
index: Number(index),
|
||||||
|
worldField: field,
|
||||||
|
clientFields: this.constructor.clientSchema.fields.ranges.fields[index],
|
||||||
|
worldValue: TacticalGridSettingsL5R5E.#setting.world.ranges[index],
|
||||||
|
clientValue: TacticalGridSettingsL5R5E.#setting.client.ranges[index]
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
isGm: game.user.isGM,
|
||||||
|
tactical_grid_enabled: {
|
||||||
|
field: this.constructor.worldSchema.fields.enabled,
|
||||||
|
value: TacticalGridSettingsL5R5E.#setting.world.enabled,
|
||||||
|
},
|
||||||
|
rangeBands,
|
||||||
|
buttons: [
|
||||||
|
{ type: "reset", label: "l5r5e.tactical_grid.settings.reset", icon: "fa-solid fa-arrow-rotate-left", action: "reset" },
|
||||||
|
{ type: "submit", label: "l5r5e.tactical_grid.settings.submit", icon: "fa-solid fa-floppy-disk" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** @override ApplicationV2 */
|
||||||
|
_onChangeForm(formConfig, event) {
|
||||||
|
const formData = new foundry.applications.ux.FormDataExtended(this.form, { readonly: true });
|
||||||
|
|
||||||
|
const {
|
||||||
|
cleaned: cleanedWorldSettings,
|
||||||
|
failure: validationFailures
|
||||||
|
} = TacticalGridSettingsL5R5E.#validateAndCleanWorldSettings(formData, event);
|
||||||
|
TacticalGridSettingsL5R5E.#applyValidationState(validationFailures);
|
||||||
|
|
||||||
|
TacticalGridSettingsL5R5E.#setting.world = cleanedWorldSettings
|
||||||
|
TacticalGridSettingsL5R5E.#setting.client = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-client"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates world schema ensuring range bands are properly ordered and connected.
|
||||||
|
* Note: internal field validation takes precedence, and will result in this validation potentially not running
|
||||||
|
* Checks that:
|
||||||
|
* - Sequential range bands connect properly (range[n].start < range[n+1].start)
|
||||||
|
* @param {*} value - The world settings object to validate
|
||||||
|
* @param {DataFieldValidationOptions} options - Validation options including lastElementChange
|
||||||
|
* @returns {boolean|foundry.data.validation.DataModelValidationFailure} True if valid, otherwise validation failure object
|
||||||
|
*/
|
||||||
|
static #validateWorldRangeConfiguration(value, options) {
|
||||||
|
if(!value.enabled) // don't validate if tactical_grids are disabled
|
||||||
|
return true;
|
||||||
|
|
||||||
|
let previousStart = -1;
|
||||||
|
let previousRangeIndex = null;
|
||||||
|
const failure = new foundry.data.validation.DataModelValidationFailure({ unresolved: true });
|
||||||
|
const changedElementName = options?.element?.name;
|
||||||
|
|
||||||
|
for (const [rangeIndex, range] of Object.entries(value.ranges)) {
|
||||||
|
if (range.start <= previousStart) {
|
||||||
|
let errorKey = TacticalGridSettingsL5R5E.worldSchema.fields.ranges.fields[rangeIndex].fields.start.fieldPath;
|
||||||
|
const previousErrorKey = errorKey.replace(/\.(\d+)\./, `.${previousRangeIndex}.`);
|
||||||
|
|
||||||
|
let isErrorOnPrevious = false;
|
||||||
|
|
||||||
|
// If the previous field was changed, show error there instead
|
||||||
|
if (changedElementName === previousErrorKey) {
|
||||||
|
errorKey = previousErrorKey;
|
||||||
|
isErrorOnPrevious = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
failure.fields[errorKey] = new foundry.data.validation.DataModelValidationFailure({
|
||||||
|
invalidValue: isErrorOnPrevious ? previousStart : range.start,
|
||||||
|
unresolved: true,
|
||||||
|
message: game.i18n.format(
|
||||||
|
isErrorOnPrevious
|
||||||
|
? "l5r5e.tactical_grid.settings.validate.start-too-large"
|
||||||
|
: "l5r5e.tactical_grid.settings.validate.start-too-small",
|
||||||
|
isErrorOnPrevious
|
||||||
|
? { nextRangeIndex: Number(rangeIndex), nextStart: range.start }
|
||||||
|
: { previousRangeIndex: Number(previousRangeIndex), previousStart: previousStart }
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
previousStart = range.start;
|
||||||
|
previousRangeIndex = rangeIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(failure.fields).length > 0 ? failure : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and cleans the world portion of the tactical grid settings.
|
||||||
|
*
|
||||||
|
* Expands raw form data, validates it against the schema, updates form input
|
||||||
|
* error states and tooltips, and returns a cleaned object ready to save.
|
||||||
|
*
|
||||||
|
* @param {foundry.applications.ux.FormDataExtended} formData - The submitted form data.
|
||||||
|
* @param {Event} [event] - Optional event for determining which field changed.
|
||||||
|
* @returns {WorldSettings} A cleaned and validated copy of the world settings.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #validateAndCleanWorldSettings(formData, event) {
|
||||||
|
const expanded = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-world"];
|
||||||
|
const validate = TacticalGridSettingsL5R5E.#schema.world.validate(expanded, { element: event.target});
|
||||||
|
|
||||||
|
// validation from Number etc. itself has the error key just as "ranges.0.start"
|
||||||
|
// so fixing that here so that we can directly reference they html elements
|
||||||
|
const prefix = "l5r5e.tactical-grid-settings-world.";
|
||||||
|
const failures = Object.fromEntries(
|
||||||
|
Object.entries(validate?.asError()?.getAllFailures() ?? {}).map(([key, value]) => [
|
||||||
|
key.startsWith(prefix) ? key : `${prefix}${key}`,
|
||||||
|
value
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return cleaned schema so that we have something that is somewhat correct we can save
|
||||||
|
return {
|
||||||
|
cleaned: TacticalGridSettingsL5R5E.#schema.world.clean(expanded),
|
||||||
|
failure: failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a validation message to a form element.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element - The element to apply validation to
|
||||||
|
* @param {string|null} message - The validation message, or null to clear
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #applyValidationMessage(element, message) {
|
||||||
|
if (message) {
|
||||||
|
element.setCustomValidity(message);
|
||||||
|
element.dataset.tooltip = message;
|
||||||
|
element.ariaLabel = game.i18n.localize(element.dataset.tooltip);
|
||||||
|
game.tooltip.activate(element, {
|
||||||
|
direction: foundry.CONFIG.ux.TooltipManager.TOOLTIP_DIRECTIONS.RIGHT,
|
||||||
|
locked: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
element?.setCustomValidity("");
|
||||||
|
delete element?.dataset?.tooltip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies validation state to all range band start fields.
|
||||||
|
*
|
||||||
|
* @param {Object} failures - Validation failures keyed by field name
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static #applyValidationState(failures) {
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const name = `l5r5e.tactical-grid-settings-world.ranges.${i}.start`;
|
||||||
|
this.#applyValidationMessage(
|
||||||
|
document.getElementsByName(name)[0],
|
||||||
|
failures?.[name]?.message || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles form submission.
|
||||||
|
*
|
||||||
|
* @param {Event} event - The submission event
|
||||||
|
* @param {HTMLFormElement} form - The form element
|
||||||
|
* @param {foundry.applications.ux.FormDataExtended} formData - The submitted form data
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static async #onSubmit(event, form, formData) {
|
||||||
|
const {
|
||||||
|
cleaned: cleanedWorldSettings,
|
||||||
|
failure: validationFailures
|
||||||
|
} = TacticalGridSettingsL5R5E.#validateAndCleanWorldSettings(formData, event);
|
||||||
|
TacticalGridSettingsL5R5E.#applyValidationState(validationFailures);
|
||||||
|
|
||||||
|
TacticalGridSettingsL5R5E.#setting.world = cleanedWorldSettings;
|
||||||
|
TacticalGridSettingsL5R5E.#setting.client = foundry.utils.expandObject(formData.object).l5r5e["tactical-grid-settings-client"];
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
promises.push(game.settings.set(CONFIG.l5r5e.namespace, "tactical-grid-settings-world", TacticalGridSettingsL5R5E.#setting.world));
|
||||||
|
promises.push(game.settings.set(CONFIG.l5r5e.namespace, "tactical-grid-settings-client", TacticalGridSettingsL5R5E.#setting.client));
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles reset action to restore default settings.
|
||||||
|
*
|
||||||
|
* @param {Event} event - The reset event
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
static async #onReset(event) {
|
||||||
|
const client = TacticalGridSettingsL5R5E.clientSchema.clean();
|
||||||
|
const world = TacticalGridSettingsL5R5E.worldSchema.clean();
|
||||||
|
TacticalGridSettingsL5R5E.#setting = foundry.utils.deepClone({client: client, world: world});
|
||||||
|
|
||||||
|
await this.render({ force: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
81
system/scripts/tatical-grid-rulers.js
Normal file
81
system/scripts/tatical-grid-rulers.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
function getRangeband(gridSettings, distance) {
|
||||||
|
const entries = Object.entries(gridSettings.ranges);
|
||||||
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
|
const [range, { start }] = entries[i];
|
||||||
|
if (distance >= start) {
|
||||||
|
return Number(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RulerL5r5e extends foundry.canvas.interaction.Ruler {
|
||||||
|
|
||||||
|
static WAYPOINT_LABEL_TEMPLATE = "systems/l5r5e/templates/" + "hud/tactical-grid-ruler.html"
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_getWaypointLabelContext(waypoint, state) {
|
||||||
|
const context = super._getWaypointLabelContext(waypoint, state);
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||||
|
if(gridSettings.enabled) {
|
||||||
|
const diagonalCost = game.canvas.grid.distance * waypoint.measurement.diagonals;
|
||||||
|
context.distance.total = waypoint.measurement.distance.toNearest(0.1) + diagonalCost; //Diagonals count twice
|
||||||
|
context.additional = {
|
||||||
|
label: game.i18n.format("l5r5e.tactical_grid.range_abbriviation", {range: getRangeband(gridSettings, waypoint.measurement.distance)})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_getSegmentStyle(waypoint) {
|
||||||
|
const context = super._getSegmentStyle(waypoint);
|
||||||
|
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||||
|
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||||
|
if(gridSettings.enabled) {
|
||||||
|
const rangeband = getRangeband(gridSettings, waypoint.measurement.distance);
|
||||||
|
context.color = client.ranges[rangeband].color;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokenRulerL5r5e extends foundry.canvas.placeables.tokens.TokenRuler {
|
||||||
|
static WAYPOINT_LABEL_TEMPLATE = "systems/l5r5e/templates/" + "hud/tactical-grid-ruler.html"
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_getWaypointLabelContext(waypoint, state) {
|
||||||
|
const context = super._getWaypointLabelContext(waypoint, state);
|
||||||
|
if (!context)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.token.actor)
|
||||||
|
return context;
|
||||||
|
|
||||||
|
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||||
|
if(gridSettings.enabled) {
|
||||||
|
const diagonalCost = game.canvas.grid.distance * waypoint.measurement.diagonals;
|
||||||
|
context.cost.total = waypoint.measurement.cost.toNearest(0.1) + diagonalCost; //Diagonals count twice
|
||||||
|
context.additional = {
|
||||||
|
label: game.i18n.format("l5r5e.tactical_grid.range_abbriviation", {range: getRangeband(gridSettings, waypoint.measurement.distance)})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
_getGridHighlightStyle(waypoint, offset) {
|
||||||
|
const context = super._getGridHighlightStyle(waypoint, offset);
|
||||||
|
const client = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-client");
|
||||||
|
const gridSettings = game.settings.get(CONFIG.l5r5e.namespace, "tactical-grid-settings-world");
|
||||||
|
if(gridSettings.enabled) {
|
||||||
|
const rangeband = getRangeband(gridSettings, waypoint.measurement.distance);
|
||||||
|
context.color = client.ranges[rangeband].color;
|
||||||
|
context.alpha = client.ranges[rangeband].alpha;
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,5 +20,6 @@
|
|||||||
@import "../scss/skills";
|
@import "../scss/skills";
|
||||||
@import "../scss/items";
|
@import "../scss/items";
|
||||||
@import "../scss/twenty-questions";
|
@import "../scss/twenty-questions";
|
||||||
|
@import "../scss/tactical-grid";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
32
system/styles/scss/tactical-grid.scss
Normal file
32
system/styles/scss/tactical-grid.scss
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
// Set the label for in-world measurement to be the same as normal waypoint-label
|
||||||
|
@at-root #measurement .waypoint-label-additional {
|
||||||
|
color: var(--color-text-emphatic);
|
||||||
|
font-size: var(--font-size-24);
|
||||||
|
}
|
||||||
|
|
||||||
|
@at-root #tactical-grid-settings {
|
||||||
|
input[type="number"]:invalid {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]:read-only {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none; /* not clickable or focusable */
|
||||||
|
user-select: none; /* text cannot be selected */
|
||||||
|
-webkit-user-select: none; /* Safari/Chrome */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.range_band {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: wrap;
|
||||||
|
fieldset {
|
||||||
|
flex: 25%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
system/templates/hud/tactical-grid-ruler.html
Normal file
45
system/templates/hud/tactical-grid-ruler.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="waypoint-label vertical {{cssClass}}">
|
||||||
|
<div>
|
||||||
|
{{#if action.icon}}
|
||||||
|
<i class="icon {{action.icon}}"></i>
|
||||||
|
{{else if action.label}}
|
||||||
|
<label class="action-label">Action: {{localize action.label}}</label>
|
||||||
|
{{/if}}
|
||||||
|
{{#if cost}}
|
||||||
|
<span class="total-measurement">{{cost.total}}</span>
|
||||||
|
{{#if cost.delta}}
|
||||||
|
<span class="delta-measurement">Cost Delta: ({{cost.delta}})</span>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<span class="total-measurement">{{distance.total}} {{units}}</span>
|
||||||
|
{{#if distance.delta}}
|
||||||
|
<span class="delta-measurement">Total Measure: ({{distance.delta}})</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if (and elevation (not elevation.hidden))}}
|
||||||
|
<i class="icon {{elevation.icon}}"></i>
|
||||||
|
<span class="total-elevation">{{elevation.total}} {{units}}</span>
|
||||||
|
{{#if elevation.delta}}
|
||||||
|
<span class="delta-elevation">({{elevation.delta}})</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if secret}}
|
||||||
|
<i class="icon fa-solid fa-eye-slash"></i>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{#if additional}}
|
||||||
|
<div class="waypoint-label-additional {{additional.cssClass}}">
|
||||||
|
{{#if additional.icon}}
|
||||||
|
<i class="icon {{additional.icon}}"></i>
|
||||||
|
{{/if}}
|
||||||
|
<span class="waypoint-label-text">{{additional.label}} {{additional.cost}}</span>
|
||||||
|
{{#if additional.imgs.length}}
|
||||||
|
{{#each additional.imgs as |img|}}
|
||||||
|
<img class="icon" src="{{img}}">
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<img class="icon" src="{{additional.img}}">
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
37
system/templates/settings/tactical-grid-settings.html
Normal file
37
system/templates/settings/tactical-grid-settings.html
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<section class="standard-form scrollable">
|
||||||
|
{{!-- GM-only: Enable/Disable Tactical Grid --}}
|
||||||
|
{{#if isGm}}
|
||||||
|
<fieldset>
|
||||||
|
{{formGroup tactical_grid_enabled.field value=tactical_grid_enabled.value localize=true}}
|
||||||
|
</fieldset>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{!-- Range Band Configuration --}}
|
||||||
|
<div class="range_band">
|
||||||
|
{{#each rangeBands as |band|}}
|
||||||
|
<fieldset>
|
||||||
|
<legend>
|
||||||
|
{{localize "l5r5e.tactical_grid.range_band" band=band.worldField.name}}
|
||||||
|
</legend>
|
||||||
|
|
||||||
|
{{!-- GM-only: Range start distance --}}
|
||||||
|
{{#if @root.isGm}}
|
||||||
|
{{formGroup band.worldField.fields.start
|
||||||
|
value=band.worldValue.start
|
||||||
|
localize=true
|
||||||
|
type="number"
|
||||||
|
readonly=(eq band.index 0)}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{!-- Client: Visual settings --}}
|
||||||
|
{{formGroup band.clientFields.fields.color
|
||||||
|
value=band.clientValue.color
|
||||||
|
localize=true}}
|
||||||
|
|
||||||
|
{{formGroup band.clientFields.fields.alpha
|
||||||
|
value=band.clientValue.alpha
|
||||||
|
localize=true}}
|
||||||
|
</fieldset>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user