import DynamicRingData from "./ring-data.mjs"; /** * The start and end radii of the token ring color band. * @typedef {Object} RingColorBand * @property {number} startRadius The starting normalized radius of the token ring color band. * @property {number} endRadius The ending normalized radius of the token ring color band. */ /** * Dynamic ring id. * @typedef {string} DynamicRingId */ /** * Token Ring configuration Singleton Class. * * @example Add a new custom ring configuration. Allow only ring pulse, ring gradient and background wave effects. * const customConfig = new foundry.canvas.tokens.DynamicRingData({ * id: "myCustomRingId", * label: "Custom Ring", * effects: { * RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE", * RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT", * BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE" * }, * spritesheet: "canvas/tokens/myCustomRings.json", * framework: { * shaderClass: MyCustomTokenRingSamplerShader, * ringClass: TokenRing * } * }); * CONFIG.Token.ring.addConfig(customConfig.id, customConfig); * * @example Get a specific ring configuration * const config = CONFIG.Token.ring.getConfig("myCustomRingId"); * console.log(config.spritesheet); // Output: canvas/tokens/myCustomRings.json * * @example Use a specific ring configuration * const success = CONFIG.Token.ring.useConfig("myCustomRingId"); * console.log(success); // Output: true * * @example Get the labels of all configurations * const configLabels = CONFIG.Token.ring.configLabels; * console.log(configLabels); * // Output: * // { * // "coreSteel": "Foundry VTT Steel Ring", * // "coreBronze": "Foundry VTT Bronze Ring", * // "myCustomRingId" : "My Super Power Ring" * // } * * @example Get the IDs of all configurations * const configIDs = CONFIG.Token.ring.configIDs; * console.log(configIDs); // Output: ["coreSteel", "coreBronze", "myCustomRingId"] * * @example Create a hook to add a custom token ring configuration. This ring configuration will appear in the settings. * Hooks.on("initializeDynamicTokenRingConfig", ringConfig => { * const mySuperPowerRings = new foundry.canvas.tokens.DynamicRingData({ * id: "myCustomRingId", * label: "My Super Power Rings", * effects: { * RING_PULSE: "TOKEN.RING.EFFECTS.RING_PULSE", * RING_GRADIENT: "TOKEN.RING.EFFECTS.RING_GRADIENT", * BACKGROUND_WAVE: "TOKEN.RING.EFFECTS.BACKGROUND_WAVE" * }, * spritesheet: "canvas/tokens/mySuperPowerRings.json" * }); * ringConfig.addConfig("mySuperPowerRings", mySuperPowerRings); * }); * * @example Activate color bands debugging visuals to ease configuration * CONFIG.Token.ring.debugColorBands = true; */ export default class TokenRingConfig { constructor() { if ( TokenRingConfig.#instance ) { throw new Error("An instance of TokenRingConfig has already been created. " + "Use `CONFIG.Token.ring` to access it."); } TokenRingConfig.#instance = this; } /** * The token ring config instance. * @type {TokenRingConfig} */ static #instance; /** * To know if the ring config is initialized. * @type {boolean} */ static #initialized = false; /** * To know if a Token Ring registration is possible. * @type {boolean} */ static #closedRegistration = true; /** * Core token rings used in Foundry VTT. * Each key is a string identifier for a ring, and the value is an object containing the ring's data. * This object is frozen to prevent any modifications. * @type {Readonly>} */ static CORE_TOKEN_RINGS = Object.freeze({ coreSteel: { id: "coreSteel", label: "TOKEN.RING.SETTINGS.coreSteel", spritesheet: "canvas/tokens/rings-steel.json" }, coreBronze: { id: "coreBronze", label: "TOKEN.RING.SETTINGS.coreBronze", spritesheet: "canvas/tokens/rings-bronze.json" } }); /** * Core token rings fit modes used in Foundry VTT. * @type {Readonly} */ static CORE_TOKEN_RINGS_FIT_MODES = Object.freeze({ subject: { id: "subject", label: "TOKEN.RING.SETTINGS.FIT_MODES.subject" }, grid: { id: "grid", label: "TOKEN.RING.SETTINGS.FIT_MODES.grid" } }); /* -------------------------------------------- */ /** * Register the token ring config and initialize it */ static initialize() { // If token config is initialized if ( this.#initialized ) { throw new Error("The token configuration class can be initialized only once!") } // Open the registration window for the token rings this.#closedRegistration = false; // Add default rings for ( const id in this.CORE_TOKEN_RINGS ) { const config = new DynamicRingData(this.CORE_TOKEN_RINGS[id]); CONFIG.Token.ring.addConfig(config.id, config); } // Call an explicit hook for token ring configuration Hooks.callAll("initializeDynamicTokenRingConfig", CONFIG.Token.ring); // Initialize token rings configuration if ( !CONFIG.Token.ring.useConfig(game.settings.get("core", "dynamicTokenRing")) ) { CONFIG.Token.ring.useConfig(this.CORE_TOKEN_RINGS.coreSteel.id); } // Close the registration window for the token rings this.#closedRegistration = true; this.#initialized = true; } /* -------------------------------------------- */ /** * Register game settings used by the Token Ring */ static registerSettings() { game.settings.register("core", "dynamicTokenRing", { name: "TOKEN.RING.SETTINGS.label", hint: "TOKEN.RING.SETTINGS.hint", scope: "world", config: true, type: new foundry.data.fields.StringField({required: true, blank: false, initial: this.CORE_TOKEN_RINGS.coreSteel.id, choices: () => CONFIG.Token.ring.configLabels }), requiresReload: true }); game.settings.register("core", "dynamicTokenRingFitMode", { name: "TOKEN.RING.SETTINGS.FIT_MODES.label", hint: "TOKEN.RING.SETTINGS.FIT_MODES.hint", scope: "world", config: true, type: new foundry.data.fields.StringField({ required: true, blank: false, initial: this.CORE_TOKEN_RINGS_FIT_MODES.subject.id, choices: Object.fromEntries(Object.entries(this.CORE_TOKEN_RINGS_FIT_MODES).map(([key, mode]) => [key, mode.label])) }), requiresReload: true }); } /* -------------------------------------------- */ /** * Ring configurations. * @type {Map} */ #configs = new Map(); /** * The current ring configuration. * @type {DynamicRingData} */ #currentConfig; /* -------------------------------------------- */ /* Properties */ /* -------------------------------------------- */ /** * A mapping of token subject paths where modules or systems have configured subject images. * @type {Record} */ subjectPaths = {}; /** * All color bands visual debug flag. * @type {boolean} */ debugColorBands = false; /** * Get the current ring class. * @type {typeof TokenRing} The current ring class. */ get ringClass() { return this.#currentConfig.framework.ringClass; } set ringClass(value) { this.#currentConfig.framework.ringClass = value; } /** * Get the current effects. * @type {Record} The current effects. */ get effects() { return this.#currentConfig.effects; } /** * Get the current spritesheet. * @type {string} The current spritesheet path. */ get spritesheet() { return this.#currentConfig.spritesheet; } /** * Get the current shader class. * @type {typeof PrimaryBaseSamplerShader} The current shader class. */ get shaderClass() { return this.#currentConfig.framework.shaderClass; } set shaderClass(value) { this.#currentConfig.framework.shaderClass = value; } /** * Get the current localized label. * @returns {string} */ get label() { return this.#currentConfig.label; } /** * Get the current id. * @returns {string} */ get id() { return this.#currentConfig.id; } /* -------------------------------------------- */ /* Management */ /* -------------------------------------------- */ /** * Is a custom fit mode active? * @returns {boolean} */ get isGridFitMode() { return game.settings.get("core","dynamicTokenRingFitMode") === this.constructor.CORE_TOKEN_RINGS_FIT_MODES.grid.id; } /* -------------------------------------------- */ /** * Add a new ring configuration. * @param {string} id The id of the ring configuration. * @param {RingConfig} config The configuration object for the ring. */ addConfig(id, config) { if ( this.constructor.#closedRegistration ) { throw new Error("Dynamic Rings registration window is closed. You must register a dynamic token ring configuration during" + " the `registerDynamicTokenRing` hook."); } this.#configs.set(id, config); } /* -------------------------------------------- */ /** * Get a ring configuration. * @param {string} id The id of the ring configuration. * @returns {RingConfig} The ring configuration object. */ getConfig(id) { return this.#configs.get(id); } /* -------------------------------------------- */ /** * Use a ring configuration. * @param {string} id The id of the ring configuration to use. * @returns {boolean} True if the configuration was successfully set, false otherwise. */ useConfig(id) { if ( this.#configs.has(id) ) { this.#currentConfig = this.#configs.get(id); return true; } return false; } /* -------------------------------------------- */ /** * Get the IDs of all configurations. * @returns {string[]} The names of all configurations. */ get configIDs() { return Array.from(this.#configs.keys()); } /* -------------------------------------------- */ /** * Get the labels of all configurations. * @returns {Record} An object with configuration names as keys and localized labels as values. */ get configLabels() { const labels = {}; for ( const [name, config] of this.#configs.entries() ) { labels[name] = config.label; } return labels; } /* -------------------------------------------- */ /* Deprecations and Compatibility */ /* -------------------------------------------- */ /** * @deprecated since v11 * @ignore */ get configNames() { const msg = "TokenRingConfig#configNames is deprecated and replaced by TokenRingConfig#configIDs"; foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14}); return this.configIDs; } }