diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..4a3d5bb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "useTabs": false, + "semi": true, + "trailingComma": "all" +} diff --git a/LICENSE b/LICENSE index b62f395..e3bd479 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Custom License based on MIT license Copyright (c) 2024 Den_drummer on the software -Copyright on the used data is subject to their own licences +Copyright on the used data is subject to their own licenses Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -16,7 +16,7 @@ copies or substantial portions of the Software. Publishing, distributing, sublicensing and/or selling of copies is only permitted on code found in releases of at least a year old unless given explicit approval. This is to prevent others "stealing" this project's -efforts, but still alowing others to take over should official support end. +efforts, but still allowing others to take over should official support end. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, diff --git a/README.md b/README.md index c8392b5..764c9b7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # Classless Skill Tree 5E *`classless-skill-tree-5e`* -An attempt at a classless skill tree Foundry addon module for DnD 5E inspired by [The Ugly Goblin](https://www.youtube.com/@TheUglyGoblin). -Plans to provide tools for GMs to create their own skill trees or recreate existing ones, and for these skill trees to be added to and/or replacing character sheets. -If given permission, will also include The Ugly Goblin's skill trees as presets. +An attempt at a classless skill tree Foundry addon module for DnD 5E inspired by [The Ugly Goblin](https://www.youtube.com/@TheUglyGoblin). + +Plans to provide tools for GMs to create their own skill trees or recreate existing ones, and for these skill trees to be added to and/or replacing character sheets. + +If given permission, will also include The Ugly Goblin's skill trees as presets, but the intent is to be fully compatible with it regardless. + +In the meantime, I recommend giving their explanation video a watch! +[![Video thumbnail of "I REMOVED Classes from D&D + FREE Adventure" by The Ugly Goblin](https://img.youtube.com/vi/L7ONm4qYaXI/0.jpg)](https://www.youtube.com/watch?v=L7ONm4qYaXI) This module is currently in VERY early stages, so for now you will have to manually download it yourself if you want to play around with it. diff --git a/module.json b/module.json index 3ad7606..6da1db7 100644 --- a/module.json +++ b/module.json @@ -1,43 +1,40 @@ { - "authors": [ - { - "discord": "Den_drummer", - "flags": {}, - "name": "Den_drummer" - } - ], - "compatibility": { - "minimum": "12", - "verified": "12.330" - }, - "description": "An attempt at a classless skill tree addon for 5E inspired by The Ugly Goblin on Youtube.\nPlans to include their skill trees as presets if given permission.", - "documentTypes": {}, - "esmodules": [ - "scripts/Main.js" - ], - "id": "classless-skill-tree-5e", - "languages": [ - { - "lang": "en", - "name": "English", - "path": "languages/en.json" - } - ], - "relationships": { - "systems": [ - { - "compatibility": { - "minimum": "3", - "verified": "3.3.1" - }, - "id": "dnd5e", - "type": "system" - } - ] - }, - "styles": [ - "styles/SkillTree.css" - ], - "title": "Classless Skill Tree 5e", - "version": "0.0.1" + "authors": [ + { + "discord": "Den_drummer", + "flags": {}, + "name": "Den_drummer" + } + ], + "compatibility": { + "minimum": "12", + "verified": "12.330" + }, + "description": "An attempt at a classless skill tree addon for 5E inspired by The Ugly Goblin on Youtube.\nPlans to include their skill trees as presets if given permission.", + "esmodules": ["scripts/Main.js"], + "id": "classless-skill-tree-5e", + "languages": [ + { + "lang": "en", + "name": "English", + "path": "languages/en.json", + "flags": {} + } + ], + "relationships": { + "systems": [ + { + "compatibility": { + "minimum": "3", + "verified": "3.3.1" + }, + "id": "dnd5e", + "type": "system" + } + ] + }, + "styles": ["styles/SkillTree.css"], + "title": "Classless Skill Tree 5e", + "version": "0.0.2", + "url": "https://github.com/DenDrummer/classless-skill-tree-5e" } diff --git a/scripts/ClassLessSkillTree.js b/scripts/ClassLessSkillTree.js new file mode 100644 index 0000000..2dab963 --- /dev/null +++ b/scripts/ClassLessSkillTree.js @@ -0,0 +1,16 @@ +/** + * A class representing the Classless Skill Tree 5E module + * @typedef {Object} ClasslessSkillTree5E + * @property {string} ID the full ID, used e.g. for Foundry + * @property {string} SHORT_ID the short ID, used e.g. for logging + * @property {Object} FLAGS (needed for Foundry?) + * @property {Object} TEMPLATES (needed for Foundry?) + */ +export class ClasslessSkillTree5E { + static ID = "classless-skill-tree-5e"; + static SHORT_ID = "CST5E"; + + static FLAGS = {}; + + static TEMPLATES = {}; +} diff --git a/scripts/Main.js b/scripts/Main.js index 5251ffc..1bd71b6 100644 --- a/scripts/Main.js +++ b/scripts/Main.js @@ -1,17 +1,59 @@ +import { ClasslessSkillTree5E } from "./ClassLessSkillTree.js"; +import { RequiredSkill } from "./RequiredSkill.js"; +import { SkillNode } from "./SkillNode.js"; +import { SkillRequirement } from "./SkillRequirement.js"; import { SkillTree } from "./SkillTree.js"; +import { SkillTreeUtils } from "./SkillTreeUtils.js"; +SkillTreeUtils.log(false, "Main Loaded!"); -console.log("CST5E | Main Loaded!"); +Hooks.once("devModeReady", ({ registerPackageDebugFlag }) => { + SkillTreeUtils.log(false, "Dev Mode Ready"); + registerPackageDebugFlag(ClasslessSkillTree5E.ID); +}); Hooks.on("init", () => { - console.log("CST5E | Main Initialized!"); - // Object.assign(CONFIG.JournalEntryPage.dataModels, { - // "classless-skill-tree-5e": SkillTree - // }); + SkillTreeUtils.log(false, "Main Initialized!"); }); -Hooks.on("init", () => { - console.log("CST5E | Main Available!"); - - console.log(new SkillTree()); +Hooks.on("ready", () => { + SkillTreeUtils.log(false, "Main Available!"); + + const testSkillNode = new SkillNode( + "TestNode", + "Test Node", + "This is a test node to see if the Skill Node class works.", + "#FFFFFF", + 0, + 0, + [] + ); + + const testDependentNode = new SkillNode( + "DependentNode", + "Dependent Node", + "This is a test node to see if Skill Node requirements work", + "#FFFFFF", + 100, + 0, + [ + new SkillRequirement("OR", [ + new RequiredSkill(testSkillNode, 1, "<="), + ]), + ] + ); + + const testSkillTree = new SkillTree( + "Test Tree", + "This is a test to see if the Skill Tree class works.", + [testSkillNode, testDependentNode] + ); + + const errorMessages = testSkillTree.validateTree(); + if (errorMessages.length > 0) { + SkillTreeUtils.log(false, "Errors found in the Skill Tree:"); + errorMessages.forEach((errorMsg) => { + console.warn(`CST5E | ${errorMsg}`); + }); + } }); diff --git a/scripts/MultiSkillRequirement.js b/scripts/MultiSkillRequirement.js new file mode 100644 index 0000000..cd5c3be --- /dev/null +++ b/scripts/MultiSkillRequirement.js @@ -0,0 +1,27 @@ +import { SkillRequirement } from "./SkillRequirement.js"; + +/** + * A class representing a group of requirements of which the specified amount is required to unlock the parent item. + * @typedef {Object} MultiSkillRequirement + * @property {Array} requiredItems An array of required items. \ + * These items are either a {@link SkillNode} or a {@link RequiredSkill}. \ + * This list must contain at least one valid item to be valid. + * @property {number} requiredNumber The number of these items required to unlock the parent item. + */ +export class MultiSkillRequirement extends SkillRequirement { + /** + * Create a requirement group + * @param {Array} requiredItems An array of required items. \ + * These items are either a {@link SkillNode} or a {@link RequiredSkill}. \ + * This list must contain at least one valid item to be valid. + * @param {number} requiredNumber The number of these items required to unlock the parent item. \ + * Default: 1 + */ + constructor(requiredItems, requiredNumber = 1) { + this.requiredNumber = requiredNumber; + this.requiredItems = requiredItems; + + SkillTreeUtils.log(false, "MultiSkillRequirement created"); + console.log(this); + } +} diff --git a/scripts/RequiredSkill.js b/scripts/RequiredSkill.js new file mode 100644 index 0000000..1167db3 --- /dev/null +++ b/scripts/RequiredSkill.js @@ -0,0 +1,43 @@ +import { SkillNode } from "./SkillNode.js"; + +/** + * A class representing a skill being required and at what level(s) the requirement is met. + * @typedef {Object} RequiredSkill + * @property {SkillNode} skill The {@link SkillNode} that is being depended on. + * @property {number} requiredLevel The level the required skill is being compared to. \ + * Default: 1 + * @property {"<" | "<=" | "==" | ">=" | ">"} requirementType How the current level of the required skill is compared to the required level of said skill. \ + * Default: "<=" + * @property {boolean} drawLine Whether to draw the line of this node to the required node. \ + * Default: true + */ +export class RequiredSkill { + /** + * Create a dependency on a {@link SkillNode} and define at which level(s) this requirement is met. + * @param {SkillNode} skill The {@link SkillNode} that is being depended on. + * @param {number} requiredLevel The level the required skill is being compared to. \ + * Defaults: 1 + * @param {"<" | "<=" | "==" | ">=" | ">"} requirementType How the current level of the required skill is compared to the required level of said skill. \ + * Default: "<=" + * @param {boolean} drawLine Whether to draw the line of this node to the required node. \ + * Default: true + */ + constructor( + skill, + requiredLevel = 1, + requirementType = "<=", + drawLine = true, + ) { + /** @type {SkillNode} */ + this.skill = skill; + /** @type {number} */ + this.requiredLevel = requiredLevel; + /** @type {"<" | "<=" | "==" | ">=" | ">"} */ + this.requirementType = requirementType; + /** @type {boolean} */ + this.drawLine = drawLine; + + SkillTreeUtils.log(false, "RequiredSkill created"); + console.log(this); + } +} diff --git a/scripts/SkillNode.js b/scripts/SkillNode.js index 0c6a8ae..dad8e7b 100644 --- a/scripts/SkillNode.js +++ b/scripts/SkillNode.js @@ -1,42 +1,149 @@ +import { RequiredSkill } from "./RequiredSkill.js"; +import { SkillOption } from "./SkillOption.js"; +import { SkillRequirement } from "./SkillRequirement.js"; +import { SkillTree } from "./SkillTree.js"; +import { SkillTreeUtils } from "./SkillTreeUtils.js"; + +/** + * A class representing a node in the {@link SkillTree} + * @typedef {Object} SkillNode + * @property {string} id The id of the skill, used to link a {@link RequiredSkill} to this one. \ + * Only shown and editable in GM-view. + * @property {string} title The title of the skill. \ + * Should be a single line of text. + * @property {string} description The title of the skill + * @property {string} color the HEX color code for this color + * @property {number} x The horizontal position of the node in the {@link SkillTree}. \ + * 0 is intended to be the "center" and positive numbers are to the right of it. + * @property {number} y The vertical position of the node in the {@link SkillTree}. \ + * 0 is intended to be the "center" and positive numbers are down of it. + * @property {Array} requirements A list of other require + * @property {Array} options SubOptions. These are the things the user effectively "buys" with points. + */ export class SkillNode { - x = 0; - y = 0; - id = 'default'; - title = 'Default'; - description = 'Default skill node'; - childNodes = Array([]); - - constructor() { - this.childNodes = []; - console.log("CST5E | SkillNode instance created"); + /** + * Creates a new node representing a skill. + * @param {string} id The id of the skill, used to link a {@link RequiredSkill} to this one. \ + * Only shown and editable in GM-view. + * @param {string} title The title of the skill. \ + * Should be a single line of text. + * @param {string} description The title of the skill + * @param {string} color the HEX color code for this color + * @param {number} x The horizontal position of the node in the {@link SkillTree}. \ + * 0 is intended to be the "center" and positive numbers are to the right of it. + * @param {number} y The vertical position of the node in the {@link SkillTree}. \ + * 0 is intended to be the "center" and positive numbers are down of it. + * @param {Array} requirements A list of other require + * @param {Array} options SubOptions + */ + constructor( + id, + title, + description, + color, + x, + y, + requirements = [], + options = [], + ) { + /** @type {string} */ + this.id = id; + /** @type {string} */ + this.title = title; + /** @type {string} */ + this.description = description; + /** @type {string} */ + this.color = color; + /** @type {number} */ + this.x = x; + /** @type {number} */ + this.y = y; + /** @type {Array} */ + this.requirements = requirements; + /** @type {Array} */ + this.options = options; + + SkillTreeUtils.log(false, "SkillNode instance created"); + console.log(this); } - validateTree(treePath){ - console.log("CST5E | Validating Tree Path: " + treePath); + /** + * Check if this Skill Node is completely valid + * @returns {Array} a list of validation-error messages. + */ + validate() { + /** @type {Array} */ + const errorMessages = []; + + if (!SkillTreeUtils.isValidId(this.id)) { + errorMessages.push(`"${this.id}" is not a valid id.`); + } + + if (!SkillTreeUtils.isValidColor(this.color)) { + errorMessages.push( + `"${this.color}" (found in "${this.id}") is not a valid color code.`, + ); + } + + return errorMessages; + } + + /** + * Get the total of levels of this Skill and its sub-options + * @returns {number} a number representing the total level of the node itself and its sub-options + */ + totalLevel() { + let total = this.level; + + this.options.forEach((option) => { + total += option.level; + }); + + return total; + } - const pathNodes = treePath.split(".") + /** + * Get the lowest cost possible to unlock this Skill Node + * @returns {number} a number representing the lowest cost possible to unlock this node + */ + getLowestCost() { + let lowestCost = Number.MAX_SAFE_INTEGER; - let nodeA, nodeB; - for (let i = 0; i < pathNodes.length; i++) { - nodeA = pathNodes[i]; + this.options.forEach((option) => { + lowestCost = Math.min(lowestCost, option.getCurrentCost()); + }); - for (let j = i+1; j < pathNodes.length; j++) { - nodeB = pathNodes[j]; + return lowestCost; + } - if (nodeA===nodeB) { - return false; - } - } + /** + * Get the least amount of points you will need to spend to unlock this node. + * @returns {number} A number representing the amount of points you will need to spend. + */ + getCheapestPathCost() { + if (this.totalLevel > 0 || this.requirements.length === 0) { + return 0; } - childNodes.forEach(childNode => { - if(!childNode.validateTree(treePath+"."+childNode.id)){ - return false; - } + let pathCost = this.getLowestCost(); + + const requirements = [...this.requirements]; + + requirements.forEach((requirement) => { + const parentSkills = [...requirement.getDirectParentSkills()]; + + parentSkills.forEach((parentSkill) => { + parentSkill.cost = parentSkill.getCheapestPathCost(); + }); + + parentSkills.sort((a, b) => { + return a.cost - b.cost; + }); + + SkillTreeUtils.log(false, "cheapest vs most expensive?"); + console.log(parentSkills); }); - - return true; + + return pathCost; } } - -console.log("CST5E | SkillNode Loaded"); diff --git a/scripts/SkillOption.js b/scripts/SkillOption.js new file mode 100644 index 0000000..7e95666 --- /dev/null +++ b/scripts/SkillOption.js @@ -0,0 +1,59 @@ +/** + * A class representing an option of a {@link SkillNode} + * @typedef {Object} SkillOption + * @property {string} title + * @property {string} description + * @property {Array} costs + * @property {boolean} repeatable + * @property {number} level + */ +export class SkillOption { + /** + * Create an option for a Skill + * @param {string} title + * @param {string} description + * @param {Array} costs A list of costs per level of this option. \ + * Requires at least a single entry. + * @param {boolean} repeatable Whether the costs form a loop, \ + * making this option possible to be bought "infinitely" + * @param {number} level The current amount of levels bought into this option. + */ + constructor(title, description, costs = [1], repeatable = true, level = 0) { + if (costs.length === 0) { + throw new Error("No costs provided"); + } + + this.title = title; + this.description = description; + this.costs = costs; + this.repeatable = repeatable; + this.level = level; + } + + /** + * Get the cost of the next level of this option, if available. + * @returns {number|undefined} A number representing the cost to buy the next level of this option, \ + * or undefined if no new level is available + */ + getCurrentCost() { + if (this.level >= this.costs.length && !this.repeatable) { + return; + } + + return this.costs[this.level % this.costs.length]; + } + + /** + * Get the total of of points spent on upgrading this option. + * @returns {number} a number representing the total amount of points spent + */ + getPointsSpent() { + let totalCost = 0; + + for (let i = 0; i < this.level; i++) { + totalCost += this.costs[i % this.costs.length]; + } + + return totalCost; + } +} diff --git a/scripts/SkillRequirement.js b/scripts/SkillRequirement.js new file mode 100644 index 0000000..3a50a4f --- /dev/null +++ b/scripts/SkillRequirement.js @@ -0,0 +1,84 @@ +import { RequiredSkill } from "./RequiredSkill.js"; +import { SkillNode } from "./SkillNode.js"; + +/** + * Class representing a requirement group to unlock a Skill + * @typedef {Object} SkillRequirement + * @property {Array} requiredItems An array of required items. These items are either a {@link SkillNode} or a {@link RequiredSkill}. \ + * This list must contain at least one valid item to be valid. + * @property {"AND" | "OR" | "XOR" | "XNOR"} requirementType The way multiple required skills of sub-requirements interact with each other here: \ + * - AND : All items are required to unlock the parent item. \ + * - OR : Only 1 item is required to unlock the parent item. \ + * - XOR : An odd amount of items is required to unlock the parent item. \ + * - XNOR : An even amount of items is required to unlock the parent item. \ + * Default: OR + */ +export class SkillRequirement { + /** + * Create a requirement group + * @param {Array} requiredItems An array of required items. These items are either a {@link SkillNode} or a {@link RequiredSkill}. \ + * This list must contain at least one valid item to be valid. + * @param {"AND" | "OR" | "XOR" | "XNOR"} requirementType The way multiple required skills of sub-requirements interact with each other here: \ + * - AND : All items are required to unlock the parent item. \ + * - OR : Only 1 item is required to unlock the parent item. \ + * - XOR : An odd amount of items is required to unlock the parent item. \ + * - XNOR : An even amount of items is required to unlock the parent item. \ + * Default: OR + */ + constructor(requiredItems, requirementType = "OR") { + /** @type {Array} */ + this.requiredItems = requiredItems; + /** @type {"AND" | "OR" | "XOR" | "XNOR"} */ + this.requirementType = requirementType; + + SkillTreeUtils.log(false, "SkillRequirement created"); + console.log(this); + } + + /** + * Gets all skills this requirement (and its sub-requirements) directly depend upon. \ + * Does not include the parents of its parent nodes recursively. + * @returns {Set} + */ + getDirectParentSkills() { + /** @type {Set} */ + let parentSkills = new Set(); + + this.requiredItems.forEach((requiredItem) => { + // Object.getPrototypeOf(x).constructor.name is used to get which of the possible classes it was + // if you know an easier way... PLEASE tell me + /** @type {string} */ + const itemType = + Object.getPrototypeOf(requiredItem).constructor.name; + + switch (itemType) { + case RequiredSkill.name: + /** @type {RequiredSkill} */ + const skillItem = requiredItem; + parentSkills.add(skillItem.skill); + break; + + case SkillRequirement.name: + /** @type {SkillRequirement} */ + const requirementItem = requiredItem; + requirementItem.requiredItems.forEach( + (/** @type {SkillRequirement} */ subRequirement) => { + subRequirement + .getDirectParentSkills() + .forEach((parentSkill) => { + parentSkills.add(parentSkill); + }); + }, + ); + break; + + default: + throw new Error( + `Unknown item in list of requiredItems:\nType of: ${itemType}\n${requiredItem}`, + ); + } + }); + + return parentSkills; + } +} diff --git a/scripts/SkillTree.js b/scripts/SkillTree.js index b7ebcc2..fc0a664 100644 --- a/scripts/SkillTree.js +++ b/scripts/SkillTree.js @@ -1,48 +1,100 @@ import { SkillNode } from "./SkillNode.js"; +import { SkillRequirement } from "./SkillRequirement.js"; +/** + * A class representing a Skill Tree + * @typedef {Object} SkillTree + * @property {string} title The title of the {@link SkillTree}. + * Should be a single line of text. + * @property {string} description The description of the {@link SkillTree}. + * Should be a multiline piece of formatted text. + * @property {Array} skillNodes An array of {@link SkillNode}s + */ export class SkillTree { - title = ''; - rootNodes = [new SkillNode()]; - skillNodes = [new SkillNode()]; + /** + * Create a new instance of a Skill Tree + * @param {string} title The title of the {@link SkillTree}. + * Should be a single line of text. + * @param {string} description The description of the {@link SkillTree}. + * Should be a multiline piece of formatted text. + * @param {Array} skillNodes An array of {@link SkillNode}s + */ + constructor(title, description, skillNodes) { + this.title = title; + this.description = description; + this.skillNodes = skillNodes; + this.rootNodes = this.findRootNodes(); - constructor(){ - this.findRootNodes(); - console.log("CST5E | SkillTree instance created"); + SkillTreeUtils.log(false, "SkillTree instance created"); + console.log(this); } - // static defineSchema() { - // return { - // title: String, - // skillNodes: Array = [new SkillNode()] - // }; - // } - - // prepareDerivedData() { - // this.findRootNodes(); - // } - + /** + * + * @returns {Array} a list of {@link SkillNode}s that have no requirements, and thus are root nodes. + * Root nodes are always unlocked. + */ findRootNodes() { - this.rootNodes = this.skillNodes; - - this.skillNodes.forEach(skillNode => { - skillNode.childNodes.forEach(childNode => { - this.rootNodes.filter(rootNode => { - return rootNode.id != childNode.id; - }); - }); + SkillTreeUtils.log(false, "Finding root nodes"); + return this.skillNodes.filter((skillNode) => { + return skillNode.requirements.length == 0; }); - - return this.rootNodes; } + /** + * Checks if this skill tree is completely valid. \ + * Returns all the error messages in the current tree. + * @returns + */ validateTree() { - skillNodes.forEach(skillNode => { - if (!skillNode.validateTree(skillNode.id)) { - return false; + /** + * @type {Array} + */ + const errorMessages = []; + + //#region --- AT LEAST ONE ROOT NODE --- + if (this.rootNodes.length == 0) { + errorMessages.push( + "There are no root-nodes. Make sure you have at least 1 node without requirements", + ); + } + //#endregion --- AT LEAST ONE ROOT NODE --- + + //#region --- ALL SKILL NODE IDS UNIQUE --- + const skillIds = new Set(); + const warnedIds = new Set(); + this.skillNodes.forEach((skillNode) => { + const id = skillNode.id; + + // also check if the node itself is valid + errorMessages.push(...skillNode.validate()); + + if (skillIds.has(id) && !warnedIds.has(id)) { + errorMessages.push(`Multiple nodes with id "${id}".`); + warnedIds.add(id); + } else { + skillIds.add(id); } }); - return true; + //#endregion --- ALL SKILL NODE IDS UNIQUE --- + + //#region --- ALL REQUIREMENTS VALID --- + this.skillNodes.forEach((skillNode) => { + skillNode.requirements.forEach((requirement) => { + const parentSkills = requirement.getDirectParentSkills(); + parentSkills.forEach((parentSkill) => { + if (!skillIds.has(parentSkill.id)) { + errorMessages.push( + `Skill "${skillNode.id}" requires skill "${parentSkill.id}", which does not exist in this skill tree.`, + ); + } + }); + }); + }); + //#endregion --- ALL REQUIREMENTS VALID --- + + return errorMessages; } } -console.log("CST5E | SkillTree Loaded"); +SkillTreeUtils.log(false, "SkillTree Loaded"); diff --git a/scripts/SkillTreeUtils.js b/scripts/SkillTreeUtils.js new file mode 100644 index 0000000..51108ed --- /dev/null +++ b/scripts/SkillTreeUtils.js @@ -0,0 +1,61 @@ +import { ClasslessSkillTree5E } from "./ClassLessSkillTree"; + +/** + * A class with utility functions that can be used anywhere + */ +export class SkillTreeUtils { + /** + * Checks if an id is valid + * @param {string} id the id you want to check the validity of. \ + * Uses the Regular Expression `/^[a-zA-Z][a-zA-Z\d]*([_\-][a-zA-Z\d]+)*$/` \ + * This means that the id must be alphanumeric, but may have dashes and underscores in the middle, one at a time. + * @returns {boolean} true if this is a valid id, false if it is invalid. + */ + static isValidId(id) { + return id.match(/^[a-zA-Z][a-zA-Z\d]*([_\-][a-zA-Z\d]+)*$/).length > 0; + } + + /** + * Checks if a string is a HEX color code + * @param {string} color The string you want to check whether or not it is a valid HEX-code for a color. \ + * Uses the Regular Expression `/^#[0-9A-F]{6}$/` + * @returns {boolean} true if this is a valid HEX color code, false if it is invalid. + */ + static isValidColor(color) { + return color.match(/^#[\dA-F]{6}$/).length > 0; + } + + /** + * Output a log to the console with the package prefix if debug mode is on or force is true. + * @param {boolean} force Whether to force output, or let Foundry Dev Mode setting decide. + * @param {...any} args The arguments you would otherwise give to console.log() + */ + static log(force, ...args) { + const shouldLog = + force || + game.modules + .get("_dev-mode") + ?.api?.getPackageDebugValue(ClasslessSkillTree5E.ID); + + if (shouldLog) { + console.log(`${ClasslessSkillTree5E.ID} | `, ...args); + } + } + + /** + * Output a warning to the console with the package prefix if debug mode is on or force is true. + * @param {boolean} force Whether to force output, or let Foundry Dev Mode setting decide. + * @param {...any} args The arguments you would otherwise give to console.warn() + */ + static warn(force, ...args) { + const shouldLog = + force || + game.modules + .get("_dev-mode") + ?.api?.getPackageDebugValue(ClasslessSkillTree5E.ID); + + if (shouldLog) { + console.warn(`${ClasslessSkillTree5E.ID} | `, ...args); + } + } +}