zischenstand

This commit is contained in:
centron\schwoerer
2025-11-14 14:52:43 +01:00
parent 30aa03c6db
commit f054a31b20
8733 changed files with 900639 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Zoty
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,71 @@
{
"id": "objects-interactions-fx",
"title": "✨ Automated Objects, Interactions and Effects 🔥",
"description": "Animations. Automations. Interactions. Effects. And soon Objects too! Immerse your players on the game, let they experience the interactions and feel the combat!",
"version": "1.1.5",
"authors": [
{
"name": "ZotyDev",
"email": "official@zoty.dev",
"url": "https://www.zoty.dev",
"flags": {}
}
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "./module/lang/en.json",
"flags": {}
}
],
"compatibility": {
"minimun": "10",
"verified": "11"
},
"license": "./LICENSE",
"url": "https://github.com/ZotyDev/objects-interactions-fx",
"bugs": "https://github.com/ZotyDev/objects-interactions-fx/issues",
"manifest": "https://github.com/ZotyDev/objects-interactions-fx/releases/latest/download/module.json",
"download": "https://github.com/ZotyDev/objects-interactions-fx/releases/download/1.1.5/module.zip",
"readme": "https://github.com/ZotyDev/objects-interactions-fx/blob/main/README.md",
"changelog": "https://github.com/ZotyDev/objects-interactions-fx/blob/main/CHANGELOG.md",
"relationships": {
"systems": [
{
"id": "dnd5e",
"type": "system",
"compatibility": {}
}
],
"requires": [
{
"id": "item-tags",
"type": "module",
"compatibility": {}
},
{
"id": "sequencer",
"type": "module",
"compatibility": {}
},
{
"id": "socketlib",
"type": "module",
"compatibility": {}
}
]
},
"socket": true,
"esmodules": [
"./module/scripts/Main.js",
"./module/scripts/automation/ActorInventor.js",
"./module/scripts/library/CanvasEffects.js",
"./module/scripts/library/Socket.js",
"./module/scripts/library/HandlebarsHelpers.js"
],
"styles": [
"./module/styles/ItemTags.css",
"./module/styles/Settings.css"
]
}

View File

@@ -0,0 +1,15 @@
{
"id": "Empty",
"name": "Empty",
"default": "true",
"tags": {
"powerful": {
"type": "special",
"enabled": true
},
"generateCurrency": {
"type": "special",
"enabled": true
}
}
}

View File

@@ -0,0 +1,329 @@
{
"id": "DefaultFantasyTagsJB2AComplete",
"name": "Default Packs: Fantasy (JB2A Complete)",
"requires": [
"jb2a_patreon"
],
"default": true,
"tags": {
"powerful": {
"type": "special",
"enabled": true
},
"generateCurrency": {
"type": "special",
"enabled": true
},
"club": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.club.melee.01.white",
"delay": 1250
}
},
"dagger": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.dagger.melee.fire.white",
"delay": 950
},
"throwAnimation": {
"source": "jb2a.dagger.throw.01.white",
"delay": 950
},
"returnAnimation": {
"source": "jb2a.dagger.return.01.white",
"delay": 900
}
},
"falchion": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.falchion.melee.01.white",
"delay": 1150
}
},
"glaive": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.glaive.melee.01.white",
"delay": 1150
}
},
"greataxe": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greataxe.melee.standard.white",
"delay": 1700
},
"throwAnimation": {
"source": "jb2a.greataxe.throw.white",
"delay": 1250
}
},
"greatclub": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greatclub.standard.white",
"delay": 1100
}
},
"greatsword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greatsword.melee.standard.white",
"delay": 1700
},
"throwAnimation": {
"source": "jb2a.greatsword.throw",
"delay": 1250
},
"returnAnimation": {
"source": "jb2a.greatsword.return",
"delay": 800
}
},
"halberd": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.halberd.melee.01.white",
"delay": 1150
}
},
"hammer": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.hammer.melee.01.white",
"delay": 1150
},
"throwAnimation": {
"source": "jb2a.hammer.throw",
"delay": 900
},
"returnAnimation": {
"source": "jb2a.hammer.return",
"delay": 950
}
},
"javelin": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.spear.melee.01.white.2",
"delay": 1250
},
"throwAnimation": {
"source": "jb2a.javelin.01.throw",
"delay": 1000
},
"returnAnimation": {
"source": "jb2a.javelin.01.return",
"delay": 800
}
},
"kunai": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.dagger.melee.fire.white",
"delay": 950
},
"throwAnimation": {
"source": "jb2a.kunai.throw.01",
"delay": 900
}
},
"handaxe": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.handaxe.melee.standard.white",
"delay": 1150
},
"throwAnimation": {
"source": "jb2a.handaxe.throw.01",
"delay": 900
}
},
"mace": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.mace.melee.01.white",
"delay": 1200
},
"throwAnimation": {
"source": "jb2a.mace.throw",
"delay": 900
}
},
"maul": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.maul.melee.standard.white",
"delay": 1900,
"powerful": true
}
},
"quarterstaff": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.quarterstaff.melee.01.white",
"delay": 900
}
},
"rapier": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.rapier.melee.01.white",
"delay": 1250
}
},
"scimitar": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.scimitar.melee.01.white",
"delay": 1250
}
},
"shortsword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.shortsword.melee.01.white",
"delay": 1250
}
},
"spear": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.spear.melee.01.white",
"delay": 1250
},
"throwAnimation": {
"source": "jb2a.spear.throw.01",
"delay": 950
},
"returnAnimation": {
"source": "jb2a.spear.return.01",
"delay": 850
}
},
"sword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.sword.melee.01.white",
"delay": 1250
},
"throwAnimation": {
"source": "jb2a.sword.throw.white",
"delay": 1150
}
},
"warhammer": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.warhammer.melee.01.white",
"delay": 1250
}
},
"wrench": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.wrench.melee.01.white",
"delay": 1250
}
},
"shortbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.arrow.physical.white.01",
"delay": 600
}
},
"longbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.arrow.physical.white.01",
"delay": 600
}
},
"handcrossbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.bolt.physical.white02",
"delay": 600
}
},
"lightcrossbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.bolt.physical.white02",
"delay": 600
}
},
"heavycrossbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.bolt.physical.white02",
"delay": 600
}
},
"torch": {
"type": "lighting",
"enabled": true,
"light": {
"animationType": "torch",
"animationSpeed": 5,
"animationIntensity": 5,
"animationReverse": false,
"color": "#ffae00",
"intensity": 0.4,
"angle": 360
},
"icons": {
"unlit": "icons/sundries/lights/torch-black.webp",
"lit": "icons/sundries/lights/torch-brown-lit.webp"
}
},
"lamp": {
"type": "lighting",
"enabled": true,
"light": {
"animationType": "torch",
"animationSpeed": 5,
"animationIntensity": 5,
"animationReverse": false,
"color": "#ffa500",
"intensity": 0.5,
"angle": 360
},
"icons": {
"unlit": "icons/sundries/lights/lantern-steel.webp",
"lit": "icons/sundries/lights/lantern-iron-lit-yellow.webp"
}
}
}
}

View File

@@ -0,0 +1,240 @@
{
"id": "DefaultFantasyTagsJB2AFree",
"name": "Default Packs: Fantasy (JB2A Free)",
"requires": [
"JB2A_DnD5e"
],
"default": true,
"tags": {
"powerful": {
"type": "special",
"enabled": true
},
"generateCurrency": {
"type": "special",
"enabled": true
},
"club": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.club.melee.01.white",
"delay": 1250
}
},
"dagger": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.dagger.melee.fire.white",
"delay": 950
},
"throwAnimation": {
"source": "jb2a.dagger.throw.01.white",
"delay": 950
},
"returnAnimation": {
"source": "jb2a.dagger.return.01.white",
"delay": 900
}
},
"falchion": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.falchion.melee.01.white",
"delay": 1150
}
},
"glaive": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.glaive.melee.01.white",
"delay": 1150
}
},
"greataxe": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greataxe.melee.standard.white",
"delay": 1700
},
"throwAnimation": {
"source": "jb2a.greataxe.throw.white",
"delay": 1250
}
},
"greatclub": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greatclub.standard.white",
"delay": 1100
}
},
"greatsword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.greatsword.melee.standard.white",
"delay": 1700
}
},
"halberd": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.halberd.melee.01.white",
"delay": 1150
}
},
"hammer": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.hammer.melee.01.white",
"delay": 1150
}
},
"javelin": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.spear.melee.01.white.2",
"delay": 1250
}
},
"handaxe": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.handaxe.melee.standard.white",
"delay": 1150
}
},
"mace": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.mace.melee.01.white",
"delay": 1200
}
},
"maul": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.maul.melee.standard.white",
"delay": 1900
}
},
"quarterstaff": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.quarterstaff.melee.01.white",
"delay": 900
}
},
"rapier": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.rapier.melee.01.white",
"delay": 1250
}
},
"scimitar": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.scimitar.melee.01.white",
"delay": 1250
}
},
"shortsword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.shortsword.melee.01.white",
"delay": 1250
}
},
"spear": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.spear.melee.01.white",
"delay": 1250
}
},
"sword": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.sword.melee.01.white",
"delay": 1250
}
},
"warhammer": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.warhammer.melee.01.white",
"delay": 1250
}
},
"wrench": {
"type": "meleeAttack",
"enabled": true,
"meleeAnimation": {
"source": "jb2a.wrench.melee.01.white",
"delay": 1250
}
},
"shortbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.arrow.physical.white.01",
"delay": 600
}
},
"longbow": {
"type": "rangedAttack",
"enabled": true,
"rangedAnimation": {
"source": "jb2a.arrow.physical.white.01",
"delay": 600
}
},
"torch": {
"type": "lighting",
"enabled": true,
"ligth": {
"animationType": "torch",
"color": "#ffae00",
"alpha": 0.4
},
"icons": {
"unlit": "icons/sundries/lights/torch-black.webp",
"lit": "icons/sundries/lights/torch-brown-lit.webp"
}
},
"lamp": {
"type": "lighting",
"enabled": true,
"light": {
"animationType": "torch",
"color": "#ffa500",
"alpha": 0.5
},
"icons": {
"unlit": "icons/sundries/lights/lantern-steel.webp",
"lit": "icons/sundries/lights/lantern-iron-lit-yellow.webp"
}
}
}
}

View File

@@ -0,0 +1,157 @@
{
"id": "DefaultFantasyTagsNoAnimations",
"name": "Default Packs: Fantasy (No Animation)",
"default": true,
"tags": {
"powerful": {
"type": "special",
"enabled": true
},
"generateCurrency": {
"type": "special",
"enabled": true
},
"club": {
"type": "meleeAttack",
"enabled": true
},
"dagger": {
"type": "meleeAttack",
"enabled": true
},
"falchion": {
"type": "meleeAttack",
"enabled": true
},
"glaive": {
"type": "meleeAttack",
"enabled": true
},
"greataxe": {
"type": "meleeAttack",
"enabled": true
},
"greatclub": {
"type": "meleeAttack",
"enabled": true
},
"greatsword": {
"type": "meleeAttack",
"enabled": true
},
"halberd": {
"type": "meleeAttack",
"enabled": true
},
"hammer": {
"type": "meleeAttack",
"enabled": true
},
"javelin": {
"type": "meleeAttack",
"enabled": true
},
"kunai": {
"type": "meleeAttack",
"enabled": true
},
"handaxe": {
"type": "meleeAttack",
"enabled": true
},
"mace": {
"type": "meleeAttack",
"enabled": true
},
"maul": {
"type": "meleeAttack",
"enabled": true
},
"quarterstaff": {
"type": "meleeAttack",
"enabled": true
},
"rapier": {
"type": "meleeAttack",
"enabled": true
},
"scimitar": {
"type": "meleeAttack",
"enabled": true
},
"shortsword": {
"type": "meleeAttack",
"enabled": true
},
"spear": {
"type": "meleeAttack",
"enabled": true
},
"sword": {
"type": "meleeAttack",
"enabled": true
},
"warhammer": {
"type": "meleeAttack",
"enabled": true
},
"wrench": {
"type": "meleeAttack",
"enabled": true
},
"shortbow": {
"type": "rangedAttack",
"enabled": true
},
"longbow": {
"type": "rangedAttack",
"enabled": true
},
"handcrossbow": {
"type": "rangedAttack",
"enabled": true
},
"lightcrossbow": {
"type": "rangedAttack",
"enabled": true
},
"heavycrossbow": {
"type": "rangedAttack",
"enabled": true
},
"torch": {
"type": "lighting",
"enabled": true,
"light": {
"animationType": "torch",
"animationSpeed": 5,
"animationIntensity": 5,
"animationReverse": false,
"color": "#ffae00",
"intensity": 0.4,
"angle": 360
},
"icons": {
"unlit": "icons/sundries/lights/torch-black.webp",
"lit": "icons/sundries/lights/torch-brown-lit.webp"
}
},
"lamp": {
"type": "lighting",
"enabled": true,
"light": {
"animationType": "torch",
"animationSpeed": 5,
"animationIntensity": 5,
"animationReverse": false,
"color": "#ffa500",
"intensity": 0.5,
"angle": 360
},
"icons": {
"unlit": "icons/sundries/lights/lantern-steel.webp",
"lit": "icons/sundries/lights/lantern-iron-lit-yellow.webp"
}
}
}
}

View File

@@ -0,0 +1,430 @@
{
"OIF": {
"Core": {
"MissingRequiredModule": "Automated Objects, Interactions and Effects | The required module \"$module\" is required but is currently not installed and/or enabled!"
},
"ItemTags": {
"Text": "Tags:",
"Save": "Save"
},
"Attack": {
"Melee": {
"Error": {
"NotEnough": "You have no more of this item!",
"TooFar": "Attack outside of maximun range!"
}
},
"Ranged": {
"Error": {
"NoAmmo": "You have no ammunition selected!",
"TooFar": "Attack outside of maximum range!"
}
}
},
"Item": {
"Lighting": {
"Error": {
"AlreadySource": "There is another item already providing light",
"NotSource": "There is no item providing light",
"NotRightSource": "This item is not the one currently providing light"
}
}
},
"Tooltips": {
"MasterTags": {
"Title": "Master Tags"
},
"ClearLighting": {
"Title": "Clear Lighting"
},
"Configuration": {
"Title": "Configuration"
}
},
"Settings": {
"AttachHooks": {
"Attack": {
"DnD5eAfterAttackRoll": {
"Label": "DnD5e After JUST Attack Roll"
},
"DnD5eAfterDamageRoll": {
"Label": "DnD5e After JUST Damage Roll"
},
"MidiQOLAfterAttackRoll": {
"Label": "MidiQOL After JUST Attack Roll",
"DisabledMessage": "- Requires MidiQOL"
},
"MidiQOLAfterDamageRoll": {
"Label": "MidiQOL After JUST Damage Roll",
"DisabledMessage": "- Requires MidiQOL"
},
"MidiQOLAfterCompleteRoll": {
"Label": "MidiQOL After Complete Roll",
"DisabledMessage": "- Requires MidiQOL"
},
"Pf2eAfterAttackRoll": {
"Label": "Pf2e After JUST Attack Roll"
}
},
"Item": {
"DnD5AfterItemRoll": {
"Label": "DnD5e After Item Roll"
},
"Pf2eAfterItemRoll": {
"Label": "Pf2e After Item Roll"
}
}
},
"MasterTagsSettings": {
"Title": "Master Tags Settings",
"Hint": "Edit Master Tags and the interactions related to them.",
"Label": "Master Tags",
"Load": {
"Error": {
"NotFound": "The ${pack} pack was not found"
}
},
"Creation": {
"Error": {
"Duplicated": "This tag is already defined"
}
},
"Deletion": {
"Title": "Delete \"${tag}\" tag?",
"Description": "There is no way to recover a deleted tag, if you delete it by accident you only chance of getting it back is by accessing the JSON file before saving the modifications",
"PendingChanges": "You have pending changes, please save or discard them before deleting a new pack"
},
"Pack": {
"Title": "Tag Pack",
"Select": "Select Tag Pack:",
"DisabledMessage": "- Requires \"${modules}\"",
"Delete": {
"Label": "Delete",
"Description": "Are you sure you want to delete this pack? This action cannot be undone",
"Title": "Delete ${pack}?",
"Confirm": "Ok",
"Cancel": "Ok"
},
"PendingChanges": "You have pending changes, do you want to discard them?",
"Rename": {
"Label": "Rename",
"Title": "Rename Pack",
"Id": "New Pack Id (ONLY letters and numbers, no spaces and etc)",
"Name": "New Pack Name",
"Error": {
"EmptyValues": "The new pack id and name cannot be empty",
"BothAlreadyExist": "The pack \"${pack}\" already exists and the new pack id \"${id}\" is already in use",
"IDAlreadyExist": "The new pack id \"${id}\" is already in use",
"NameAlreadyExist": "The pack \"${pack}\" already exists"
}
},
"Export": "Export",
"Import": {
"Title": "Import",
"Error": {
"DuplicatedID": "The ID \"${id}\" is already in use",
"DuplicatedName": "The name \"${name}\" is already in use"
},
"Duplicated": {
"Title": "Duplicated Pack",
"Description": "The pack \"${pack}\" already exists. Do you want to replace it?"
}
},
"Copy": "Copy",
"Create": "Create",
"Save": "Save"
},
"NewPackCopy": {
"Title": "New Pack",
"Description": "You cannot save changes at default packs, but you can create a new one containing the changes you just made.",
"Id": "New Pack Id (ONLY letters and numbers, no spaces and etc)",
"Name": "New Pack Name",
"Create": "Create",
"Copy": "Copy \"${pack}\"",
"Error": {
"BothAlreadyExist": "The pack \"${pack}\" already exists and the new pack id \"${id}\" is already in use",
"IDAlreadyExist": "The new pack id \"${id}\" is already in use",
"NameAlreadyExist": "The pack \"${pack}\" already exists"
}
},
"NewPack": {
"Title": "New Pack",
"Description": "Create a new pack from scratch with no Tags.",
"Id": "New Pack Id (ONLY letters and numbers, no spaces and etc)",
"Name": "New Pack Name",
"Create": "Create",
"Copy": "Copy",
"Error": {
"BothAlreadyExist": "The pack \"${pack}\" already exists and the new pack id \"${id}\" is already in use",
"IDAlreadyExist": "The new pack id \"${id}\" is already in use",
"NameAlreadyExist": "The pack \"${pack}\" already exists"
}
},
"Discard": {
"Title": "Discard changes?",
"Description": "Do you want to discard your changes? This action cannot be undone"
},
"SaveChanges": "Save Changes",
"DiscardChanges": "Discard Changes"
},
"GeneralSettings": {
"Title": "Module General Settings",
"Hint": "Fine tune the features to your desire.",
"Label": "Settings"
},
"ActorInventorSettings": {
"Title": "Actor Inventor Settings",
"Hint": "Setup actor inventor settings",
"Label": "Actor Inventor"
},
"DefaultAttackHook": {
"Title": "Default Attack Hook",
"Hint": "Hook used to trigger attack effects",
"Options": {
"DND5EfterAttackRoll": {
"Label": "After JUST Attack Roll (DnD5e)"
},
"DND5EfterDamageRoll": {
"Label": "After JUST Damage Roll (DnD5e)"
},
"MIDIAfterAttackRoll": {
"Label": "After JUST Attack Roll (MidiQOL)",
"DisabledMessage": "- Requires MidiQOL"
},
"MIDIAfterDamageRoll": {
"Label": "After JUST Damage Roll (MidiQOL)",
"DisabledMessage": "- Requires MidiQOL"
},
"MIDIAfterCompleteRoll": {
"Label": "After Complete Roll (MidiQOL)",
"DisabledMessage": "- Requires MidiQOL"
}
}
},
"DefaultItemHook": {
"Title": "Default Item Hook",
"Hint": "Hook used to trigger item effects",
"Options": {
"DND5EAfterItemUse": {
"Label": "After Item Use (DnD5e)"
}
}
},
"MasterTagConfiguration": {
"Title": "Master Tag Configuration:",
"Tag": {
"MeleeSingleAttackAnimation": "Melee Single Attack Animation",
"ThrowSingleAttackAnimation": "Throw Single Attack Animation",
"ReturnSingleAttackAnimation": "Return Single Attack Animation",
"RangedSingleAttackAnimation": "Ranged Single Attack Animation"
},
"Options": {
"Enabled": {
"Title": "Enabled"
},
"Type": {
"Title": "Tag Type",
"Choices": {
"None": "None",
"MeleeAttack": "Melee Attack",
"RangedAttack": "Ranged Attack",
"Lighting": "Lighting"
}
},
"Weapon": {
"MeleeAnimation": {
"Source": {
"Title": "Melee Animation Source"
},
"Delay": {
"Title": "Melee Animation Delay"
}
},
"ThrowAnimation": {
"Source": {
"Title": "Throw Animation Source"
},
"Delay": {
"Title": "Throw Animation Delay"
}
},
"ReturnAnimation": {
"Source": {
"Title": "Return Animation Source"
},
"Delay": {
"Title": "Return Animation Delay"
}
},
"RangedAnimation": {
"Source": {
"Title": "Ranged Animation Source"
},
"Delay": {
"Title": "Ranged Animation Delay"
}
}
},
"Lighting": {
"Animation": {
"Type": {
"Title": "Type",
"Hint": "The type of the light animation",
"Choices": {
"None": "None",
"Torch": "Torch",
"Pulse": "Pulse",
"Chroma": "Chroma",
"PulsingWave": "Pulsing Wave",
"SwirlingFog": "Swirling Fog",
"Sunburst": "Sunburst",
"LightDome": "Fire",
"MysteriousEmanation": "Mysterious Emanation",
"HexaDome": "Hexa Dome",
"GhostlyLight": "Ghostly Light",
"EnergyField": "Energy Field",
"RoilingMass": "Roiling Mass",
"BlackHole": "Black Hole",
"Vortex": "Vortex",
"BewitchingWave": "Bewitching Wave",
"SwirlingRainbow": "Swirling Rainbow",
"RadialRainbow": "Radial Rainbow",
"FairyLight": "Fairy Light",
"ForceGrid": "Force Grid",
"StarLight": "Start Light",
"SmokePatch": "Smoke Patch"
}
},
"Speed": {
"Title": "Animation Speed",
"Hint": "The speed of the light animation"
},
"Intensity": {
"Title": "Animation Intensity",
"Hint": "The intensity of the light animation"
},
"Reverse": {
"Title": "Reverse Animation",
"Hint": "Whether or not the light animation should be reversed"
}
},
"Color": {
"Title": "Lighting Color",
"Hint": "The color of the light"
},
"Intensity": {
"Title": "Lighting Intensity",
"Hint": "The intensity of the light"
},
"Angle": {
"Title": "Lighting Angle",
"Hint": "The angle of the light"
},
"Icons": {
"Unlit": {
"Title": "Unlit Icon",
"Hint": "The icon to be used when the light is unlit"
},
"Lit": {
"Title": "Lit Icon",
"Hint": "The icon to be used when the light is lit"
}
}
}
}
},
"UseAnimations": {
"Title": "Use Animations",
"Hint": "When disabled, OIF will only handle interactions and automations"
},
"MeleeAnimationDelay": {
"Title": "Fake Delay Before Melee Animation",
"Hint": "The delay before melee automations and interactions)"
},
"RangedAnimationDelay": {
"Title": "Fake Delay Before Ranged Animation",
"Hint": "The delay before ranged automations and interactions"
},
"CpLocation": {
"Title": "Copper Pieces Location",
"Hint": "Where are copper pieces data located inside actors"
},
"SpLocation": {
"Title": "Silver Pieces Location",
"Hint": "Where are silver pieces data located inside actors"
},
"GpLocation": {
"Title": "Gold Pieces Location",
"Hint": "Where are gold pieces data located inside actors"
},
"PpLocation": {
"Title": "Platinum Pieces Location",
"Hint": "Where are platinum pieces data located inside actors"
},
"EpLocation": {
"Title": "Electrum Pieces Location",
"Hint": "Where are electrum pieces data located inside actors"
},
"DefaultThrowableDestructionChance": {
"Title": "Default Throwable Destruction Chance",
"Hint": "Percentage that defines the default chance of throwables being destructed on impact"
},
"DefaultAmmunitionDestructionChance": {
"Title": "Default Ammunition Destruction Chance",
"Hint": "Percentage that defines the default chance of ammuntion being destructed on impact"
},
"RemoveThrowableItem": {
"Title": "Remove Throwable Item",
"Hint": "Whether or not throwable items get removed from the inventory when thrown"
},
"AddThrowableToTargetInventory": {
"Title": "Add Throwable Item to Target Inventory",
"Hint": "Whether or not throwable items get added to the target inventory when hit"
},
"AddAmmunitionToTargetInventory": {
"Title": "Add Ammunition Item to Target Inventory",
"Hint": "Whether or not ammunition items get adedd to the target inventory when hit"
},
"CreateItemPilesOnMiss": {
"Title": "Create Item Piles on Miss",
"Hint": "Whether or not Item Piles will be created when a projectile misses the target"
},
"CreateItemPilesOnHit": {
"Title": "Create Item Piles on Hit",
"Hint": "Whether or not Item Piles will be created when a projectile hits the target"
},
"SnapCreatedItemPilesToGrid": {
"Title": "Snap Created Item Piles to Grid",
"Hint": "Whether or not Item Piles created by this module should snap to grid"
},
"MinifyItemPilesNames": {
"Title": "Minify Item Piles Names",
"Hint": "Whether or not Item Piles generated by the module will have a minified name"
},
"SetElevationOfItemPiles": {
"Title": "Set Elevation of Item Piles",
"Hint": "Whether or not Item Piles created by this module should have elevation set based on target's elevation"
},
"PowerfulImpactShakeEffect": {
"Title": "Powerful Impact Shake Effect",
"Hint": "Whether or not powerful impacts will have a screen shake effect"
},
"LightingItemsAutomation": {
"Title": "Lighting Items Automation",
"Hint": "Whether or not lighting items should be automated with tags"
},
"DeveloperMode": {
"Title": "Developer Mode",
"Hint": "Logs useful information to the console !! THIS IS FOR DEVELOPERS ONLY !!"
},
"CurrencyGenerator": {
"Title": "Currency Generator",
"Hint": "Whether or not actors will have their currency generated"
},
"ModuleRequired": "Module ${module} is required",
"OptionRequired": "Requires ${option} to be enabled",
"OptionUnrequired": "Requires ${option} to be disabled",
"RestartRequired": "Requires Foundry to be restarted!!"
}
}
}

View File

@@ -0,0 +1,197 @@
import { ObjectsInteractionsFX as OIF } from "./ObjectsInteractionsFX.js";
import { Settings } from "./Settings.js";
import { SystemSupporter } from "./system/SystemSupporter.js";
import { MasterTagsSettings } from "./interface/MasterTagsSettings.js";
import { GeneralSettings } from "./interface/GeneralSettings.js";
import { ObjectsInteractionsFXData } from "./data/ObjectsInteractionsFXData.js";
import { TagHandler } from "./tags/TagHandler.js";
import { TokenLightingManipulator } from "./library/TokenLightingManipulator.js";
import { Debug as DBG } from "./library/Debug.js";
Hooks.on("init", () =>
{
console.log("%cObject Interaction FX", `
color:#FF0088;
background-color:white;
font-size:25pt;
font-weight:bold;
padding:15pt;
`);
OIF.Initialize();
Hooks.on('getSceneControlButtons', (controls) => {
if (!canvas.scene) return;
const MasterTags = {
name: 'master-tags',
title: game.i18n.localize('OIF.Tooltips.MasterTags.Title'),
icon: 'fas fa-tags',
onClick: async () => {
new MasterTagsSettings().render(true);
},
button: true
}
const ClearLighting = {
name: 'clear-lighting',
title: game.i18n.localize('OIF.Tooltips.ClearLighting.Title'),
icon: 'fas fa-lightbulb-slash',
onClick: async () => {
TokenLightingManipulator.RemoveAllLighting();
},
button: true
}
const Configuration =
{
name: 'configuration',
title: game.i18n.localize('OIF.Tooltips.Configuration.Title'),
icon: 'fas fa-gears',
onClick: async () => {
new GeneralSettings().render(true);
},
button: true
}
controls.push({
name: OIF.ID,
title: OIF.NAME,
layer: 'CanvasEffects',
icon: 'fas fa-snowflake',
visible: game.user.isGM,
tools: [
MasterTags,
ClearLighting,
Configuration
]
});
})
Hooks.on("ready", async () => {
await SystemSupporter.Initialize();
await Settings.Initialize();
await DBG.Initialize();
// Check for missing modules
let requiredModules = game.modules.get(OIF.ID).relationships.requires;
for (let module of requiredModules) {
if (!(game.modules.get(module.id)?.active)) {
ui.notifications.error(game.i18n.localize('OIF.Core.MissingRequiredModule').replace('$module', module.id));
}
}
// Create the folders that are going to be used
if (game.user.isGM)
{
// Create the root folder if it doesn't exist
let Folders = await FilePicker.browse(OIF.FILES.ORIGIN, '.');
if (!Folders.dirs.includes(OIF.FILES.DATA_FOLDERS.ROOT))
{
console.warn("Root folder doesn't exist, creating it...");
await FilePicker.createDirectory(OIF.FILES.ORIGIN, OIF.FILES.DATA_FOLDERS.ROOT);
}
// Create the default tag packs file if it doesn't exist
Folders = await FilePicker.browse(OIF.FILES.ORIGIN, OIF.FILES.DATA_FOLDERS.ROOT);
if (!Folders.files.includes(`${OIF.FILES.DATA_FOLDERS.ROOT}/TagPacks.json`))
{
console.warn("TagPacks.json doesn't exist, creating it...");
let Data = {};
await ObjectsInteractionsFXData.SaveJSON(Data, 'TagPacks.json', OIF.FILES.DATA_FOLDERS.ROOT);
}
}
DBG.Log('First breakpoint');
// Load default packs
await MasterTagsSettings.LoadFromConfig();
DBG.Log('Second breakpoint');
////////////////////////////////////////////////////////////
// Hooks to attach
////////////////////////////////////////////////////////////
let HooksToAttach =
{
attack:
{
hook: GeneralSettings.Get(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ATTACK),
id : 0
},
item:
{
hook: GeneralSettings.Get(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ITEM),
id : 0
}
}
Hooks.on(OIF.HOOKS.CHANGE_SETTINGS, async (settings) =>
{
DBG.Log('Changing settings', settings);
// Update the hooks to attach
HooksToAttach.attack.hook = GeneralSettings.Get(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ATTACK);
HooksToAttach.item.hook = GeneralSettings.Get(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ITEM);
Hooks.call(OIF.HOOKS.ATTACH_HOOKS);
});
Hooks.on(OIF.HOOKS.ATTACH_HOOKS, async () =>
{
DBG.Log('Attaching hooks', HooksToAttach);
if (HooksToAttach.attack.id != 0)
{
Hooks.off(HooksToAttach.attack.hook, HooksToAttach.attack.id);
}
if (HooksToAttach.item.id != 0)
{
Hooks.off(HooksToAttach.item.hook, HooksToAttach.item.id);
}
////////////////////////////////////////////////////////////
// Attack Hook
////////////////////////////////////////////////////////////
HooksToAttach.attack.id = Hooks.on(HooksToAttach.attack.hook, async (arg1, arg2, arg3) =>
{
// Extract relevant information
let Workflow = [arg1, arg2, arg3];
let Options = await SystemSupporter.ExtractOptions(Workflow, 'attack', HooksToAttach.attack.hook);
// Start the workflow
Hooks.call(OIF.HOOKS.WORKFLOW.POST_PREPARE, Options);
DBG.Log('Post prepare hook called from attack hook', HooksToAttach.attack.hook, Options);
});
////////////////////////////////////////////////////////////
// Item Hook
////////////////////////////////////////////////////////////
HooksToAttach.item.id = Hooks.on(HooksToAttach.item.hook, async (arg1, arg2, arg3) =>
{
// Extract relevant information
let Workflow = [arg1, arg2, arg3];
let Options = await SystemSupporter.ExtractOptions(Workflow, 'item', HooksToAttach.item.hook);
// Start the workflow
Hooks.call(OIF.HOOKS.WORKFLOW.POST_PREPARE, Options);
DBG.Log('Post prepare hook called from item hook', HooksToAttach.item.hook, Options);
});
});
////////////////////////////////////////////////////////////
// Main workflow
////////////////////////////////////////////////////////////
Hooks.on(OIF.HOOKS.WORKFLOW.POST_PREPARE, async (options) =>
{
DBG.Log('Post prepare hook called', options);
// Check if there are tags to be used
if (options.tags.length > 0)
{
// Send tags to the handler
TagHandler.Dispatch(options);
}
});
Hooks.call(OIF.HOOKS.ATTACH_HOOKS);
Hooks.callAll("oifReady", game.modules.get(OIF.ID).api);
console.log("Automated Objects, Interactions and Effects is ready!!");
});
});

View File

@@ -0,0 +1,174 @@
export class ObjectsInteractionsFX
{
static ID = 'objects-interactions-fx';
static NAME = 'Automated Objects, Interactions and Effects';
static FLAGS = {
OIF: 'OIF',
}
static TEMPLATES = {
MASTER_TAGS_SETTINGS : `modules/${this.ID}/module/templates/MasterTagsSettings.hbs`,
MASTER_TAG_CONFIGURATION: `modules/${this.ID}/module/templates/MasterTagConfiguration.hbs`,
SETTINGS_SKELETON : `modules/${this.ID}/module/templates/SettingsSkeleton.hbs`,
CONFIG_SKELETON : `modules/${this.ID}/module/templates/ConfigSkeleton.hbs`,
}
static SOCKET;
static SETTINGS = {
GENERAL: {
ATTACH_HOOKS: {
ATTACK: 'hookAttachAttack',
ITEM : 'hookAttachItem',
},
MASTER_TAGS_SETTINGS : 'masterTagsSettings',
GENERAL_SETTINGS : 'generalSettings',
ACTOR_INVENTOR_SETTINGS : 'actorInventor',
DEFAULT_ATTACK_HOOK : 'defaultAttackHook',
DEFAULT_ITEM_HOOK : 'defaultItemHook',
USE_ANIMATIONS : 'useAnimations',
MELEE_ANIMATION_DELAY : 'animationDelay',
RANGED_ANIMATION_DELAY : 'rangedAnimationDelay',
DEFAULT_AMMUNITION_DESTRUCTION_CHANCE: 'defaultAmmunitionDestructionChance',
DEFAULT_THROWABLE_DESTRUCTION_CHANCE : 'defaultThrowableDestructionChance',
REMOVE_THROWABLE_ITEM : 'removeThrowableItem',
ADD_THROWABLE_TO_TARGET_INVENTORY : 'addThrowableToTargetInventory',
ADD_AMMUNITION_TO_TARGET_INVENTORY : 'addAmmunitionToTargetInventory',
CREATE_ITEM_PILES_ON_MISS : 'createItemPilesOnMiss',
CREATE_ITEM_PILES_ON_HIT : 'createItemPilesOnHit',
SNAP_CREATED_ITEM_PILES_TO_GRID : 'snapCreatedItemPilesToGrid',
MINIFY_ITEM_PILES_NAMES : 'minifyItemPilesNames',
POWERFUL_IMPACT_SHAKE_EFFECT : 'powerfulImpactShakeEffect',
SET_ELEVATION_OF_ITEM_PILES : 'setElevationOfItemPiles',
LIGHTING_ITEMS_AUTOMATION : 'lightingItemsAutomation',
DEVELOPER_MODE : 'developerMode',
},
ACTOR_INVENTOR: {
CURRENCY_GENERATOR: 'currencyGenerator',
CP_LOCATION : 'copperPiecesLocation',
SP_LOCATION : 'silverPiecesLocation',
GP_LOCATION : 'goldPiecesLocation',
PP_LOCATION : 'platinumPiecesLocation',
EP_LOCATION : 'electrumPiecesLocation',
},
MASTER_TAGS: {
CURRENT_TAG_PACK: 'currentTagPack',
}
}
static FILES = {
ORIGIN : 'data',
DATA_FOLDERS: {
DEFAULT_TAG_PACKS: `modules/${this.ID}/module/data/defaultTagPacks`,
ROOT : 'oif'
}
}
static HOOKS = {
CHANGE_SETTINGS: 'oif.ChangeSettings',
ATTACH_HOOKS : 'oif.attachHooks',
WORKFLOW: {
POST_PREPARE: 'oifWorkflowPostPrepare',
POST_EXECUTE: 'oifWorkflowPostExecute',
},
ITEM: {
LIGHTING: {
POST_PREPARE: 'oifItemLightingPostPrepare',
LIGHT: {
POST_APPLY: 'oifItemLightingLightPostApply',
POST_SOUND: 'oifPostItemLightingLightSound',
},
EXTINGUISH: {
POST_APPLY: 'oifItemLightingExtinguishPostApply',
POST_SOUND: 'oifPostItemLightingExtinguishSound',
},
},
},
WEAPON: {
MELEE: {
POST_PREPARE: 'oifWeaponMeleePostPrepare',
HIT: {
POST_ANIMATION : 'oifWeaponMeleeHitPostAnimation',
POST_INTERACTION: 'oifWeaponMeleeHitPostInteraction',
POST_SOUND : 'oifWeaponMeleeHitPostSound',
},
THROW: {
POST_ANIMATION : 'oifWeaponMeleeThrowPostAnimation',
POST_INTERACTION: 'oifWeaponMeleeThrowPostInteraction',
POST_SOUND : 'oifWeaponMeleeThrowPostSound',
},
},
RANGED: {
POST_PREPARE: 'oifWeaponRangedPostPrepare',
HIT: {
POST_ANIMATION : 'oifWeaponRangedHitPostAnimation',
POST_INTERACTION: 'oifWeaponRangedHitPostInteraction',
POST_SOUND : 'oifWeaponRangedHitPostSound',
}
},
},
}
static OPTIONAL_MODULES = {
MIDI_QOL: {
id : 'midi-qol',
name : 'MidiQOL',
active: false,
},
LEVELS: {
id : 'levels',
name : 'Levels',
active: false,
},
ITEM_PILES: {
id : 'item-piles',
name : 'Item Piles',
active: false,
},
TAGGER: {
id : 'tagger',
name : 'Tagger',
active: false,
},
TIDY_SHEET: {
id : 'tidy5e-sheet',
name : 'Tidy5e Sheet',
active: false,
}
}
static WORKFLOW = {
DATA: {
TYPE: {
ACTION: {
ATTACK: {
MELEE : 'meleeAttack',
RANGED: 'rangedAttack',
},
USE: {
ITEM: 'useItem',
}
}
}
}
}
static Initialize()
{
// Check if optional modules are loaded
for (const key in this.OPTIONAL_MODULES)
{
if (game.modules.get(this.OPTIONAL_MODULES[key].id)?.active)
{
this.OPTIONAL_MODULES[key].active = true;
}
else
{
console.warn(`OIF | module ${this.OPTIONAL_MODULES[key].name} not active - some features disabled`);
}
}
}
}

View File

@@ -0,0 +1,276 @@
import { GeneralSettings } from "./interface/GeneralSettings.js";
import { MasterTagsSettings } from "./interface/MasterTagsSettings.js";
import { ActorInventorSettings } from "./interface/ActorInventorSettings.js";
import { ObjectsInteractionsFX as OIF } from "./ObjectsInteractionsFX.js";
import { SettingsSkeleton } from "./library/skeletons/SettingsSkeleton.js";
import { SystemSupporter } from "./system/SystemSupporter.js";
export class Settings
{
static async Initialize()
{
//////////////////////////////////////////////////
// Register Master Tags menu
game.settings.registerMenu(OIF.ID, OIF.SETTINGS.GENERAL.MASTER_TAGS_SETTINGS, {
name: 'OIF.Settings.MasterTagsSettings.Title',
hint: 'OIF.Settings.MasterTagsSettings.Hint',
label: 'OIF.Settings.MasterTagsSettings.Label',
icon: 'fas fa-tags',
type: MasterTagsSettings,
restricted: true,
});
await MasterTagsSettings.Register();
//////////////////////////////////////////////////
// Register General Settings menu
game.settings.registerMenu(OIF.ID, OIF.SETTINGS.GENERAL.GENERAL_SETTINGS, {
name: 'OIF.Settings.GeneralSettings.Title',
hint: 'OIF.Settings.GeneralSettings.Hint',
label: 'OIF.Settings.GeneralSettings.Label',
icon: 'fas fa-cog',
type: GeneralSettings,
restricted: false,
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ATTACK, {
name: 'OIF.Settings.DefaultAttackHook.Title',
hint: 'OIF.Settings.DefaultAttackHook.Hint',
scope: 'world',
type: 'dropdown',
default: SystemSupporter.GetDefaultHookAttack(),
choices: SystemSupporter.GetPossibleHooksAttack()
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.ATTACH_HOOKS.ITEM, {
name: 'OIF.Settings.DefaultItemHook.Title',
hint: 'OIF.Settings.DefaultItemHook.Hint',
scope: 'world',
type: 'dropdown',
default: SystemSupporter.GetDefaultHookItem(),
choices: SystemSupporter.GetPossibleHooksItem()
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.USE_ANIMATIONS, {
name: 'OIF.Settings.UseAnimations.Title',
hint: 'OIF.Settings.UseAnimations.Hint',
scope: 'world',
type: 'checkbox',
default: true,
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.MELEE_ANIMATION_DELAY, {
name: 'OIF.Settings.MeleeAnimationDelay.Title',
hint: 'OIF.Settings.MeleeAnimationDelay.Hint',
scope: 'world',
type: 'slider',
range: {
min: 0,
max: 3000,
step: 1,
},
default: 1950,
excludesOn: [OIF.SETTINGS.GENERAL.USE_ANIMATIONS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.RANGED_ANIMATION_DELAY, {
name: 'OIF.Settings.RangedAnimationDelay.Title',
hint: 'OIF.Settings.RangedAnimationDelay.Hint',
scope: 'world',
type: 'slider',
range: {
min: 0,
max: 3000,
step: 1,
},
default: 850,
excludesOn: [OIF.SETTINGS.GENERAL.USE_ANIMATIONS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM, {
name: 'OIF.Settings.RemoveThrowableItem.Title',
hint: 'OIF.Settings.RemoveThrowableItem.Hint',
scope: 'world',
type: 'checkbox',
default: false,
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.ADD_THROWABLE_TO_TARGET_INVENTORY, {
name: 'OIF.Settings.AddThrowableToTargetInventory.Title',
hint: 'OIF.Settings.AddThrowableToTargetInventory.Hint',
scope: 'world',
type: 'checkbox',
default: false,
dependsOn: [OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.ADD_AMMUNITION_TO_TARGET_INVENTORY, {
name: 'OIF.Settings.AddAmmunitionToTargetInventory.Title',
hint: 'OIF.Settings.AddAmmunitionToTargetInventory.Hint',
scope: 'world',
type: 'checkbox',
default: false,
dependsOn: [OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS, {
name: 'OIF.Settings.CreateItemPilesOnMiss.Title',
hint: 'OIF.Settings.CreateItemPilesOnMiss.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.ITEM_PILES,
default: OIF.OPTIONAL_MODULES.ITEM_PILES.active,
dependsOn: [OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_HIT, {
name: 'OIF.Settings.CreateItemPilesOnHit.Title',
hint: 'OIF.Settings.CreateItemPilesOnHit.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.ITEM_PILES,
default: false,
dependsOn: [OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM],
excludesOn: [OIF.SETTINGS.GENERAL.ADD_THROWABLE_TO_TARGET_INVENTORY, OIF.SETTINGS.GENERAL.ADD_AMMUNITION_TO_TARGET_INVENTORY],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.SNAP_CREATED_ITEM_PILES_TO_GRID, {
name: 'OIF.Settings.SnapCreatedItemPilesToGrid.Title',
hint: 'OIF.Settings.SnapCreatedItemPilesToGrid.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.ITEM_PILES,
default: OIF.OPTIONAL_MODULES.ITEM_PILES.active,
dependsOn: [OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.MINIFY_ITEM_PILES_NAMES, {
name: 'OIF.Settings.MinifyItemPilesNames.Title',
hint: 'OIF.Settings.MinifyItemPilesNames.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.ITEM_PILES,
default: OIF.OPTIONAL_MODULES.ITEM_PILES.active,
dependsOn: [OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.DEFAULT_THROWABLE_DESTRUCTION_CHANCE, {
name: 'OIF.Settings.DefaultThrowableDestructionChance.Title',
hint: 'OIF.Settings.DefaultThrowableDestructionChance.Hint',
scope: 'world',
type: 'slider',
range: {
min: 0,
max: 100,
step: 1,
},
default: 0,
dependsOn: [OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.DEFAULT_AMMUNITION_DESTRUCTION_CHANCE, {
name: 'OIF.Settings.DefaultAmmunitionDestructionChance.Title',
hint: 'OIF.Settings.DefaultAmmunitionDestructionChance.Hint',
scope: 'world',
type: 'slider',
range: {
min: 0,
max: 100,
step: 1,
},
default: 50,
dependsOn: [OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.SET_ELEVATION_OF_ITEM_PILES, {
name: 'OIF.Settings.SetElevationOfItemPiles.Title',
hint: 'OIF.Settings.SetElevationOfItemPiles.Hint',
scope: 'world',
type: 'checkbox',
disabled: !(OIF.OPTIONAL_MODULES.LEVELS.active && OIF.OPTIONAL_MODULES.ITEM_PILES.active),
default: OIF.OPTIONAL_MODULES.LEVELS.active && OIF.OPTIONAL_MODULES.ITEM_PILES.active,
dependsOn: [OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS],
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.POWERFUL_IMPACT_SHAKE_EFFECT, {
name: 'OIF.Settings.PowerfulImpactShakeEffect.Title',
hint: 'OIF.Settings.PowerfulImpactShakeEffect.Hint',
scope: 'client',
type: 'checkbox',
default: true,
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.LIGHTING_ITEMS_AUTOMATION, {
name: 'OIF.Settings.LightingItemsAutomation.Title',
hint: 'OIF.Settings.LightingItemsAutomation.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.TAGGER,
disabled: !OIF.OPTIONAL_MODULES.TAGGER.active,
default: OIF.OPTIONAL_MODULES.TAGGER.active,
});
GeneralSettings.Register(OIF.SETTINGS.GENERAL.DEVELOPER_MODE, {
name: 'OIF.Settings.DeveloperMode.Title',
hint: 'OIF.Settings.DeveloperMode.Hint',
scope: 'world',
type: 'checkbox',
default: false,
});
//////////////////////////////////////////////////
// Register Actor Inventor Settings menu
game.settings.registerMenu(OIF.ID, OIF.SETTINGS.GENERAL.ACTOR_INVENTOR_SETTINGS, {
name: 'OIF.Settings.ActorInventorSettings.Title',
hint: 'OIF.Settings.ActorInventorSettings.Hint',
label: 'OIF.Settings.ActorInventorSettings.Label',
icon: 'fas fa-user',
type: ActorInventorSettings,
restricted: false,
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR, {
name: 'OIF.Settings.CurrencyGenerator.Title',
hint: 'OIF.Settings.CurrencyGenerator.Hint',
scope: 'world',
type: 'checkbox',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET.active,
default: OIF.OPTIONAL_MODULES.TIDY_SHEET.active,
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.CP_LOCATION, {
name: 'OIF.Settings.CpLocation.Title',
hint: 'OIF.Settings.CpLocation.Hint',
scope: 'world',
type: 'string',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET,
default: "system.currency.cp",
dependsOn: [OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR],
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.SP_LOCATION, {
name: 'OIF.Settings.SpLocation.Title',
hint: 'OIF.Settings.SpLocation.Hint',
scope: 'world',
type: 'string',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET,
default: "system.currency.sp",
dependsOn: [OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR],
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.GP_LOCATION, {
name: 'OIF.Settings.GpLocation.Title',
hint: 'OIF.Settings.GpLocation.Hint',
scope: 'world',
type: 'string',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET,
default: "system.currency.gp",
dependsOn: [OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR],
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.PP_LOCATION, {
name: 'OIF.Settings.PpLocation.Title',
hint: 'OIF.Settings.PpLocation.Hint',
scope: 'world',
type: 'string',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET,
default: "system.currency.pp",
dependsOn: [OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR],
});
ActorInventorSettings.Register(OIF.SETTINGS.ACTOR_INVENTOR.EP_LOCATION, {
name: 'OIF.Settings.EpLocation.Title',
hint: 'OIF.Settings.EpLocation.Hint',
scope: 'world',
type: 'string',
requiredModule: OIF.OPTIONAL_MODULES.TIDY_SHEET,
disabled: !OIF.OPTIONAL_MODULES.TIDY_SHEET,
default: "system.currency.ep",
dependsOn: [OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR],
});
GeneralSettings.UpdateSettings();
ActorInventorSettings.UpdateSettings();
}
}

View File

@@ -0,0 +1,411 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { ItemDropper} from "../library/ItemDropper.js";
import { InventoryManipulator } from "../library/InventoryManipulator.js";
import { GeneralSettings } from "../interface/GeneralSettings.js";
import { Helpers } from "../library/Helpers.js";
import { TokenLightingManipulator } from "../library/TokenLightingManipulator.js";
import { Debug as DBG } from "../library/Debug.js";
export class ItemAnimator
{
static async GetLandedPosAnimated(options, sequenceIdentifier)
{
let [effect] = Sequencer.EffectManager.getEffects({ name: sequenceIdentifier });
options.landedPosX = effect.targetPosition.x - options.gridSize / 2;
options.landedPosY = effect.targetPosition.y - options.gridSize / 2;
return options;
}
//////////////////////////////////////////////////
// Get a random position
//////////////////////////////////////////////////
static GetLandedPosRandomCorner(options)
{
let corners = Helpers.GetCornersOfToken(options.target.document);
let randomCorner = corners[Math.floor(Math.random() * corners.length)];
options.landedPosX = randomCorner.x;
options.landedPosY = randomCorner.y;
return options;
}
//////////////////////////////////////////////////
// Validate and prepare the data to be usede
// inside other functions, everything that needs
// to be validated and prepared should be here
//////////////////////////////////////////////////
static ValidateAndPrepare(options)
{
// Check if there is no targets
if (options.targets == undefined || options.targets.length == 0)
{
// Stop the workflow if there is no target
options.stopWorkflow = true;
return options;
}
// Get the first target (only valid)
options.target = options.targets[0];
// Calculate de distance between author and target
options.distance = canvas.grid.measureDistance(options.token, options.target);
// Get grid unit size
options.gridUnitSize = canvas.dimensions.distance;
// Get grid size
options.gridSize = canvas.dimensions.size;
return options;
}
/**
* * MeleeWeaponSingleAttack
* This is the function responsible for melee weapon attacks with
* single targets
*/
static async MeleeWeaponSingleAttack(options)
{
////////////////////////////////////////////////////////////
// Preparation
////////////////////////////////////////////////////////////
// Validate and prepare for the workflow
options = ItemAnimator.ValidateAndPrepare(options);
if (options.stopWorkflow == true) { return; }
Hooks.call(OIF.HOOKS.WEAPON.MELEE.POST_PREPARE, options);
// Check if item can be thrown at the current distance
if (options.system.isThrowable && options.distance >= options.system.meleeWeaponDistance + options.gridUnitSize)
{
// Check if the distance is below max distance
if (options.distance <= options.system.longDistance)
{
////////////////////////////////////////////////////////////
// Thrown Attack
////////////////////////////////////////////////////////////
// TODO: consult system rules to see if we should do a correction to the throw attack
// Check if animation should be played
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS) && options.throwAnimation != undefined)
{
// Define throw sequence to be played
let SequenceIdentifier = `${options.name}-throw-${options.token.document._id}`;
let SequenceEffect = new Sequence(OIF.ID)
.effect()
.file(options.throwAnimation.source)
.atLocation(options.token)
.stretchTo(options.target)
.name(SequenceIdentifier)
.missed(options.miss ?? false)
// Play throw sequence
await SequenceEffect.play();
// Get where the effect landed
options = await ItemAnimator.GetLandedPosAnimated(options, SequenceIdentifier);
}
else
{
// Get a random place to simulate where the projectile landed
options = await ItemAnimator.GetLandedPosRandomCorner(options);
}
//////////////////////////////////////////////////////////////
// Thrown Attack Interaction
//////////////////////////////////////////////////////////////
setTimeout(() => {
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS))
{
Hooks.call(OIF.HOOKS.WEAPON.MELEE.THROW.POST_ANIMATION, options);
}
// Create a copy of the item
let ItemCopy = options.item.toObject();
ItemCopy.system.quantity = 1;
let DidInteract = false;
// Check if the item should or not break
if(!(Helpers.RandomMax(100) <= GeneralSettings.Get(OIF.SETTINGS.GENERAL.DEFAULT_THROWABLE_DESTRUCTION_CHANCE)))
{
let CreateItemPileOnMiss = GeneralSettings.Get(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS);
let CreateItemPileOnHit = GeneralSettings.Get(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_HIT);
let AddThrowableToTargetInventory = GeneralSettings.Get(OIF.SETTINGS.GENERAL.ADD_THROWABLE_TO_TARGET_INVENTORY);
let RemoveThrowableItem = GeneralSettings.Get(OIF.SETTINGS.GENERAL.REMOVE_THROWABLE_ITEM);
let ShouldRemoveItem = false;
// Check if the attack missed and if a item pile should be created
if (options.miss && CreateItemPileOnMiss)
{
// Setup item pile position
let ItemPilePosition = {
x: options.landedPosX,
y: options.landedPosY,
}
// Drop item
ItemDropper.DropAt(options.item, 1, ItemPilePosition, options.target.document.elevation);
ShouldRemoveItem = true;
}
else if (AddThrowableToTargetInventory)
{
// Add item to target's inventory
InventoryManipulator.AddItem(options.target, options.item, 1);
ShouldRemoveItem = true;
}
else if (CreateItemPileOnHit)
{
options = ItemAnimator.GetLandedPosRandomCorner(options);
// Setup item pile position
let ItemPilePosition = {
x: options.landedPosX,
y: options.landedPosY,
}
// Drop item
ItemDropper.DropAt(options.item, 1, ItemPilePosition, options.target.document.elevation);
ShouldRemoveItem = true;
}
// Check if item should be removed
if (RemoveThrowableItem && ShouldRemoveItem)
{
// Remove item from author inventory
InventoryManipulator.RemoveItem(options.token, options.item, 1);
}
DidInteract = true;
}
// Check if poweful impact effect should be played
if ((options.throwAnimation.powerful || options.tags.includes('powerful')) && GeneralSettings.Get(OIF.SETTINGS.GENERAL.POWERFUL_IMPACT_SHAKE_EFFECT))
{
OIF_SOCKET.executeForEveryone('ScreenShake');
DidInteract = true;
}
if (DidInteract)
{
Hooks.call(OIF.HOOKS.WEAPON.MELEE.THROW.POST_INTERACTION, options);
}
}, options.throwAnimation?.delay?? 0);
}
else
{
ui.notifications.error(game.i18n.localize("OIF.Attack.Melee.Error.TooFar"));
}
}
else
{
// Check if the distance is within max distance
if (options.distance < options.system.meleeWeaponDistance + options.gridUnitSize)
{
////////////////////////////////////////////////////////////
// Melee Attack
////////////////////////////////////////////////////////////
// Check if the animation should be played
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS) && options.meleeAnimation != undefined)
{
// Define melee sequence to be played
let SequenceIdentifier = `${options.name}-melee-${options.token.document._id}`;
let SequenceEffect = new Sequence(OIF.ID)
.effect()
.file(options.meleeAnimation.source)
.atLocation(options.token)
.stretchTo(options.target)
.name(SequenceIdentifier)
// Play melee sequence
await SequenceEffect.play();
}
////////////////////////////////////////////////////////////
// Melee Attack Interaction
////////////////////////////////////////////////////////////
setTimeout(() => {
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS))
{
Hooks.call(OIF.HOOKS.WEAPON.MELEE.HIT.POST_ANIMATION, options);
}
let DidInteract = false;
// Check if poweful impact effect should be played
if ((options.meleeAnimation.powerful || options.tags.includes('powerful')) && GeneralSettings.Get(OIF.SETTINGS.GENERAL.POWERFUL_IMPACT_SHAKE_EFFECT))
{
OIF_SOCKET.executeForEveryone('ScreenShake');
DidInteract = true;
}
if (DidInteract)
{
Hooks.call(OIF.HOOKS.WEAPON.MELEE.HIT.POST_INTERACTION, options);
}
}, options.meleeAnimation?.delay ?? 0);
}
else
{
ui.notifications.error(game.i18n.localize("OIF.Attack.Melee.Error.TooFar"));
}
}
}
/**
* * RangedWeaponSingleAttack
* This is the function responsible for ranged weapon attacks with
* single targets
*/
static async RangedWeaponSingleAttack(options)
{
////////////////////////////////////////////////////////////
// Preparation
////////////////////////////////////////////////////////////
// Validate and prepare for the workflow
options = ItemAnimator.ValidateAndPrepare(options);
if (options.stopWorkflow == true) { return; }
Hooks.call(OIF.HOOKS.WEAPON.RANGED.POST_PREPARE, options);
// Check if the distance is below the maximum distance
if (options.distance <= options.system.longDistance)
{
// Check if item has ammo property
if (options.system.isConsumeAmmo)
{
////////////////////////////////////////////////////////////
// Ranged Ammo Attack
////////////////////////////////////////////////////////////
// Check if item has ammo set
if (options.system.ammoItem == undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Attack.Ranged.Error.NoAmmo'));
console.error('Could not find the ammunition item!');
return;
}
// Create a copy of the ammo item
options.ammoItemCopy = await options.token.actor.getEmbeddedDocument('Item', options.system.ammoItem);
// Check if animation should be played
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS) && options.rangedAnimation != undefined)
{
// Define ranged sequence to be played
let SequenceIdentifier = `${options.name}-ranged-${options.token.document._id}`;
let SequenceEffect = new Sequence(OIF.ID)
.effect()
.file(options.rangedAnimation.source)
.atLocation(options.token)
.stretchTo(options.target)
.name(SequenceIdentifier)
.missed(options.miss ?? false)
// Play ranged sequence
await SequenceEffect.play();
// Get where the effect landed
options = await ItemAnimator.GetLandedPosAnimated(options, SequenceIdentifier);
}
else
{
// Get a random place to simulate where the projectile landed
options = await ItemAnimator.GetLandedPosRandomCorner(options);
}
////////////////////////////////////////////////////////////
// Ranged Attack Interaction
////////////////////////////////////////////////////////////
setTimeout(() => {
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS))
{
Hooks.call(OIF.HOOKS.WEAPON.RANGED.HIT.POST_ANIMATION, options);
}
let DidInteract = false;
// Check if the item should or not break
if (!(Helpers.RandomMax(100) <= GeneralSettings.Get(OIF.SETTINGS.GENERAL.DEFAULT_AMMUNITION_DESTRUCTION_CHANCE)))
{
let CreateItemPileOnMiss = GeneralSettings.Get(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_MISS);
let CreateItemPileOnHit = GeneralSettings.Get(OIF.SETTINGS.GENERAL.CREATE_ITEM_PILES_ON_HIT);
let AddAmmunitionToTargetInventory = GeneralSettings.Get(OIF.SETTINGS.GENERAL.ADD_AMMUNITION_TO_TARGET_INVENTORY);
let ShouldRemoveItem = false;
// Check if the attack missed and if a item pile should be created
if (options.miss && CreateItemPileOnMiss)
{
// Setup item pile position
let ItemPilePosition = {
x: options.landedPosX,
y: options.landedPosY,
}
// Drop item
ItemDropper.DropAt(options.ammoItemCopy, 1, ItemPilePosition, options.target.document.elevation);
}
else if (AddAmmunitionToTargetInventory)
{
// Add item to target's inventory
InventoryManipulator.AddItem(options.target, options.ammoItemCopy, 1);
}
else if (CreateItemPileOnHit)
{
options = ItemAnimator.GetLandedPosRandomCorner(options);
// Setup item pile position
let ItemPilePosition = {
x: options.landedPosX,
y: options.landedPosY,
}
// Drop item
ItemDropper.DropAt(options.ammoItemCopy, 1, ItemPilePosition, options.target.document.elevation);
}
DidInteract = true;
}
// Check if poweful impact effect should be played
if ((options.rangedAnimation.powerful || options.tags.indexOf('powerful') > 0) && GeneralSettings.Get(OIF.SETTINGS.GENERAL.POWERFUL_IMPACT_SHAKE_EFFECT))
{
OIF_SOCKET.executeForEveryone('ScreenShake');
DidInteract = true;
}
if (DidInteract)
{
Hooks.call(OIF.HOOKS.WEAPON.RANGED.HIT.POST_INTERACTION, options);
}
}, options.rangedAnimation?.delay ?? 0);
}
else
{
////////////////////////////////////////////////////////////
// Ranged Attack
////////////////////////////////////////////////////////////
// Check if animation should be played
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.USE_ANIMATIONS) && options.rangedAnimation != undefined)
{
// Define ranged sequence to be played
let SequenceIdentifier = `${options.name}-ranged-${options.token.document._id}`;
let SequenceEffect = new Sequence(OIF.ID)
.effect()
.file(options.rangedAnimation.source)
.atLocation(options.token)
.stretchTo(options.target)
.name(SequenceIdentifier)
.missed(options.miss ?? false)
// Play ranged sequence
await SequenceEffect.play();
Hooks.call(OIF.HOOKS.WEAPON.RANGED.HIT.POST_ANIMATION, options);
}
}
}
else
{
ui.notifications.error(game.i18n.localize('OIF.Attack.Ranged.Error.TooFar'));
}
}
}

View File

@@ -0,0 +1,36 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { ObjectsInteractionsFXData } from "../data/ObjectsInteractionsFXData.js";
import { ActorInventorSettings } from "../interface/ActorInventorSettings.js";
Hooks.on("oifReady", () => {
Hooks.on("createToken", async (actor) => {
if (game.user.isGM) {
if (actor != null && actor != undefined && actor.actor.type == "npc")
{
actor = actor.actor;
let Tags = ItemTags.Get(actor);
// Make sure we are checking for tags, not strange promises..
if ((typeof Tags) == Array)
{
if (Tags.indexOf("generateCurrency") > -1 && ActorInventorSettings.Get(OIF.SETTINGS.ACTOR_INVENTOR.CURRENCY_GENERATOR))
{
let CpLocation = game.settings.get(OIF.ID, OIF.SETTINGS.ACTOR_INVENTOR.CP_LOCATION);
let SpLocation = game.settings.get(OIF.ID, OIF.SETTINGS.ACTOR_INVENTOR.SP_LOCATION);
let GpLocation = game.settings.get(OIF.ID, OIF.SETTINGS.ACTOR_INVENTOR.GP_LOCATION);
let PpLocation = game.settings.get(OIF.ID, OIF.SETTINGS.ACTOR_INVENTOR.PP_LOCATION);
let EpLocation = game.settings.get(OIF.ID, OIF.SETTINGS.ACTOR_INVENTOR.EP_LOCATION);
actor.update({
[CpLocation]: Math.floor(Math.random() * (getProperty(actor, CpLocation) + 1)),
[SpLocation]: Math.floor(Math.random() * (getProperty(actor, SpLocation) + 1)),
[GpLocation]: Math.floor(Math.random() * (getProperty(actor, GpLocation) + 1)),
[PpLocation]: Math.floor(Math.random() * (getProperty(actor, PpLocation) + 1)),
[EpLocation]: Math.floor(Math.random() * (getProperty(actor, EpLocation) + 1))
})
}
}
}
}
});
});

View File

@@ -0,0 +1,27 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js"
export class ObjectsInteractionsFXData
{
static async SaveMasterTags(data, name, path)
{
const NewFile = new File([JSON.stringify(data)], name, { type: 'application/json' });
await FilePicker.upload(OIF.FILES.ORIGIN, path, NewFile, {});
}
static async LoadJSON(path)
{
return await foundry.utils.fetchJsonWithTimeout(path);
}
static async SaveJSON(data, name, path)
{
const NewFile = new File([JSON.stringify(data)], name, { type: 'application/json' });
await FilePicker.upload(OIF.FILES.ORIGIN, path, NewFile, {});
}
static async GetUserMasterTags(pack)
{
let UserPacks = await ObjectsInteractionsFXData.LoadJSON(`${OIF.FILES.DATA_FOLDERS.ROOT}/TagPacks.json`);
return UserPacks[pack];
}
}

View File

@@ -0,0 +1,44 @@
import { SettingsSkeleton } from "../library/skeletons/SettingsSkeleton.js";
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
export class ActorInventorSettings extends SettingsSkeleton {
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
id: 'oif-actor-inventor-settings',
title: game.i18n.localize('OIF.Settings.ActorInventorSettings.Title'),
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
activateListeners(html)
{
super.activateListeners(html, 'oif-actor-inventor-settings');
}
static Register(name, options)
{
SettingsSkeleton._protoRegister('oif-actor-inventor-settings', name, options);
}
static Get(name)
{
return SettingsSkeleton._protoGet('oif-actor-inventor-settings', name);
}
static UpdateSettings()
{
SettingsSkeleton._protoUpdateSettings('oif-actor-inventor-settings');
}
getData(options)
{
return SettingsSkeleton._protoGetData('oif-actor-inventor-settings', options);
}
async _updateObject(event, formData)
{}
}

View File

@@ -0,0 +1,46 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { ActorInventorSettings } from "./ActorInventorSettings.js";
import { SettingsSkeleton } from "../library/skeletons/SettingsSkeleton.js";
export class GeneralSettings extends SettingsSkeleton {
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
id: 'oif-general-settings',
title: game.i18n.localize('OIF.Settings.GeneralSettings.Title')
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
activateListeners(html)
{
super.activateListeners(html, 'oif-general-settings');
}
static Register(name, options)
{
SettingsSkeleton._protoRegister('oif-general-settings', name, options);
}
static Get(name)
{
return SettingsSkeleton._protoGet('oif-general-settings', name);
}
static UpdateSettings()
{
SettingsSkeleton._protoUpdateSettings('oif-general-settings');
}
getData(options)
{
return SettingsSkeleton._protoGetData('oif-general-settings', options)
}
async _updateObject(event, formData)
{}
}

View File

@@ -0,0 +1,368 @@
import { MasterTagsSettings } from "./MasterTagsSettings.js";
import { ConfigSkeleton } from "../library/skeletons/ConfigSkeleton.js";
export class MasterTagConfiguration extends ConfigSkeleton
{
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
id: 'oif-master-tag-configuration',
title: game.i18n.localize('OIF.Settings.MasterTagConfiguration.Title'),
tag: '',
caller: null,
updaterFunction: MasterTagsSettings.UpdateTag,
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
static MAX_DELAY = 3000;
getData(options)
{
let ReturnedConfiguration;
let DefaultConfigurationHeader =
[
{
disabled: false,
name: 'enabled',
title: 'OIF.Settings.MasterTagConfiguration.Options.Enabled.Title',
type: 'checkbox',
value: this.options.configurationData.enabled,
},
{
disabled: false,
name: 'type',
title: 'OIF.Settings.MasterTagConfiguration.Options.Type.Title',
type: 'dropdown',
value: this.options.configurationData.type,
choices:
[
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Type.Choices.None',
value: 'none',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Type.Choices.MeleeAttack',
value: 'meleeAttack'
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Type.Choices.RangedAttack',
value: 'rangedAttack'
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Type.Choices.Lighting',
value: 'lighting'
}
]
}
]
switch (this.options.configurationData.type)
{
case 'meleeAttack':
case 'rangedAttack':
ReturnedConfiguration =
[
...DefaultConfigurationHeader,
{
disabled: false,
name: 'meleeAnimation.source',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.MeleeAnimation.Source.Title',
type: 'string',
value: this.options.configurationData?.meleeAnimation?.source ?? '',
},
{
disabled: false,
name: 'meleeAnimation.delay',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.MeleeAnimation.Delay.Title',
type: 'slider',
value: this.options.configurationData?.meleeAnimation?.delay ?? 0,
range:
{
min: 0,
max: MasterTagConfiguration.MAX_DELAY,
step: 1,
},
},
{
disabled: false,
name: 'throwAnimation.source',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.ThrowAnimation.Source.Title',
type: 'string',
value: this.options.configurationData?.throwAnimation?.source ?? '',
},
{
disabled: false,
name: 'throwAnimation.delay',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.ThrowAnimation.Delay.Title',
type: 'slider',
value: this.options.configurationData?.throwAnimation?.delay ?? 0,
range:
{
min: 0,
max: MasterTagConfiguration.MAX_DELAY,
step: 1,
},
},
{
disabled: false,
name: 'returnAnimation.source',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.ReturnAnimation.Source.Title',
type: 'string',
value: this.options.configurationData?.returnAnimation?.source ?? '',
},
{
disabled: false,
name: 'returnAnimation.delay',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.ReturnAnimation.Delay.Title',
type: 'slider',
value: this.options.configurationData?.returnAnimation?.delay ?? 0,
range:
{
min: 0,
max: MasterTagConfiguration.MAX_DELAY,
step: 1,
},
},
{
disabled: false,
name: 'rangedAnimation.source',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.RangedAnimation.Source.Title',
type: 'string',
value: this.options.configurationData?.rangedAnimation?.source ?? '',
},
{
disabled: false,
name: 'rangedAnimation.delay',
title: 'OIF.Settings.MasterTagConfiguration.Options.Weapon.RangedAnimation.Delay.Title',
type: 'slider',
value: this.options.configurationData?.rangedAnimation?.delay ?? 0,
range:
{
min: 0,
max: MasterTagConfiguration.MAX_DELAY,
step: 1,
},
}
]
break;
case 'lighting':
ReturnedConfiguration =
[
...DefaultConfigurationHeader,
{
disabled: false,
name: 'light.animationType',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Hint',
type: 'dropdown',
value: this.options.configurationData?.light?.animationType ?? 'none',
choices:
[
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.None',
value: 'none',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.Torch',
value: 'torch',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.Pulse',
value: 'pulse',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.Chroma',
value: 'chroma',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.PulsingWave',
value: 'wave',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.SwirlingFog',
value: 'fog',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.Sunburst',
value: 'sunburst',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.LightDome',
value: 'dome',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.MysteriousEmanation',
value: 'emanation',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.HexaDome',
value: 'hexa',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.GhostlyLight',
value: 'ghost',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.EnergyField',
value: 'energy',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.RoilingMass',
value: 'roiling',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.BlackHole',
value: 'hole',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.Vortex',
value: 'vortex',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.BewitchingWave',
value: 'witchwave',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.SwirlingRainbow',
value: 'rainbowswirl',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.RadialRainbow',
value: 'radialrainbow',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.FairyLight',
value: 'fairy',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.ForceGrid',
value: 'grid',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.StarLight',
value: 'starlight',
},
{
name: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Type.Choices.SmokePatch',
value: 'smokepatch',
}
]
},
{
disabled: false,
name: 'light.animationSpeed',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Speed.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Speed.Hint',
type: 'slider',
value: this.options.configurationData?.light?.animationSpeed ?? 0,
range:
{
min: 0,
max: 10,
step: 1,
}
},
{
disabled: false,
name: 'light.animationIntensity',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Intensity.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Intensity.Hint',
type: 'slider',
value: this.options.configurationData?.light?.animationIntensity ?? 1,
range:
{
min: 1,
max: 10,
step: 1,
}
},
{
disabled: false,
name: 'light.animationReverse',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Reverse.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Animation.Reverse.Hint',
type: 'checkbox',
value: this.options.configurationData?.light?.animationReverse ?? false,
},
{
disabled: false,
name: 'light.color',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Color.Title',
type: 'color',
value: this.options.configurationData?.light?.color ?? '#000000',
},
{
disabled: false,
name: 'light.intensity',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Intensity.Title',
type: 'slider',
value: this.options.configurationData?.light?.intensity ?? 0.5,
range:
{
min: 0,
max: 1,
step: 0.05,
}
},
{
disabled: false,
name: 'light.angle',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Angle.Title',
type: 'slider',
value: this.options.configurationData?.light?.angle ?? 360,
range:
{
min: 0,
max: 360,
step: 1,
}
},
{
disabled: false,
name: 'icons.unlit',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Icons.Unlit.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Icons.Unlit.Hint',
type: 'icon',
value: this.options.configurationData?.icons?.unlit ?? '',
},
{
disabled: false,
name: 'icons.lit',
title: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Icons.Lit.Title',
hint: 'OIF.Settings.MasterTagConfiguration.Options.Lighting.Icons.Lit.Hint',
type: 'icon',
value: this.options.configurationData?.icons?.lit ?? '',
}
]
break;
case 'special':
ReturnedConfiguration =
[
]
break;
default:
ReturnedConfiguration =
{
...DefaultConfigurationHeader,
}
break;
}
return {
data: this.options.configurationData,
config: ReturnedConfiguration
}
}
async activateListeners(html)
{
super.activateListeners(html);
}
async _updateObject(event, formData)
{}
}

View File

@@ -0,0 +1,929 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { ObjectsInteractionsFXData as OIFD } from '../data/ObjectsInteractionsFXData.js';
import { MasterTagConfiguration } from "./MasterTagConfiguration.js";
import { SystemSupporter } from "../system/SystemSupporter.js";
import { TagHandler } from "../tags/TagHandler.js";
import { Debug as DBG } from "../library/Debug.js";
export class MasterTagsSettings extends FormApplication {
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
closeOnSubmit: false,
height: 'auto',
width: 600,
id: 'oif-master-tags-settings',
submitOnChange: true,
template: OIF.TEMPLATES.MASTER_TAGS_SETTINGS,
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Title'),
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
////////////////////////////////////////////////////////////
// Data
////////////////////////////////////////////////////////////
static PackHeaders = {};
static CurrentPackHeader = {};
static Tags = {};
static ResultTags = [];
static Changed = false;
// Interface
static SearchString = '';
static FocusOn = 'input';
////////////////////////////////////////////////////////////
// Register the settings related to master tags
////////////////////////////////////////////////////////////
static async Register()
{
// Prepare the internal foundry setting
let FoundrySettingOptions = {
scope : 'world',
default: '',
config : false,
};
// Register setting inside foundry
game.settings.register(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK, FoundrySettingOptions);
// Assign the default tag pack
let Setting = await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK);
if (Setting == '')
{
await game.settings.set(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK, await SystemSupporter.GetDefaultTagPack());
Setting = await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK);
}
}
////////////////////////////////////////////////////////////
// Load tag pack from object
////////////////////////////////////////////////////////////
static async LoadTagPackFromObject(data)
{
// Build the header for the pack
let PackHeader =
{
id : data.id,
name : data.name,
default : data.default,
}
// Verifies if the required modules are present
if (data.requires != undefined)
{
data.requires.forEach((element) => {
if (!game.modules.get(element)?.active)
{
// Disable the pack if the required modules are not present
// and set the disabled message
PackHeader.disabled = true;
PackHeader.disabledMessage = game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.DisabledMessage').replace('${modules}', data.requires);
return;
}
});
}
// Check if the pack is the selected one
if (await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK) == data.id)
{
PackHeader.selected = true;
MasterTagsSettings.CurrentPackHeader = PackHeader;
}
// Insert the pack into pack list
MasterTagsSettings.PackHeaders[PackHeader.id] = PackHeader;
}
////////////////////////////////////////////////////////////
// Load tag pack from file
////////////////////////////////////////////////////////////
static async LoadTagPackFromFile(file)
{
// Build the header for the pack
let PackData = await OIFD.LoadJSON(file);
let PackHeader =
{
location: file,
id : PackData.id,
name : PackData.name,
default : PackData.default,
}
// Verifies if the required modules are present
if (PackData.requires != undefined)
{
PackData.requires.forEach((element) => {
if (!game.modules.get(element)?.active)
{
// Disable the pack if the required modules are not present
// and set the disabled message
PackHeader.disabled = true;
PackHeader.disabledMessage = game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.DisabledMessage').replace('${modules}', PackData.requires);
return;
}
});
}
// Check if the pack is the selected one
if (await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK) == PackData.id)
{
PackHeader.selected = true;
MasterTagsSettings.CurrentPackHeader = PackHeader;
}
// Insert the pack into pack list
MasterTagsSettings.PackHeaders[PackHeader.id] = PackHeader;
}
////////////////////////////////////////////////////////////
// Load default tag packs
////////////////////////////////////////////////////////////
static async LoadDefaultTagPacks()
{
const DefaultTagPacks =
[
'Empty',
'FantasyJB2AComplete',
'FantasyJB2AFree',
'FantasyNoAnimations'
];
for (const path of DefaultTagPacks)
{
await MasterTagsSettings.LoadTagPackFromFile(`modules/${OIF.ID}/module/data/defaultTagPacks/${path}.json`);
}
}
////////////////////////////////////////////////////////////
// Load tags from a registered pack
////////////////////////////////////////////////////////////
static async LoadTags(pack)
{
// Check if the pack exists
if (MasterTagsSettings.PackHeaders[pack] == undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Load.Error.NotFound').replace('${pack}', pack));
pack = "Empty"
await game.settings.set(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK, pack);
}
// Load the tags from the pack
let Data = {};
if (MasterTagsSettings.PackHeaders[pack].default)
{
// Load the tags from the default pack
Data = await OIFD.LoadJSON(MasterTagsSettings.PackHeaders[pack].location);
}
else
{
// Load the tags from the user pack
const UserPacks = await OIFD.LoadJSON(`${OIF.FILES.DATA_FOLDERS.ROOT}/TagPacks.json`);
Data = UserPacks[pack];
}
MasterTagsSettings.CurrentPackHeader = MasterTagsSettings.PackHeaders[pack];
MasterTagsSettings.Tags = Data.tags;
MasterTagsSettings.ResultTags = MasterTagsSettings.Tags;
await game.settings.set(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK, pack);
// Update the tags at the tag handler
await TagHandler.UpdateTags(MasterTagsSettings.Tags);
// Set a flag to indicate that the tags have changed
MasterTagsSettings.Changed = false;
}
////////////////////////////////////////////////////////////
// Updates a single tag
////////////////////////////////////////////////////////////
static async UpdateTag(tag, data)
{
// Update the tag
MasterTagsSettings.Tags[tag] = data;
MasterTagsSettings.ResultTags[tag] = data;
// Update the tags at the tag handler
TagHandler.UpdateTags(MasterTagsSettings.Tags);
// Set a flag to indicate that the tags have changed
MasterTagsSettings.Changed = true;
}
////////////////////////////////////////////////////////////
// Save user packs
////////////////////////////////////////////////////////////
static async SaveUserPacks()
{
// Put all the packs into a single file
let Data = {}
for (const [key, value] of Object.entries(MasterTagsSettings.PackHeaders))
{
// Skip the default packs and the current pack
if (!value.default && value.id != MasterTagsSettings.CurrentPackHeader.id)
{
let PackData = await OIFD.GetUserMasterTags(value.id);
Data[PackData.id] = PackData;
}
}
// Insert the current pack data if it is not a default pack
if (!MasterTagsSettings.CurrentPackHeader.default)
{
let CurrentPackData =
{
...MasterTagsSettings.CurrentPackHeader,
tags: MasterTagsSettings.Tags
}
CurrentPackData.selected = undefined;
CurrentPackData.disabled = undefined;
CurrentPackData.disabledMessage = undefined;
CurrentPackData.location = undefined;
Data[MasterTagsSettings.CurrentPackHeader.id] = CurrentPackData;
}
MasterTagsSettings.Changed = false;
// Save the file
await OIFD.SaveJSON(Data, 'TagPacks.json', OIF.FILES.DATA_FOLDERS.ROOT);
// Propagate changes
OIF_SOCKET.executeForOthers('LoadFromConfig');
}
////////////////////////////////////////////////////////////
// Load user packs
////////////////////////////////////////////////////////////
static async LoadUserPacks()
{
// Load the file
let Data = await OIFD.LoadJSON(`${OIF.FILES.DATA_FOLDERS.ROOT}/TagPacks.json`);
// Iterate through all packs
for (const [key, value] of Object.entries(Data))
{
let PackHeader =
{
id : value.id,
name: value.name,
selected: value.id == await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK) ? true : undefined,
}
MasterTagsSettings.PackHeaders[value.id] = PackHeader;
}
}
////////////////////////////////////////////////////////////
// Load everything
static async LoadFromConfig()
{
// Load default packs
await MasterTagsSettings.LoadDefaultTagPacks();
// Load user packs
await MasterTagsSettings.LoadUserPacks();
// Get the current tag pack
let CurrentTagPack = await game.settings.get(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK);
// Is the pack disabled?
if (MasterTagsSettings.PackHeaders[CurrentTagPack]?.disabled)
{
// Reset to default
MasterTagsSettings.PackHeaders[CurrentTagPack].selected = false;
CurrentTagPack = 'Emtpy';
}
// Load the tags
await MasterTagsSettings.LoadTags(CurrentTagPack);
DBG.Log('Loaded tags from config', MasterTagsSettings.Tags);
}
////////////////////////////////////////////////////////////
// Internal function that filters the tags
////////////////////////////////////////////////////////////
static async _filterTags()
{
let FilteredTags = Object.keys(MasterTagsSettings.Tags)
.filter((key) => key == MasterTagsSettings.SearchString || key.toLowerCase().includes(MasterTagsSettings.SearchString.toLowerCase()))
.reduce((obj, key) => {
return Object.assign(obj, {
[key]: MasterTagsSettings.Tags[key]
});
}, {});
return FilteredTags;
}
////////////////////////////////////////////////////////////
// Internal function that creates a new pack
////////////////////////////////////////////////////////////
async _createNewPack(id, name, tags)
{
// Current pack is no longer selected
MasterTagsSettings.CurrentPackHeader.selected = false;
MasterTagsSettings.PackHeaders[MasterTagsSettings.CurrentPackHeader.id].selected = false;
// Create the new pack
let NewTagPackHeader = {
id : id,
name : name,
selected: true
};
// Add the pack to the list
MasterTagsSettings.PackHeaders[id] = NewTagPackHeader;
MasterTagsSettings.CurrentPackHeader = NewTagPackHeader;
MasterTagsSettings.Tags = tags;
// Update the tags at the tag handler
TagHandler.UpdateTags(MasterTagsSettings.Tags);
// Set the new pack as the current pac
await game.settings.set(OIF.ID, OIF.SETTINGS.MASTER_TAGS.CURRENT_TAG_PACK, id);
MasterTagsSettings.Changed = true;
this.render();
}
////////////////////////////////////////////////////////////
// Handle pack selection
////////////////////////////////////////////////////////////
async _handlePackSelection(event)
{
let ClickedElement = $(event.currentTarget)[0];
Object.values(MasterTagsSettings.PackHeaders).forEach((element) =>
{
element.selected = element.id == ClickedElement.value ? true : false;
});
await MasterTagsSettings.LoadTags(ClickedElement.value);
// Propagate changes
OIF_SOCKET.executeForOthers('LoadFromConfig');
this.render();
}
////////////////////////////////////////////////////////////
// Load user packs
////////////////////////////////////////////////////////////
async _handleTagSearching(event)
{
if (event.key == 'Enter')
{
MasterTagsSettings.SearchString = event.target.value;
if (MasterTagsSettings.SearchString != '')
{
MasterTagsSettings.ResultTags = await MasterTagsSettings._filterTags();
}
else
{
MasterTagsSettings.ResultTags = MasterTagsSettings.Tags;
}
MasterTagsSettings.FocusOn = 'search';
this.render();
}
}
////////////////////////////////////////////////////////////
// Handle tag creation
////////////////////////////////////////////////////////////
async _handleTagCreation(event)
{
if (event.key == 'Enter')
{
let CurrentTag = event.target.value;
if (MasterTagsSettings.Tags[CurrentTag] == undefined || MasterTagsSettings.Tags[CurrentTag] == null)
{
MasterTagsSettings.Tags[CurrentTag] = {
enabled: false,
masterDeco: true
};
MasterTagsSettings.ResultTags = await MasterTagsSettings._filterTags();
TagHandler.UpdateTags(MasterTagsSettings.Tags);
}
else
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Creation.Error.Duplicated'))
}
MasterTagsSettings.FocusOn = 'input';
MasterTagsSettings.Changed = true;
this.render();
}
}
////////////////////////////////////////////////////////////
// Handle tag configuration
////////////////////////////////////////////////////////////
async _handleTagConfiguration(event)
{
let CurrentTagName = event.target.parentElement.parentElement.id;
let CurrentTag = MasterTagsSettings.Tags[CurrentTagName];
let ConfigurationInterface = new MasterTagConfiguration;
ConfigurationInterface.render(true,
{
configurationData: CurrentTag,
caller: this,
dataNameAtSingleton: CurrentTagName
});
this.render();
}
////////////////////////////////////////////////////////////
// Handle tag deletion
////////////////////////////////////////////////////////////
async _handleTagDeletion(event)
{
let CurrentTag = event.target.parentElement.parentElement.id;
let ConfirmDialog = Dialog.confirm({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Deletion.Title').replace('${tag}', CurrentTag),
content: `<p style="color:red">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Deletion.Description')}</p`,
no: async () =>
{
this.render();
},
yes: async () =>
{
delete MasterTagsSettings.Tags[CurrentTag];
MasterTagsSettings.ResultTags = await MasterTagsSettings._filterTags();
TagHandler.UpdateTags(MasterTagsSettings.Tags);
MasterTagsSettings.Changed = true;
this.render();
},
defaultYes: false
});
}
////////////////////////////////////////////////////////////
// Handle tag pack deletion
////////////////////////////////////////////////////////////
async _handleTagPackDeletion(event)
{
let ConfirmDialog = Dialog.confirm({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Delete.Title').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name),
content: `<p style="color:red">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Delete.Description')}</p`,
no: async () =>
{
this.render();
},
yes: async () =>
{
delete MasterTagsSettings.PackHeaders[MasterTagsSettings.CurrentPackHeader.id];
await MasterTagsSettings.LoadTags("Empty");
await MasterTagsSettings.SaveUserPacks();
this.render();
},
defaultYes: false
});
}
////////////////////////////////////////////////////////////
// Handle tag pack rename
////////////////////////////////////////////////////////////
async _handleTagPackRenaming(event)
{
let NewPackDialog = new Dialog({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Title'),
content: `
<label for="oif-pack-new-id">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Id')}</label>
<input type="text" id="oif-pack-new-id" name="oif-pack-new-id" value="${MasterTagsSettings.CurrentPackHeader.id}">
<label for="oif-pack-new-name">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Name')}</label>
<input type="text" id="oif-pack-new-name" name="oif-pack-new-name" value="${MasterTagsSettings.CurrentPackHeader.name}">
`,
buttons: {
rename: {
icon: '<i class="fas fa-pen"></i>',
label: game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Label'),
callback: async (html) => {
let NewPackID = html.find('#oif-pack-new-id').val();
let NewPackName = html.find('#oif-pack-new-name').val();
if (NewPackID == '' || NewPackName == '')
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Error.EmptyValues'));
}
else if (NewPackID != MasterTagsSettings.CurrentPackHeader.id || NewPackName != MasterTagsSettings.CurrentPackHeader.name)
{
let DoesNameCollide = false;
Object.values(MasterTagsSettings.PackHeaders).every((element) => {
if (element.name == NewPackName)
{
DoesNameCollide = true;
return false;
}
return true;
})
if (MasterTagsSettings.PackHeaders[NewPackID] != undefined && DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Error.BothAlreadyExist'));
}
else if (MasterTagsSettings.PackHeaders[NewPackID] != undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Error.IDAlreadyExist'));
}
else if (DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Rename.Error.NameAlreadyExist'));
}
else
{
// Rename the pack
this.render();
}
}
}
}
}
});
NewPackDialog.render(true);
}
////////////////////////////////////////////////////////////
// Handle tag pack export
////////////////////////////////////////////////////////////
async _handleTagPackExport(event)
{
let CurrentPackData =
{
...MasterTagsSettings.CurrentPackHeader,
tags: MasterTagsSettings.Tags
}
CurrentPackData.selected = undefined;
CurrentPackData.disabled = undefined;
CurrentPackData.disabledMessage = undefined;
CurrentPackData.location = undefined;
saveDataToFile(JSON.stringify(CurrentPackData), 'application/json', `${MasterTagsSettings.CurrentPackHeader.name}.tagpack`);
}
////////////////////////////////////////////////////////////
// Handle tag pack import
////////////////////////////////////////////////////////////
async _handleTagPackImport(event)
{
// Create a file input element
const inputElement = document.createElement('input');
inputElement.type = 'file';
// Listen for the file to be selected
inputElement.addEventListener('change', async (event) =>
{
// Read the file
const selectedFile = event.target.files[0];
readTextFromFile(selectedFile).then(async (json) =>
{
// Insert the file header into the pack headers
let Data = JSON.parse(json);
// Check if the pack already exists
let DuplicatedID = false;
let DuplicatedName = false;
let ShouldImport = true;
if (MasterTagsSettings.PackHeaders[Data.id] != undefined)
{
DuplicatedID = true;
}
// Check if there is a pack with the same name
for (let PackId in MasterTagsSettings.PackHeaders)
{
if (MasterTagsSettings.PackHeaders[PackId].name == Data.name)
{
DuplicatedName = true;
}
}
// Pack already exists
if (DuplicatedID && DuplicatedName)
{
// Ask the user if he wants to overwrite the pack
let ConfirmDialog = await Dialog.confirm({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Import.Duplicated.Title'),
content: `<p style="color:red">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Import.Duplicated.Description').replace('${pack}', Data.name)}</p`,
no: () =>
{
ShouldImport = false;
},
yes: () =>
{
ShouldImport = true;
},
defaultYes: false
});
}
// Duplicated ID
else if (DuplicatedID)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Import.Error.DuplicatedID'));
return;
}
// Duplicated name
else if (DuplicatedName)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.Pack.Import.Error.DuplicatedName'));
return;
}
// Import the pack
if (ShouldImport)
{
await MasterTagsSettings.LoadTagPackFromObject(JSON.parse(json));
// Unselects the current pack
MasterTagsSettings.CurrentPackHeader.selected = undefined;
MasterTagsSettings.PackHeaders[MasterTagsSettings.CurrentPackHeader.id].selected = undefined;
// Set the imported pack as the current pack
MasterTagsSettings.CurrentPackHeader = MasterTagsSettings.PackHeaders[Data.id];
MasterTagsSettings.CurrentPackHeader.selected = true;
MasterTagsSettings.PackHeaders[Data.id].selected = true;
// Load the tags from the imported pack
MasterTagsSettings.Tags = Data.tags;
MasterTagsSettings.ResultTags = await MasterTagsSettings._filterTags();
TagHandler.UpdateTags(MasterTagsSettings.Tags);
// Save the pack headers
await MasterTagsSettings.SaveUserPacks();
}
this.render();
});
});
inputElement.click();
}
////////////////////////////////////////////////////////////
// Handle tag pack copy
////////////////////////////////////////////////////////////
async _handleTagPackCopy(event)
{
let NewPackDialog = new Dialog({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Title'),
content: `
<br>
<label for="oif-pack-new-id">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Id')}</label>
<input type="text" id="oif-pack-new-id" name="oif-pack-new-id">
<label for="oif-pack-new-name">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Name')}</label>
<input type="text" id="oif-pack-new-name" name="oif-pack-new-name">
`,
buttons: {
create: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Copy').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name),
callback: async (html) => {
let NewPackID = html.find('#oif-pack-new-id').val();
let NewPackName = html.find('#oif-pack-new-name').val();
if (NewPackID == '' || NewPackName == '')
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.EmptyValues'));
}
else
{
let DoesNameCollide = false;
Object.values(MasterTagsSettings.PackHeaders).every((element) => {
if (element.name == NewPackName)
{
DoesNameCollide = true;
return false;
}
return true;
})
if (MasterTagsSettings.PackHeaders[NewPackID] != undefined && DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.BothAlreadyExist'));
}
else if (MasterTagsSettings.PackHeaders[NewPackID] != undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.IDAlreadyExist'));
}
else if (DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.NameAlreadyExist'));
}
else
{
await this._createNewPack(NewPackID, NewPackName, MasterTagsSettings.Tags);
await MasterTagsSettings.SaveUserPacks();
this.render();
}
}
}
}
},
default: 'create'
});
NewPackDialog.render(true);
}
async _handleTagPackCreate(event)
{
let NewPackDialog = new Dialog({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Title'),
content: `
<p>${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Description')}</p>
<label for="oif-pack-new-id">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Id')}</label>
<input type="text" id="oif-pack-new-id" name="oif-pack-new-id">
<label for="oif-pack-new-name">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Name')}</label>
<input type="text" id="oif-pack-new-name" name="oif-pack-new-name">
<br>
`,
buttons: {
create: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Create'),
callback: async (html) => {
let NewPackID = html.find('#oif-pack-new-id').val();
let NewPackName = html.find('#oif-pack-new-name').val();
if (NewPackID == '' || NewPackName == '')
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.EmptyValues'));
}
else
{
let DoesNameCollide = false;
Object.values(MasterTagsSettings.PackHeaders).every((element) => {
if (element.name == NewPackName)
{
DoesNameCollide = true;
return false;
}
return true;
})
if (MasterTagsSettings.PackHeaders[NewPackID] != undefined && DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.BothAlreadyExist').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name).replace('${id}', MasterTagsSettings.CurrentPackHeader.id));
}
else if (MasterTagsSettings.PackHeaders[NewPackID] != undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.IDAlreadyExist').replace('${id}', MasterTagsSettings.CurrentPackHeader.id));
}
else if (DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.NameAlreadyExist').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name));
}
else
{
await MasterTagsSettings.LoadTags('Empty');
await this._createNewPack(NewPackID, NewPackName, MasterTagsSettings.Tags);
await MasterTagsSettings.SaveUserPacks();
this.render();
}
}
}
}
},
default: 'create'
});
NewPackDialog.render(true);
}
async _handleTagPackDiscardChanges()
{
let ConfirmDialog = Dialog.confirm({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.Discard.Title'),
content: `<p style="color:red">${game.i18n.localize('OIF.Settings.MasterTagsSettings.Discard.Description')}</p`,
no: async () =>
{
this.render();
},
yes: async () =>
{
await MasterTagsSettings.LoadTags(MasterTagsSettings.CurrentPackHeader.id);
this.render();
},
defaultYes: false
});
}
async _handleTagPackSaveChanges(event)
{
if (!MasterTagsSettings.Changed)
{
return;
}
if (MasterTagsSettings.CurrentPackHeader.default)
{
let NewPackDialog = new Dialog({
title: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Title'),
content: `
<p>${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Description')}</p>
<br>
<label for="oif-pack-new-id">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Id')}</label>
<input type="text" id="oif-pack-new-id" name="oif-pack-new-id">
<label for="oif-pack-new-name">${game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Name')}</label>
<input type="text" id="oif-pack-new-name" name="oif-pack-new-name">
<br>
`,
buttons: {
create: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Copy').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name),
callback: async (html) => {
let NewPackID = html.find('#oif-pack-new-id').val();
let NewPackName = html.find('#oif-pack-new-name').val();
if (NewPackID == '' || NewPackName == '')
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPackCopy.Error.EmptyValues'));
}
else
{
let DoesNameCollide = false;
Object.values(MasterTagsSettings.PackHeaders).every((element) => {
if (element.name == NewPackName)
{
DoesNameCollide = true;
return false;
}
return true;
})
if (MasterTagsSettings.PackHeaders[NewPackID] != undefined && DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.BothAlreadyExist').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name).replace('${id}', MasterTagsSettings.CurrentPackHeader.id));
}
else if (MasterTagsSettings.PackHeaders[NewPackID] != undefined)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.IDAlreadyExist').replace('${id}', MasterTagsSettings.CurrentPackHeader.id));
}
else if (DoesNameCollide)
{
ui.notifications.error(game.i18n.localize('OIF.Settings.MasterTagsSettings.NewPack.Error.NameAlreadyExist').replace('${pack}', MasterTagsSettings.CurrentPackHeader.name));
}
else
{
await this._createNewPack(NewPackID, NewPackName, MasterTagsSettings.Tags);
await MasterTagsSettings.SaveUserPacks();
this.render();
}
}
}
}
},
default: 'create'
});
NewPackDialog.render(true);
}
else
{
await MasterTagsSettings.SaveUserPacks();
this.render();
}
}
activateListeners(html)
{
super.activateListeners(html);
// Tags
html.on('change' , 'select' , this._handlePackSelection.bind(this));
html.on('keypress', 'input[class="oif-tag-input"]' , this._handleTagCreation.bind(this));
html.on('keypress', 'input[class="oif-tag-search"]' , this._handleTagSearching.bind(this));
html.on('click' , 'i[class="fas fa-wrench"]' , this._handleTagConfiguration.bind(this));
html.on('click' , 'i[class="fas fa-times"]' , this._handleTagDeletion.bind(this));
// Tag Packs
html.on('click', 'button[id="oif-delete-tag-pack"]' , this._handleTagPackDeletion.bind(this));
html.on('click', 'button[id="oif-rename-tag-pack"]' , this._handleTagPackRenaming.bind(this));
html.on('click', 'button[id="oif-export-tag-pack"]' , this._handleTagPackExport.bind(this));
html.on('click', 'button[id="oif-import-tag-pack"]' , this._handleTagPackImport.bind(this));
html.on('click', 'button[id="oif-copy-tag-pack"]' , this._handleTagPackCopy.bind(this));
html.on('click', 'button[id="oif-create-tag-pack"]' , this._handleTagPackCreate.bind(this));
html.on('click', 'button[id="oif-tag-pack-discard-changes"]', this._handleTagPackDiscardChanges.bind(this));
html.on('click', 'button[id="oif-tag-pack-save-changes"]' , this._handleTagPackSaveChanges.bind(this));
let SelectedElement;
switch (MasterTagsSettings.FocusOn)
{
case 'input' : SelectedElement = html[0].querySelector('input[class="oif-tag-input"]'); break;
case 'search': SelectedElement = html[0].querySelector('input[class="oif-tag-search"]'); break;
default: break;
}
let CursorEnd = SelectedElement.value.length ?? 0;
SelectedElement?.setSelectionRange(CursorEnd, CursorEnd);
SelectedElement?.focus();
}
getData(options)
{
return {
packs: MasterTagsSettings.PackHeaders,
currentPack: MasterTagsSettings.CurrentPackHeader,
search: MasterTagsSettings.SearchString != '' ? MasterTagsSettings.SearchString : undefined,
tags: MasterTagsSettings.ResultTags,
canSave: MasterTagsSettings.Changed,
}
}
async _updateObject(event, formData)
{
}
}

View File

@@ -0,0 +1,66 @@
////////////////////////////////////////////////////////////
// The credits of this code goes to kandashi and ChueyB
// I've just copied the sections that OIF uses, I actually
// have no idea how it works or why it works, in the future
// I will improve this section of OIF, but for now
// this is good enough
//
// Thank you kandashi and ChueyB :D
////////////////////////////////////////////////////////////
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
export class CanvasLayer extends InteractionLayer
{
constructor()
{
super();
if (game.release.generation == 10)
{
this.loadar = new PIXI.Loader();
}
this.mouseInteractionManager = null;
this._interactiveChildren = false;
this._dragging = false;
this.options = this.constructor.layerOptions;
}
async _draw(options)
{
}
static ScreenShake()
{
let Intensity = 1;
let a = 1 * Intensity;
let b = 2 * Intensity;
let c = 3 * Intensity;
document.getElementById('board').animate([
{ transform: `translate(${a}px, ${a}px) rotate(0deg)` },
{ transform: `translate(-${a}px, -${b}px) rotate(-${a}deg)` },
{ transform: `translate(-${c}px, 0px) rotate(${a}deg)` },
{ transform: `translate(${c}px, ${b}px) rotate(0deg)` },
{ transform: `translate(${a}px, -${a}px) rotate(${a}deg)` },
{ transform: `translate(-${a}px, ${b}px) rotate(-${a}deg)` },
{ transform: `translate(-${c}px, ${a}px) rotate(0deg)` },
{ transform: `translate(${c}px, ${a}px) rotate(-${a}deg)` },
{ transform: `translate(-${a}px, -${a}px) rotate(${a}deg)` },
{ transform: `translate(${a}px, ${b}px) rotate(0deg)` },
{ transform: `translate(${a}px, -${b}px) rotate(-${a}deg)` }
],
{
duration: 500,
iterations: 1
});
}
}
Hooks.on('init', () => {
CONFIG.Canvas.layers['CanvasEffects'] = {
group: 'interface',
layerClass: CanvasLayer
};
})

View File

@@ -0,0 +1,116 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
export class Debug
{
static Initialize()
{
Debug._enabled = game.settings.get(OIF.ID, OIF.SETTINGS.GENERAL.DEVELOPER_MODE);
Debug._errorStack = {};
Debug._logStack = {};
Debug._warnStack = {};
Hooks.on(OIF.HOOKS.CHANGE_SETTINGS, (settings) =>
{
Debug._enabled = settings[OIF.SETTINGS.GENERAL.DEVELOPER_MODE].value;
});
}
static Error(...args)
{
if (Debug._enabled)
{
console.error('OIF DEBUG - ERROR - ', ...args);
}
}
static Log(...args)
{
if (Debug._enabled)
{
console.log('OIF DEBUG - LOG - ', ...args);
}
}
static Warn(...args)
{
if (Debug._enabled)
{
console.warn('OIF DEBUG - WARN - ', ...args);
}
}
static ErrorStack(id, ...args)
{
if (Debug._enabled)
{
if (Debug._errorStack[id] == undefined)
{
Debug._errorStack[id] = [];
}
Debug._errorStack[id].push(args);
}
}
static LogStack(id, ...args)
{
if (Debug._enabled)
{
if (Debug._logStack[id] == undefined)
{
Debug._logStack[id] = [];
}
Debug._logStack[id].push(args);
}
}
static WarnStack(id, ...args)
{
if (Debug._enabled)
{
if (Debug._warnStack[id] == undefined)
{
Debug._warnStack[id] = [];
}
Debug._warnStack[id].push(args);
}
}
static FlushErrorStack(id)
{
if (Debug._enabled)
{
if (Debug._errorStack[id] != undefined)
{
console.error('OIF DEBUG - ERROR STACK - ', id, ...Debug._errorStack[id]);
Debug._errorStack[id] = [];
}
}
}
static FlushLogStack(id)
{
if (Debug._enabled)
{
if (Debug._logStack[id] != undefined)
{
console.log('OIF DEBUG - LOG STACK - ', id, ...Debug._logStack[id]);
Debug._logStack[id] = [];
}
}
}
static FlushWarnStack(id)
{
if (Debug._enabled)
{
if (Debug._warnStack[id] != undefined)
{
console.warn('OIF DEBUG - WARN STACK - ', id, ...Debug._warnStack[id]);
Debug._warnStack[id] = [];
}
}
}
}

View File

@@ -0,0 +1,49 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
let Parts = {
'oif.settings.name' : 'settings/Name.hbs',
'oif.settings.checkbox': 'settings/Checkbox.hbs',
'oif.settings.slider' : 'settings/Slider.hbs',
'oif.settings.dropdown': 'settings/Dropdown.hbs',
'oif.settings.string' : 'settings/String.hbs',
'oif.config.title' : 'config/Title.hbs',
'oif.config.name' : 'config/Name.hbs',
'oif.config.checkbox': 'config/Checkbox.hbs',
'oif.config.slider' : 'config/Slider.hbs',
'oif.config.dropdown': 'config/Dropdown.hbs',
'oif.config.string' : 'config/String.hbs',
'oif.config.color' : 'config/Color.hbs',
'oif.config.icon' : 'config/Icon.hbs',
'oif.masterTag.lighting': 'masterTag/Lighting.hbs',
'oif.masterTag.special.powerful': 'masterTag/special/Powerful.hbs',
'oif.masterTag.special.generateCurrency': 'masterTag/special/GenerateCurrency.hbs'
}
Hooks.on('ready', () => {
Object.keys(Parts).forEach((key) => {
Parts[key] = `modules/${OIF.ID}/module/templates/parts/${Parts[key]}`
});
loadTemplates(Parts);
Handlebars.registerHelper('masterTagAnimation', (origin, type) => {
let Header = `
<h3> ${game.i18n.localize('OIF.Settings.MasterTagConfiguration.Tag.' + type)} </h3>
`;
let Body = `
<label for="oif-${type}-animation"> ${game.i18n.localize('OIF.Settings.MasterTagConfiguration.Tag.Animation')} </label>
<input class="oif-animation-input-text" name="oif-${type}-animation" type="text" ${origin?.source ? 'value="' + origin.source + '"' : ''}>
<label for="oif-${type}-delay"> ${game.i18n.localize('OIF.Settings.MasterTagConfiguration.Tag.Delay')} </label>
<input class="oif-animation-input-text" name="oif-${type}-delay" type="text" ${origin?.delay ? 'value="' + origin.delay + '"' : ''}>
`;
let Complete = (Header + Body);
return new Handlebars.SafeString(Complete);
})
});

View File

@@ -0,0 +1,38 @@
export class Helpers
{
static RandomMax(max)
{
return Math.floor(Math.random() * max + 1);
}
static RandomMinMax(min, max)
{
return Math.floor(Math.random() * (max - min + 1)) + min;
}
static GetCornersOfToken(token)
{
const width = token.width * game.canvas.dimensions.size;
const height = token.height * game.canvas.dimensions.size;
const corners = [
{
x: token.x - width / 2,
y: token.y - height / 2
},
{
x: token.x + width / 2,
y: token.y - height / 2
},
{
x: token.x + width / 2,
y: token.y + height / 2
},
{
x: token.x - width / 2,
y: token.y + height / 2
}
];
return corners;
}
}

View File

@@ -0,0 +1,44 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
export class InventoryManipulator
{
static async RemoveItem(owner, item, quantity)
{
// Check if the quantity is exact, if so delete the item from the inventory
if (item.system.quantity == quantity)
{
// Remove the item from the inventory
await owner.actor.deleteEmbeddedDocuments("Item", [item.id]);
return true;
}
// More than "quantity" units of the item remaining
else if (item.system.quantity > quantity)
{
// Remove "quantity" units from the item
await owner.actor.updateEmbeddedDocuments("Item", [{_id: item.id, "system.quantity": item.system.quantity - quantity}]);
return true;
}
// Not enough items to be removed
else
{
console.error(`Could not remove item! Tried to remove ${quantity} while having only ${item.system.quantity}`)
return false;
}
}
static async AddItem(target, item, quantity)
{
if (quantity < 1)
{
console.error(`Could not add item! ${quantity} is negative and thus invalid`);
return false;
}
else
{
let ItemCopy = item.toObject();
ItemCopy.system.quantity = quantity;
await target.actor.createEmbeddedDocuments("Item", [ItemCopy]);
return true;
}
}
}

View File

@@ -0,0 +1,60 @@
import { GeneralSettings } from "../interface/GeneralSettings.js";
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
export class ItemDropper
{
static async DropAt(item, quantity, position, elevation)
{
// Check for invalid quantity
if (quantity < 1)
{
console.error(`Failed to drop item! ${quantity} is negative and thus invalid`);
return false;
}
// Copy the item
let ItemCopy = await item.toObject();
ItemCopy.system.quantity = quantity;
// Create a ItemPile and get token reference
let ItemPileOptions = {
items: ItemCopy,
pileActorName: false,
position: position,
tokenOverrides: {}
}
// Minify the name of dropped items
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.MINIFY_ITEM_PILES_NAMES))
{
ItemPileOptions.tokenOverrides.name = '▲';
}
else
{
ItemPileOptions.tokenOverrides.name = ItemCopy.name;
}
// Check if ItemPile should snap to grid
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.SNAP_CREATED_ITEM_PILES_TO_GRID))
{
ItemPileOptions.position = canvas.grid.getSnappedPosition(ItemPileOptions.position.x, ItemPileOptions.position.y);
}
let ItemPileTokenUuid = await game.itempiles.API.createItemPile(ItemPileOptions);
let ItemPileToken = await fromUuid(ItemPileTokenUuid.tokenUuid);
// Set ItemPile elevation if needed
if (OIF.OPTIONAL_MODULES.LEVELS.active && GeneralSettings.Get(OIF.SETTINGS.GENERAL.SET_ELEVATION_OF_ITEM_PILES))
{
ItemPileToken.update({
elevation: elevation ?? 0
});
}
// Add a tag to the ItemPile to indicate it was created by OIF
if (OIF.OPTIONAL_MODULES.TAGGER.active)
{
Tagger.addTags(ItemPileToken, [`${OIF.ID}-dropped-item-${item.id}`]);
}
}
}

View File

@@ -0,0 +1,18 @@
import { CanvasLayer } from './CanvasEffects.js';
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { MasterTagsSettings } from '../interface/MasterTagsSettings.js';
let Socket;
Hooks.on('socketlib.ready', () =>
{
Socket = socketlib.registerModule(OIF.ID);
// Canvas effects
Socket.register('ScreenShake', CanvasLayer.ScreenShake);
// Tags
Socket.register('LoadFromConfig', MasterTagsSettings.LoadFromConfig);
window.OIF_SOCKET = Socket;
})

View File

@@ -0,0 +1,201 @@
import { ObjectsInteractionsFX as OIF } from "../ObjectsInteractionsFX.js";
import { ObjectsInteractionsFXData as OIFD } from "../data/ObjectsInteractionsFXData.js";
import { GeneralSettings } from "../interface/GeneralSettings.js";
import { TagHandler } from "../tags/TagHandler.js";
export class TokenLightingManipulator
{
static LIGHT_SOURCE_ID = OIF.ID + "_light_source";
static async SetDefaultLightingOptions(options)
{
await options.token.document.update({
"light.bright" : 0,
"light.dim" : 0,
"light.animation.type" : undefined,
"light.animation.speed" : 5,
"light.animation.intensity": 5,
"light.animation.reverse" : false,
"light.color" : "#000000",
"light.alpha" : 0.5,
"light.angle" : 360,
});
await options.item?.update({
"img": options?.icons?.unlit ?? options?.item?.img ?? undefined,
});
}
static async SetLightingOptions(options)
{
await options.token.document.update({
"light.bright" : options?.item?.system?.range?.value ?? 0,
"light.dim" : options?.item?.system?.range?.long ?? 0,
"light.animation.type" : options?.light?.animationType ?? undefined,
"light.animation.speed" : options?.light?.animationSpeed ?? 5,
"light.animation.intensity": options?.light?.animationIntensity ?? 5,
"light.animation.reverse" : options?.light?.animationReverse ?? false,
"light.color" : options?.light?.color ?? "#000000",
"light.alpha" : options?.light?.intensity ?? 0.5,
"light.angle" : options?.light?.angle ?? 360,
});
if (options.item != undefined)
{
if (options.lit)
{
await options.item.update({
"img": options?.icons?.lit ?? options?.item?.img ?? undefined,
});
}
else
{
await options.item.update({
"img": options?.icons?.unlit ?? options?.item?.img ?? undefined,
});
}
}
}
static async SetLighting(options)
{
// Check if light_source tag is already set
if (Tagger.hasTags(options.token, TokenLightingManipulator.LIGHT_SOURCE_ID))
{
ui.notifications.error(game.i18n.localize("OIF.Item.Lighting.Error.AlreadySource"));
console.error("Failed to set token lighting based on item! Token is already a light source");
}
else
{
// Call hook
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.LIGHT.PRE, options);
// Set token light properties
TokenLightingManipulator.SetLightingOptions(options);
// Add the light source tag
await Tagger.addTags(options.token, [TokenLightingManipulator.LIGHT_SOURCE_ID, `${OIF.ID}_${options.name}`]);
// Call hook
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.LIGHT.POS, options);
}
}
static async RemoveLighting(options)
{
// Check if light_source tag is not set
if (!Tagger.hasTags(options.token, TokenLightingManipulator.LIGHT_SOURCE_ID))
{
ui.notifications.error(game.i18n.localize("OIF.Item.Lighting.Error.NotSource"));
console.error("Failed to reset token lighting based on item! Token is not a light source");
}
else if (!Tagger.hasTags(options.token, `${OIF.ID}_${options.name}`))
{
ui.notifications.error(game.i18n.localize("OIF.Item.Lighting.Error.NotRightSource"));
console.error("Failed to reset token lighting based on item! Specified item is not the one providing light");
}
else
{
// Call hook
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.EXTINGUISH.PRE, options);
// Reset token light properties
TokenLightingManipulator.SetDefaultLightingOptions(options);
// Remove the light source tag
await Tagger.removeTags(options.token, [TokenLightingManipulator.LIGHT_SOURCE_ID, `${OIF.ID}_${options.name}`]);
// Call hook
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.EXTINGUISH.POS, options);
}
}
static async ToggleItemLighting(options)
{
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.POST_PREPARE, options);
if (GeneralSettings.Get(OIF.SETTINGS.GENERAL.LIGHTING_ITEMS_AUTOMATION))
{
// Check if light_source tag is set
if (Tagger.hasTags(options.token, TokenLightingManipulator.LIGHT_SOURCE_ID))
{
// Remove if set
this.RemoveLighting({ ...options, lit: false});
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.EXTINGUISH.POST_APPLY, options);
}
else
{
// Add if unset
this.SetLighting({ ...options, lit: true });
Hooks.call(OIF.HOOKS.ITEM.LIGHTING.LIGHT.POST_APPLY, options);
}
}
}
static async RemoveAllLighting()
{
let Tokens = Tagger.getByTag(TokenLightingManipulator.LIGHT_SOURCE_ID);
for (let Token of Tokens)
{
await TokenLightingManipulator.SetDefaultLightingOptions({ token: await canvas.tokens.get(Token.id) });
// Get the tags of the token
let TokenTags = Tagger.getTags(Token);
// Remove the prefix
TokenTags = TokenTags.map(tag => tag.replace(OIF.ID + "_", ""));
let LightingTags = Object.assign({}, TagHandler.Tags['Lighting']);
for (let key in LightingTags)
{
if (LightingTags.hasOwnProperty(key))
{
LightingTags[key].name = key;
}
}
// Filter the tags to only get the ones related to lighting
let itemTags = {};
for (let key in LightingTags)
{
if (LightingTags.hasOwnProperty(key))
{
let lightingTag = LightingTags[key];
TokenTags.forEach((tokenTag) =>
{
if (lightingTag.name == tokenTag)
{
itemTags[lightingTag.name] = lightingTag;
}
});
}
}
// Get the lighting items
for (let lightingTag of Object.values(itemTags))
{
for (let item of Token.actor.items)
{
// Get the tags of the item
let Tags = itemTags.Get(item);
// Check if the item has the tag
if (Tags.includes(lightingTag.name))
{
// Set the item icon to the unlit one
item.update({ "img": lightingTag.icons.unlit });
// Remove the tag
await Tagger.removeTags(Token, [`${OIF.ID}_${lightingTag.name}`]);
}
}
}
// Remove the light source tag
Tagger.removeTags(Token, [TokenLightingManipulator.LIGHT_SOURCE_ID]);
}
}
}

View File

@@ -0,0 +1,145 @@
import { ObjectsInteractionsFX as OIF } from "../../ObjectsInteractionsFX.js";
export class ConfigSkeleton extends FormApplication {
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
closeOnSubmit: false,
height: 'auto',
width: 600,
submitOnChange: true,
template: OIF.TEMPLATES.CONFIG_SKELETON,
caller: null,
updaterFunction: null,
configurationData: {},
dataNameAtSingleton: '',
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
////////////////////////////////////////////////////////////
// Get the data from the singleton
////////////////////////////////////////////////////////////
async _updateDataAtSingleton(topography, data)
{
// Separate string into array of strings using the dot
// as separator
let Location = topography.split('.');
let Data = this.options.configurationData;
for (let i = 0; i < Location.length; i++)
{
let Piece = Location[i];
if (!Data.hasOwnProperty(Piece))
{
Data[Piece] = {};
}
if (i == Location.length - 1)
{
Data[Piece] = data;
}
Data = Data[Piece];
}
return this.options.configurationData;
}
////////////////////////////////////////////////////////////
// Handle checkbox change
////////////////////////////////////////////////////////////
async _handleCheckboxChange(event)
{
let ClickedElement = $(event.currentTarget)[0];
let Data = await this._updateDataAtSingleton(ClickedElement.name, ClickedElement.checked);
this.options.configurationData = Data;
await this.options.updaterFunction(this.options.dataNameAtSingleton, Data);
this.render();
this.options.caller?.render();
}
////////////////////////////////////////////////////////////
// Handle slider change
////////////////////////////////////////////////////////////
async _handleSliderChange(event)
{
let ClickedElement = $(event.currentTarget)[0];
let Data = await this._updateDataAtSingleton(ClickedElement.name, ClickedElement.value);
this.options.configurationData = Data;
await this.options.updaterFunction(this.options.dataNameAtSingleton, Data);
this.render();
this.options.caller?.render();
}
////////////////////////////////////////////////////////////
// Handle dropdown change
////////////////////////////////////////////////////////////
async _handleDropdownChange(event)
{
let ClickedElement = $(event.currentTarget)[0];
let Data = await this._updateDataAtSingleton(ClickedElement.name, ClickedElement.value);
this.options.configurationData = Data;
await this.options.updaterFunction(this.options.dataNameAtSingleton, Data);
this.render();
this.options.caller?.render();
}
////////////////////////////////////////////////////////////
// Handle text change
////////////////////////////////////////////////////////////
async _handleTextChange(event)
{
let ClickedElement = $(event.currentTarget)[0];
let Data = await this._updateDataAtSingleton(ClickedElement.name, ClickedElement.value);
this.options.configurationData = Data;
await this.options.updaterFunction(this.options.dataNameAtSingleton, Data);
this.render();
this.options.caller?.render();
}
////////////////////////////////////////////////////////////
// Handle color change
////////////////////////////////////////////////////////////
async _handleColorChange(event)
{
let ClickedElement = $(event.currentTarget)[0];
let Data = await this._updateDataAtSingleton(ClickedElement.name, ClickedElement.value);
this.options.configurationData = Data;
await this.options.updaterFunction(this.options.dataNameAtSingleton, Data);
this.render();
this.options.caller?.render();
}
////////////////////////////////////////////////////////////
// Activate listeners
////////////////////////////////////////////////////////////
activateListeners(html)
{
super.activateListeners(html);
// Checkbox
html.on('change', 'input[type="checkbox"]', this._handleCheckboxChange.bind(this));
// Slider
html.on('change', 'input[type="range"]', this._handleTextChange.bind(this));
html.on('change', 'input[type="number"]', this._handleTextChange.bind(this));
// String
html.on('change', 'input[type="text"]', this._handleTextChange.bind(this));
// Dropdown
html.on('change', 'select', this._handleDropdownChange.bind(this));
// Color
html.on('change', 'input[type="color"]', this._handleColorChange.bind(this));
}
}

View File

@@ -0,0 +1,465 @@
import { GeneralSettings } from "../../interface/GeneralSettings.js";
import { ObjectsInteractionsFX as OIF } from "../../ObjectsInteractionsFX.js";
export class SettingsSkeleton extends FormApplication
{
static get defaultOptions() {
const DefaultOptions = super.defaultOptions;
const OverrideOptions = {
closeOnSubmit: false,
height: 'auto',
with: 600,
submitOnChange: true,
template: OIF.TEMPLATES.SETTINGS_SKELETON
};
const MergedOptions = foundry.utils.mergeObject(DefaultOptions, OverrideOptions);
return MergedOptions;
}
static Settings = {}
////////////////////////////////////////////////////////////
// Prototype function used to register custom settings
////////////////////////////////////////////////////////////
static _protoRegister(config, name, options)
{
if (SettingsSkeleton.Settings[config] == undefined)
{
SettingsSkeleton.Settings[config] = {};
}
// Prepare the internal foundry setting options
let FoundrySettingOptions = {
name: options['name'],
hint: options['hint'],
scope: options['scope'],
default: options['default'],
config: false
}
// Detect and convert custom type to be used by foundry
if (options['type'] != undefined)
{
// Sets a value inside option, named after the type name, as true
// This is used inside .hbs templates to select which option
// display/editor will be shown
options[options['type']] = true;
// Convert type to a foundry compatible
switch (options['type'])
{
case 'checkbox':
FoundrySettingOptions['type'] = Boolean;
break;
case 'slider':
FoundrySettingOptions['type'] = Number;
FoundrySettingOptions['range'] = options['range'];
break;
case 'string':
FoundrySettingOptions['type'] = String;
break;
case 'dropdown':
FoundrySettingOptions['type'] = String;
FoundrySettingOptions['choices'] = options['choices'];
break;
default:
console.error(`Cannot register setting, ${options['type']} is not a valid type`);
break;
}
}
// Register setting inside foundry
game.settings.register(OIF.ID, name, FoundrySettingOptions);
// Get currently set value to be displayed
options['value'] = game.settings.get(OIF.ID, name);
// Dropdown types are handled differently
if (options['type'] == 'dropdown')
{
// Iterate through the choices and set them as selected
// or not
options['choices'].forEach((element) => {
if (element.value == options['value'])
{
element['selected'] = true;
}
else
{
element['selected'] = false;
}
})
}
// Set the ID to be used as reference later
options['referenceID'] = name;
// Check there are required modules
let RequiredModule = options['requiredModule'];
if (RequiredModule != undefined)
{
// Check if the required module is active
if (!RequiredModule.active)
{
options['disabled'] = true;
options['disabledMessage'] = game.i18n.localize('OIF.Settings.ModuleRequired').replace("${module}", `"${RequiredModule.name}"`);
}
}
// Register inside OIF
SettingsSkeleton.Settings[config][name] = options;
}
////////////////////////////////////////////////////////////
// Prototype function used to get custom settings
////////////////////////////////////////////////////////////
static _protoGet(config, name)
{
let CurrentSetting = SettingsSkeleton.Settings[config][name];
switch (CurrentSetting['type'])
{
case 'checkbox':
return CurrentSetting['value'] && !CurrentSetting['disabled'];
break;
case 'slider':
case 'string':
case 'dropdown':
return CurrentSetting['disabled'] ? CurrentSetting['default'] : CurrentSetting['value'];
break;
default:
return CurrentSetting['value'];
break;
}
}
////////////////////////////////////////////////////////////
// Internal function that checks if restart is required
////////////////////////////////////////////////////////////
async _checkForRestart(config, id)
{
if (SettingsSkeleton.Settings[config][id]['restart'] != null && SettingsSkeleton.Settings[config][id]['restart'] != undefined && SettingsSkeleton.Settings[config][id]['restart'])
{
SettingsSkeleton.Settings[config][id]['requiresRestart'] = true;
SettingsSkeleton.Settings[config][id]['restartMessage'] = 'OIF.Settings.RestartRequired';
}
}
////////////////////////////////////////////////////////////
// Internal function for handling Checkbox interactions
////////////////////////////////////////////////////////////
async _handleCheckBoxInteraction(config, event)
{
let ClickedElement = $(event.currentTarget)[0];
game.settings.set(OIF.ID, ClickedElement.id, ClickedElement.checked);
SettingsSkeleton.Settings[config][ClickedElement.id]['value'] = ClickedElement.checked;
this._checkForRestart(config, ClickedElement.id);
SettingsSkeleton._protoUpdateSettings(config);
this.render();
}
////////////////////////////////////////////////////////////
// Internal function for handling Slider interactions
////////////////////////////////////////////////////////////
async _handleSliderInteraction(config, event)
{
let ClickedElement = $(event.currentTarget)[0];
game.settings.set(OIF.ID, ClickedElement.id, ClickedElement.value);
// Check value
let CurrentValue = Number(ClickedElement.value);
if (CurrentValue > ClickedElement.max)
{
CurrentValue = ClickedElement.max;
}
else if (CurrentValue < ClickedElement.min)
{
CurrentValue = ClickedElement.min;
}
SettingsSkeleton.Settings[config][ClickedElement.id]['value'] = CurrentValue;
this._checkForRestart(config, ClickedElement.id);
SettingsSkeleton._protoUpdateSettings(config);
this.render();
}
////////////////////////////////////////////////////////////
// Internal function for handling String interactions
////////////////////////////////////////////////////////////
async _handleStringInteraction(config, event)
{
let ClickedElement = $(event.currentTarget)[0];
game.settings.set(OIF.ID, ClickedElement.id, ClickedElement.value);
SettingsSkeleton.Settings[config]['value'] = ClickedElement.value;
this._checkForRestart(config, ClickedElement.id);
SettingsSkeleton._protoUpdateSettings(config);
this.render();
}
////////////////////////////////////////////////////////////
// Internal function for handling Dropdown interactions
////////////////////////////////////////////////////////////
async _handleDropdownInteraction(config, event)
{
let ClickedElement = $(event.currentTarget)[0];
game.settings.set(OIF.ID, ClickedElement.id, ClickedElement.value);
SettingsSkeleton.Settings[config][ClickedElement.id]['value'] = ClickedElement.value;
SettingsSkeleton.Settings[config][ClickedElement.id]['choices'].forEach(function(element)
{
element['selected'] = false;
if (element.value == ClickedElement.value)
{
element['selected'] = true;
}
});
this._checkForRestart(config, ClickedElement.id);
SettingsSkeleton._protoUpdateSettings(config);
this.render();
}
////////////////////////////////////////////////////////////
// Prototype function that updates settings
////////////////////////////////////////////////////////////
static _protoUpdateSettings(config)
{
for (const key in SettingsSkeleton.Settings[config])
{
// Check if current element has a dependecy
let CurrentSetting = SettingsSkeleton.Settings[config][key];
let CurrentDependencies = CurrentSetting['dependsOn'];
let CurrentUndependencies = CurrentSetting['excludesOn'];
// Check if setting requires a module to be used
let RequiredModule = CurrentSetting.requiredModule;
if (RequiredModule != null && RequiredModule != undefined)
{
// Check if required module isn't loaded
if (!RequiredModule.active)
{
CurrentSetting['disabled'] = true;
CurrentSetting['disabledMessage'] = game.i18n.localize('OIF.Settings.ModuleRequired').replace("${module}", `"${RequiredModule.name}"`);
continue;
}
}
// Check if setting has dependencies
let AllRequirementsMet = true;
if (CurrentDependencies != null && CurrentDependencies != undefined)
{
// Loop through all dependencies to check if the are Boolean
let BoolCheck = true;
for (let index = 0; index < CurrentDependencies.length; index++)
{
// Check if the current dependency is a Boolean
const CurrentDependency = CurrentDependencies[index];
if (SettingsSkeleton.Settings[config][CurrentDependency]['type'] != 'checkbox')
{
BoolCheck = false;
}
}
// If one of the dependencies isn't a Boolean, skip the rest
if (BoolCheck == false)
{
console.error(`OIF | ${config} | ${key} | Dependency isn't a Boolean`);
continue;
}
// Check if the dependecies exist
let CurrentRequirements = [];
for (let index = 0; index < CurrentDependencies.length; index++)
{
// Check if the current dependency exists
const CurrentDependency = CurrentDependencies[index];
let CurrentRequirement = SettingsSkeleton.Settings[config][CurrentDependency];
if (CurrentRequirement != null && CurrentRequirement != undefined)
{
CurrentRequirements.push(CurrentRequirement);
}
}
// Check if all dependencies are met
for (let index = 0; index < CurrentRequirements.length; index++)
{
// Check if the current dependency is met
const CurrentRequirement = CurrentRequirements[index];
if (!SettingsSkeleton._protoGet(config, CurrentRequirement.referenceID))
{
AllRequirementsMet = false;
}
}
// If all dependencies are met, enable the setting
if (AllRequirementsMet)
{
CurrentSetting['disabled'] = false;
CurrentSetting['disabledMessage'] = '';
}
// If not, disable the setting
else
{
// Create a string with all dependencies, so the user knows which ones are missing
// separeted all dependencies with a comma, except the last one, which is separeted with 'and'
let DependenciesString = '';
for (let index = 0; index < CurrentRequirements.length; index++)
{
const CurrentRequirement = CurrentRequirements[index];
if (index == CurrentRequirements.length - 1)
{
DependenciesString += ` and "${game.i18n.localize(CurrentRequirement['name'])}"`;
}
else
{
DependenciesString += `"${game.i18n.localize(CurrentRequirement['name'])}", `;
}
}
// Disable the setting and add a message
CurrentSetting['disabled'] = true;
CurrentSetting['disabledMessage'] = game.i18n.localize('OIF.Settings.OptionRequired').replace("${option}", DependenciesString);
}
}
// Check if setting has undependencies
if (CurrentUndependencies != null && CurrentUndependencies != undefined && AllRequirementsMet)
{
// Loop through all undependencies to check if the are Boolean
let BoolCheck = true;
for (let index = 0; index < CurrentUndependencies.length; index++)
{
// Check if the current undependency is a Boolean
const CurrentUndependency = CurrentUndependencies[index];
if (SettingsSkeleton.Settings[config][CurrentUndependency]['type'] != 'checkbox')
{
BoolCheck = false;
}
}
// If one of the undependencies isn't a Boolean, skip the rest
if (BoolCheck == false)
{
console.error(`OIF | ${config} | ${key} | Undependency isn't a Boolean`);
continue;
}
// Check if the undependecies exist
let CurrentUnrequirements = [];
for (let index = 0; index < CurrentUndependencies.length; index++)
{
// Check if the current undependency exists
const CurrentUndependency = CurrentUndependencies[index];
let CurrentRequirement = SettingsSkeleton.Settings[config][CurrentUndependency];
if (CurrentRequirement != null && CurrentRequirement != undefined)
{
CurrentUnrequirements.push(CurrentRequirement);
}
}
// Check if all undependencies are met
let AllUnrequirementsMet = true;
for (let index = 0; index < CurrentUnrequirements.length; index++)
{
// Check if the current undependency is met
const CurrentUnrequirement = CurrentUnrequirements[index];
if (SettingsSkeleton._protoGet(config, CurrentUnrequirement.referenceID))
{
AllUnrequirementsMet = false;
}
}
// If all undependencies are met, enable the setting
if (AllUnrequirementsMet)
{
CurrentSetting['disabled'] = false;
CurrentSetting['disabledMessage'] = '';
}
// If not, disable the setting
else
{
// Create a string with all undependencies, so the user knows which ones are missing
// separeted all undependencies with a comma, except the last one, which is separeted with 'and'
let UndependenciesString = '';
for (let index = 0; index < CurrentUnrequirements.length; index++)
{
const CurrentUnrequirement = CurrentUnrequirements[index];
if (index == CurrentUnrequirements.length - 1)
{
UndependenciesString += ` and "${game.i18n.localize(CurrentUnrequirement['name'])}"`;
}
else
{
UndependenciesString += `"${game.i18n.localize(CurrentUnrequirement['name'])}", `;
}
}
// Disable the setting and add a message
CurrentSetting['disabled'] = true;
CurrentSetting['disabledMessage'] = game.i18n.localize('OIF.Settings.OptionUnrequired').replace("${option}", UndependenciesString);
}
}
}
Hooks.call(OIF.HOOKS.CHANGE_SETTINGS, SettingsSkeleton.Settings[config]);
}
////////////////////////////////////////////////////////////
// Prototype function to get data for rendering .hbs
////////////////////////////////////////////////////////////
static _protoGetData(config, options)
{
// Only return settings that are in the 'world' scope
// for GMs
let ReturnedSettings = {};
for (const key in SettingsSkeleton.Settings[config])
{
let CurrentSetting = SettingsSkeleton.Settings[config][key];
if (CurrentSetting.scope == 'world')
{
if (game.user.isGM)
{
ReturnedSettings[key] = CurrentSetting;
}
else
{
continue;
}
}
else
{
ReturnedSettings[key] = CurrentSetting;
}
}
return { settings: ReturnedSettings }
}
////////////////////////////////////////////////////////////
// Activate listeners
////////////////////////////////////////////////////////////
activateListeners(html, config)
{
super.activateListeners(html);
// Checkbox
html.on('change', 'input[type=checkbox]', this._handleCheckBoxInteraction.bind(this, config));
// Slider
html.on('change', 'input[type=range]', this._handleSliderInteraction.bind(this, config));
html.on('change', 'input[type=number]', this._handleSliderInteraction.bind(this, config));
// String
html.on('change', 'input[type=text]', this._handleStringInteraction.bind(this, config));
// Dropdown
html.on('change', 'select', this._handleDropdownInteraction.bind(this, config));
}
}

View File

@@ -0,0 +1,25 @@
import { DnD5e } from './systems/dnd5e.js'
import { Pf2e } from './systems/pf2e.js'
export class SystemSupporter
{
static Initialize()
{
switch (game.system.id)
{
case 'dnd5e': SystemSupporter._provider = DnD5e ; break;
case 'pf2e' : SystemSupporter._provider = Pf2e ; break;
}
}
static _provider = {}
static GetDefaultTagPack() { return SystemSupporter._provider.GetDefaultTagPack() ; }
static GetDefaultHookAttack() { return SystemSupporter._provider.GetDefaultHookAttack() ; }
static GetDefaultHookItem() { return SystemSupporter._provider.GetDefaultHookItem() ; }
static GetPossibleHooksAttack() { return SystemSupporter._provider.GetPossibleHooksAttack(); }
static GetPossibleHooksItem() { return SystemSupporter._provider.GetPossibleHooksItem() ; }
static async ExtractOptions(workflow, source, from) { return await SystemSupporter._provider.ExtractOptions(workflow, source, from); }
}

View File

@@ -0,0 +1,265 @@
import { ObjectsInteractionsFX as OIF } from "../../ObjectsInteractionsFX.js";
import { Debug as DBG } from "../../library/Debug.js";
export class DnD5e
{
////////////////////////////////////////////////////////////
// Return the default hooks
////////////////////////////////////////////////////////////
static GetDefaultHookAttack() { return 'dnd5e.rollAttack'; }
static GetDefaultHookItem() { return 'dnd5e.useItem' ; }
////////////////////////////////////////////////////////////
// Return the default tag pack
////////////////////////////////////////////////////////////
static GetDefaultTagPack()
{
if (game.modules.get('jb2a_patreon')?.active)
{
return 'DefaultFantasyTagsJB2AComplete';
}
else if (game.modules.get('JB2A_DnD5e')?.active)
{
return 'DefaultFantasyTagsJB2AFree';
}
else
{
return 'DefaultFantasyTagsNoAnimations';
}
}
////////////////////////////////////////////////////////////
// Return all the hooks that can be used to get attack info
////////////////////////////////////////////////////////////
static GetPossibleHooksAttack()
{
// System hooks
const System =
[
{
name : 'OIF.Settings.AttachHooks.Attack.DnD5eAfterAttackRoll.Label',
value: 'dnd5e.rollAttack',
},
{
name : 'OIF.Settings.AttachHooks.Attack.DnD5eAfterDamageRoll.Label',
value: 'dnd5e.rollDamage',
}
]
// MidiQOL hooks
const MidiQOL =
[
{
name : 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterAttackRoll.Label',
value: 'midi-qol.AttackRollComplete',
disabled: !OIF.OPTIONAL_MODULES.MIDI_QOL?.active,
disabledMessage: 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterAttackRoll.DisabledMessage',
},
{
name : 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterDamageRoll.Label',
value: 'midi-qol.DamageRollComplete',
disabled: !OIF.OPTIONAL_MODULES.MIDI_QOL?.active,
disabledMessage: 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterDamageRoll.DisabledMessage',
},
{
name : 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterCompleteRoll.Label',
value: 'midi-qol.RollComplete',
disabled: !OIF.OPTIONAL_MODULES.MIDI_QOL?.active,
disabledMessage: 'OIF.Settings.AttachHooks.Attack.MidiQOLAfterCompleteRoll.DisabledMessage',
}
]
return System.concat(MidiQOL);
}
////////////////////////////////////////////////////////////
// Return all the hooks that can be used to get item info
////////////////////////////////////////////////////////////
static GetPossibleHooksItem()
{
// System hooks
const System =
[
{
name : 'OIF.Settings.AttachHooks.Item.DnD5AfterItemRoll.Label',
value: 'dnd5e.useItem',
}
]
return System;
}
////////////////////////////////////////////////////////////
// Function that is called when a hook is triggered, note
// that this function is called for every hook, so it is
// important to make sure the data we are extracting is the
// one we want
//
// (for example, if we are using MidiQOL, we only want to
// extract data from the MidiQOL hooks)
////////////////////////////////////////////////////////////
static async ExtractOptions(workflow, source, from)
{
// Check if MidiQOL is active and if the hook is the one from MidiQOL
let ShouldUseMidiQOL = OIF.OPTIONAL_MODULES.MIDI_QOL?.active;
ShouldUseMidiQOL = ShouldUseMidiQOL && source == 'attack';
let UsedHook = from;
let DidUseMidiHook = UsedHook == 'midi-qol.AttackRollComplete' || UsedHook == 'midi-qol.DamageRollComplete' || UsedHook == 'midi-qol.RollComplete';
ShouldUseMidiQOL = ShouldUseMidiQOL && DidUseMidiHook;
if (ShouldUseMidiQOL)
{
DBG.Log('Using MidiQOL hook', source, from, workflow);
// Retrieve the item
let Item = workflow[0].item;
// Retrieve the item's tags
let Tags = ItemTags.Get(Item);
// Retrieve the token
let Token = await canvas.tokens.get(workflow[0].tokenId);
// Retrieve the targets
let Targets = Array.from(game.user.targets);
let HitTargets = workflow[0].hitTargets;
// Special treatment
let DidMiss;
switch (source)
{
case 'attack':
{
// Did it miss?
DidMiss = HitTargets.size === 0 ?? false;
break;
}
case 'item':
{
// Did it miss?
DidMiss = false;
break;
}
}
// Set the options
let Options =
{
name : Tags[0],
item : Item,
tags : Tags,
actor : Token.actor,
token : Token,
targets : Targets,
hitTargets: HitTargets,
miss : HitTargets.size === 0 ?? false,
type : source,
dice :
{
roll : workflow[0].roll,
total : workflow[0].attackTotal,
critical: workflow[0].isCritical,
fumble : workflow[0].isFumble,
},
system :
{
name : 'dnd5e',
meleeWeaponDistance: canvas.dimensions.distance,
normalDistance : Item.system.range.value,
longDistance : Item.system.range.long,
isThrowable : Item.system.properties?.thr,
isConsumeAmmo : Item.system.properties?.amm,
ammoItem : Item.system.consume?.target,
},
}
// Recalculate melee weapon distance if the item has the reach tag
if (Item.system.properties?.rch) { Options.system.meleeWeaponDistance *= 2; }
return Options;
}
// If Midi QOL is not active
else
{
DBG.Log('Using default hook', source, from, workflow);
// Retrieve the item
let Item = workflow[0];
// Retrieve the item's tags
let Tags = ItemTags.Get(Item);
// Retrieve the actor and token
let Actor = Item.parent;
let Token = Actor.token?.object ?? Actor.getActiveTokens()[0];
// Retrieve the targets
let Targets = Array.from(game.user.targets);
let HitTargets = Targets.filter(t => t.actor.system.attributes.ac.value <= workflow[1].total);
// Special treatment
let RollTotal = undefined;
let DidMiss = false;
switch (source)
{
case 'attack':
{
// Did it miss?
DidMiss = workflow[1].isCritical ? false : HitTargets.length === 0 ?? false;
// Retrieve the roll total
let UsedDice = workflow[1].dice[0].results;
UsedDice.filter((d) => d.active);
RollTotal = UsedDice.reduce((a, b) => a + b.result, 0);
break;
}
case 'item':
{
// Did it miss?
DidMiss = false;
break;
}
}
// Set the options
let Options =
{
name : Tags[0],
item : Item,
tags : Tags,
actor : Actor,
token : Token,
targets : Targets,
hitTargets: HitTargets,
miss : DidMiss,
type : source,
dice :
{
roll : RollTotal,
total : workflow[1]?._total,
critical: workflow[1]?.isCritical,
fumble : workflow[1]?.isFumble,
},
system :
{
name : 'dnd5e',
meleeWeaponDistance: canvas.dimensions.distance,
normalDistance : Item.system.range.value,
longDistance : Item.system.range.long,
isThrowable : Item.system.properties?.thr,
isConsumeAmmo : Item.system.properties?.amm,
ammoItem : Item.system.consume?.target,
},
}
// Recalculate melee weapon distance if the item has the reach tag
if (Item.system.properties?.rch) { Options.system.meleeWeaponDistance *= 2; }
return Options;
}
}
}

View File

@@ -0,0 +1,170 @@
import { ObjectsInteractionsFX as OIF } from "../../ObjectsInteractionsFX.js";
import { Debug as DBG } from "../../library/Debug.js";
export class Pf2e
{
////////////////////////////////////////////////////////////
// Return the default hooks
////////////////////////////////////////////////////////////
static GetDefaultHookAttack() { return 'createChatMessage'; }
static GetDefaultHookItem() { return 'createChatMessage'; }
////////////////////////////////////////////////////////////
// Return the default tag pack
////////////////////////////////////////////////////////////
static GetDefaultTagPack()
{
if (game.modules.get('jb2a_patreon')?.active)
{
return 'DefaultFantasyTagsJB2AComplete';
}
else if (game.modules.get('JB2A_DnD5e')?.active)
{
return 'DefaultFantasyTagsJB2AFree';
}
else
{
return 'DefaultFantasyTagsNoAnimations';
}
}
////////////////////////////////////////////////////////////
// Return all the hooks that can be used to get attack info
////////////////////////////////////////////////////////////
static GetPossibleHooksAttack()
{
// System hooks
const System =
[
{
name : 'OIF.Settings.AttachHooks.Attack.Pf2eAfterAttackRoll.Label',
value: 'createChatMessage',
}
]
return System;
}
////////////////////////////////////////////////////////////
// Return all the hooks that can be used to get item info
////////////////////////////////////////////////////////////
static GetPossibleHooksItem()
{
// System hooks
const System =
[
{
name : 'OIF.Settings.AttachHooks.Item.Pf2eAfterItemRoll.Label',
value: 'createChatMessage',
}
]
return System;
}
////////////////////////////////////////////////////////////
// Function that is called when a hook is triggered, note
// that this function is called for every hook, so it is
// important make sure the data we are extracting is the
// one we want
////////////////////////////////////////////////////////////
static async ExtractOptions(workflow, source, from)
{
DBG.Log('Using default hook', source, from, workflow);
// Retrieve the item
let Item = workflow[0].item;
// Retrieve the item's tags;
let Tags = ItemTags.Get(Item);
// Retrieve the token
let Token = canvas.tokens.get(workflow[0].token._id);
// Retrieve the targets
let Targets = Array.from(game.user.targets);
let HitTargets = Targets.filter((t) => true);
// Special treatment
let RollTotal = undefined;
let DidMiss = false;
let DidCrit = false;
let DidFumble = false;
switch (source)
{
case 'attack':
{
// Did it miss?
let Outcome = workflow[0].flags.pf2e.context?.outcome ?? "";
switch (Outcome)
{
case 'criticalFailure': DidMiss = true ; DidFumble = true; break;
case 'failure' : DidMiss = true ; break;
case 'success' : DidMiss = false; break;
case 'criticalSuccess': DidMiss = false; DidCrit = true; break;
default : DidMiss = false; break;
}
// Get the roll total
RollTotal = workflow[0].rolls._total;
break;
}
case 'item':
{
// Did it miss?
DidMiss = false;
break;
}
}
let IsThrowable = false;
let ThrowableDistance = undefined;
Item.system.traits.value.forEach((trait) =>
{
if (trait == 'thrown')
{
IsThrowable = true;
ThrowableDistance = Item.system.range * 7;
}
else if (trait.includes('thrown'))
{
IsThrowable = true;
ThrowableDistance = parseInt(trait.split('-')[1]) * 7;
}
});
let Options =
{
name : Tags[0],
item : Item,
tags : Tags,
actor : Token.actor,
token : Token,
targets : Targets,
hitTargets: HitTargets,
miss : DidMiss,
type : source,
dice :
{
roll : undefined,
total : RollTotal,
critical : DidCrit,
fumble : DidFumble,
},
system :
{
name : 'pf2e',
meleeWeaponDistance: canvas.dimensions.distance,
normalDistance : IsThrowable ? canvas.dimensions.distance : Item.system.range,
longDistance : IsThrowable ? ThrowableDistance : Item.system.range,
isThrowable : IsThrowable,
isConsumeAmmo : false,
ammoItem : ''
}
}
return Options;
}
}

View File

@@ -0,0 +1,152 @@
import { ItemAnimator } from "../animation/ItemAnimator.js";
import { Debug as DBG } from "../library/Debug.js";
import { TokenLightingManipulator } from "../library/TokenLightingManipulator.js";
export class TagHandler
{
static Tags = {
MeleeAttack : {},
RangedAttack: {},
Lighting : {}
}
static async UpdateTags(tags)
{
Object.entries(tags).forEach((tag) => {
switch (tag[1].type)
{
case 'meleeAttack' : TagHandler.Tags.MeleeAttack[tag[0]] = tag[1]; break;
case 'rangedAttack': TagHandler.Tags.RangedAttack[tag[0]] = tag[1]; break;
case 'lighting' : TagHandler.Tags.Lighting[tag[0]] = tag[1]; break;
default: break;
}
});
DBG.Log('Tags got updated', TagHandler.Tags);
}
////////////////////////////////////////////////////////////
// Clean Tags
////////////////////////////////////////////////////////////
static CleanTags(tags)
{
let CleanedTags = tags;
// Remove the tags inside of MeleeAttack
CleanedTags = CleanedTags.filter((tag) =>
{
return TagHandler.Tags.MeleeAttack[tag] == undefined;
});
// Remove the tags inside of RangedAttack
CleanedTags = CleanedTags.filter((tag) =>
{
return TagHandler.Tags.RangedAttack[tag] == undefined;
});
// Remove the tags inside of Lighting
CleanedTags = CleanedTags.filter((tag) =>
{
return TagHandler.Tags.Lighting[tag] == undefined;
});
return tags;
}
////////////////////////////////////////////////////////////
// Dispatch Tags
////////////////////////////////////////////////////////////
static async Dispatch(options)
{
////////////////////////////////////////////////////////////
// Iterate through all the tags trying to find the first
// valid tag
options.tags.every((tag) =>
{
////////////////////////////////////////////////////////////
// Dispatch Melee Attack Tags
////////////////////////////////////////////////////////////
if (TagHandler.Tags.MeleeAttack[tag] != undefined && options.type == 'attack')
{
// Combine the options
let MergedOptions =
{
...options,
...TagHandler.Tags.MeleeAttack[tag]
}
// Check if the tag is enabled
if (MergedOptions.enabled)
{
// Add the other tags
MergedOptions.tags = options.tags;
// Execute the animation
ItemAnimator.MeleeWeaponSingleAttack(MergedOptions);
return false;
}
else
{
return true;
}
}
////////////////////////////////////////////////////////////
// Dispatch Ranged Attack Tags
////////////////////////////////////////////////////////////
else if (TagHandler.Tags.RangedAttack[tag] != undefined && options.type == 'attack')
{
// Combine the options
let MergedOptions =
{
...options,
...TagHandler.Tags.RangedAttack[tag]
}
// Check if the tag is enabled
if (MergedOptions.enabled)
{
// Add the other tags
MergedOptions.tags = options.tags;
// Execute the animation
ItemAnimator.RangedWeaponSingleAttack(MergedOptions);
return false;
}
else
{
return true;
}
}
////////////////////////////////////////////////////////////
// Dispatch Lighting Tags
////////////////////////////////////////////////////////////
else if (TagHandler.Tags.Lighting[tag] != undefined && options.type == 'item')
{
// Combine the option
let MergedOptions =
{
...options,
...TagHandler.Tags.Lighting[tag]
}
// Check if the tag is enabled
if (MergedOptions.enabled)
{
// Add the other tags
MergedOptions.tags = options.tags;
// Execute the animation
TokenLightingManipulator.ToggleItemLighting(MergedOptions);
return false;
}
else
{
return true;
}
}
else
{
DBG.Log('Could not find tag', tag, 'on', TagHandler.Tags);
}
return true;
})
}
}

View File

@@ -0,0 +1,108 @@
.item-tags-list {
gap: 1em;
margin-top: 0;
padding: 0;
}
.item-tags-list > li {
align-items: center;
gap: 0.5em;
}
.item-tags-list input[type="checkbox"] {
margin: 0;
}
.item-tags-list-icon-button {
align-self: center;
background: transparent;
border: 0;
color: inherit;
line-height: normal;
padding: 0;
}
.item-tags-list-icon-button > i {
margin-right: 0;
}
.item-tags-list-icon-button:hover, .item-tags-list-icon-button:focus {
box-shadow: none;
text-shadow: 0 0 5px red;
}
.oif-tag-container {
display: flex;
padding: 4px;
background-color: #FFFFFF33;
border-width: 1px;
border-color: #00000033;
border-style: solid;
border-radius: 1pt;
flex-wrap: wrap;
align-content: flex-start;
overflow-x: scroll;
}
.oif-tag {
margin: 1px;
border-width: 1px;
border-color: #585858;
border-style: solid;
padding: 3px;
background-color: rgb(181, 235, 181);
border-top-left-radius: 5pt;
border-bottom-left-radius: 5pt;
border-top-right-radius: 1pt;
border-bottom-right-radius: 1pt;
align-items: center;
display: inline;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px #fff;
}
.oif-master-tag {
margin: 1px;
border-width: 1px;
border-color: #585858;
border-style: solid;
padding: 3px;
background-color: rgb(202, 181, 235);
border-top-left-radius: 5pt;
border-bottom-left-radius: 5pt;
border-top-right-radius: 1pt;
border-bottom-right-radius: 1pt;
align-items: center;
display: inline;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px #fff;
}
.oif-special-tag {
background-color: rgb(235, 216, 181);
}
.oif-melee-weapon-tag {
background-color: rgb(241, 156, 99);
}
.oif-ranged-weapon-tag {
background-color: rgb(253, 92, 92);
}
.oif-lighting-tag {
background-color: rgb(97, 216, 252);
}
.oif-disabled-tag {
background-color: rgb(106, 103, 110);
}
.oif-tag-input {
background-color: transparent;
border: none;
width: 120px;
flex-grow: 1;
}
.oif-red-button {
background-color: rgb(253, 92, 92);
}

View File

@@ -0,0 +1,83 @@
.oif-settings-container {
padding: 5px;
}
.oif-checkbox-container {
display: inline-flex;
}
.oif-slider-container {
display: inline-flex;
margin-left: 30px;
}
.oif-text-container {
display: inline-flex;
margin-left: 30px;
}
.oif-string-container {
display: inline-flex;
margin-left: 30px;
}
.oif-dropdown-container {
display: inline-flex;
margin-left: 30px;
}
.oif-color-container {
display: inline-flex;
margin-left: 30px;
}
.oif-icon-container {
display: inline-flex;
margin-left: 30px;
}
.oif-option-text-container {
margin-top: 4px;
}
.oif-option-title {
font-size: 14px;
}
.oif-option-hint {
font-size: 12px;
}
.oif-option-slider-container {
display: flex;
width: auto;
}
.oif-option-dropdown-container {
display: flex;
width: auto;
}
.oif-option-color-container {
display: flex;
width: auto;
}
.oif-option-string-container {
display: flex;
width: auto;
}
.oif-option-icon-container {
display: flex;
width: auto;
}
.oif-option-icon-img {
width: 32px;
height: 32px;
}
.oif-disabled-option-text {
color: #ff0000;
}

View File

@@ -0,0 +1,25 @@
<form>
<div class="oif-settings-container">
{{#each config}}
{{#if (eq type "checkbox")}}
{{> "oif.config.checkbox"}}
{{/if}}
{{#if (eq type "slider")}}
{{> "oif.config.slider"}}
{{/if}}
{{#if (eq type "dropdown")}}
{{> "oif.config.dropdown"}}
{{/if}}
{{#if (eq type "string")}}
{{> "oif.config.string"}}
{{/if}}
{{#if (eq type "color")}}
{{> "oif.config.color"}}
{{/if}}
{{#if (eq type "icon")}}
{{> "oif.config.icon"}}
{{/if}}
<hr>
{{/each}}
</div>
</form>

View File

@@ -0,0 +1,17 @@
<form>
<div class="oif-tag-container">
{{#if itemTags}}
{{#each itemTags}}
<div class="oif-tag">
<span>
{{this}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-times" id="{{@index}}"></i>
</a>
</div>
{{/each}}
{{/if}}
<input class="oif-tag-input" autofocus placeholder="new tag"/>
</div>
</form>

View File

@@ -0,0 +1,128 @@
<form>
<div class="oif-settings-container">
<h2 align="center"> {{localize "OIF.Settings.MasterTagsSettings.Pack.Title"}} </h2>
<label for="oif-selected-tag-pack"> {{localize "OIF.Settings.MasterTagsSettings.Pack.Select"}} </label>
<select name="oif-selected-tag-pack" style="width: 570px;">
{{#each packs}}
<option value="{{id}}" {{#if selected}} selected {{/if}} {{#if disabled}} disabled {{/if}}>
{{name}}
{{#if disabled}} {{disabledMessage}} {{/if}}
</option>
{{/each}}
</select>
<span style="display: flex;">
{{#unless currentPack.default}}
<button type="button" id="oif-delete-tag-pack" class="oif-red-button" {{#if canSave}} disabled {{/if}}> <i class="fas fa-times"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Delete.Label"}} </button>
<!--button type="button" id="oif-rename-tag-pack" {{#if canSave}} disabled {{/if}}> <i class="fas fa-pen"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Rename.Label"}} </button-->
<button type="button" id="oif-export-tag-pack" {{#if canSave}} disabled {{/if}}> <i class="fas fa-arrow-down"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Export"}} </button>
{{/unless}}
<button type="button" id="oif-import-tag-pack" {{#if canSave}} disabled {{/if}}> <i class="fas fa-arrow-up"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Import.Title"}} </button>
<button type="button" id="oif-copy-tag-pack" {{#if canSave}} disabled {{/if}}> <i class="fas fa-arrow-right"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Copy"}} </button>
<button type="button" id="oif-create-tag-pack" {{#if canSave}} disabled {{/if}}> <i class="fas fa-plus"></i> {{localize "OIF.Settings.MasterTagsSettings.Pack.Create"}} </button>
</span>
</div>
<h2 align="center"> {{localize "OIF.Settings.MasterTagsSettings.Label"}} </h2>
<input class="oif-tag-search" type="text" style="width: 585px;" placeholder="Search.." {{#if search}} value="{{search}}" {{/if}}>
<hr>
<div class="oif-tag-container">
{{#if tags}}
{{#each tags}}
{{#if (eq type "special")}}
<div class="oif-master-tag oif-special-tag" id="{{@key}}">
<i class="fad fa-star"></i>
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
</div>
{{/if}}
{{#if (eq type "meleeAttack")}}
<div class="oif-master-tag oif-melee-weapon-tag {{#unless enabled}} oif-disabled-tag {{/unless}}" id="{{@key}}">
<i class="fad fa-sword"></i>
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
<a style="margin-right: 5px;">
<i class="fas fa-times"></i>
</a>
</div>
{{/if}}
{{#if (eq type "rangedAttack")}}
<div class="oif-master-tag oif-ranged-weapon-tag {{#unless enabled}} oif-disabled-tag {{/unless}}" id="{{@key}}">
<i class="fad fa-bow-arrow"></i>
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
<a style="margin-right: 5px;">
<i class="fas fa-times"></i>
</a>
</div>
{{/if}}
{{#if (eq type "lighting")}}
<div class="oif-master-tag oif-lighting-tag {{#unless enabled}} oif-disabled-tag {{/unless}}" id="{{@key}}">
<i class="fad fa-lightbulb-on"></i>
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
<a style="margin-right: 5px;">
<i class="fas fa-times"></i>
</a>
</div>
{{/if}}
{{#if (eq type "none")}}
<div class="oif-master-tag {{#unless enabled}} oif-disabled-tag {{/unless}}" id="{{@key}}">
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
<a style="margin-right: 5px;">
<i class="fas fa-times"></i>
</a>
</div>
{{/if}}
{{#unless type}}
<div class="oif-master-tag {{#unless enabled}} oif-disabled-tag {{/unless}}" id="{{@key}}">
<span >
{{@key}}
</span>
<a style="margin-left: 5px; margin-right: 5px;">
<i class="fas fa-wrench"></i>
</a>
<a style="margin-right: 5px;">
<i class="fas fa-times"></i>
</a>
</div>
{{/unless}}
{{/each}}
{{/if}}
<input class="oif-tag-input" autofocus placeholder="new tag"/>
</div>
{{#if canSave}}
<span style="display: flex;">
<button type="button" id="oif-tag-pack-discard-changes" class="oif-red-button"> <i class="fas fa-times"></i>{{localize "OIF.Settings.MasterTagsSettings.DiscardChanges"}}</button>
<button type="button" id="oif-tag-pack-save-changes"> <i class="fas fa-check"></i>{{localize "OIF.Settings.MasterTagsSettings.SaveChanges"}}</button>;
</span>
{{/if}}
</form>

View File

@@ -0,0 +1,19 @@
<form>
<div class="oif-settings-container">
{{#each settings}}
{{#if checkbox}}
{{> "oif.settings.checkbox"}}
{{/if}}
{{#if slider}}
{{> "oif.settings.slider"}}
{{/if}}
{{#if dropdown}}
{{> "oif.settings.dropdown"}}
{{/if}}
{{#if string}}
{{> "oif.settings.string"}}
{{/if}}
<hr>
{{/each}}
</div>
</form>

View File

@@ -0,0 +1,7 @@
<div class="oif-checkbox-container {{#if disabled}} oif-disabled {{/if}}">
<input name="{{name}}" type="checkbox" {{#if disabled}} disabled {{else}} {{#if value}} checked {{/if}} {{/if}}>
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,10 @@
<div class="oif-color-container {{#if disabled}} oif-disabled {{/if}}">
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<div class="oif-option-color-container">
<input name="{{name}}" style="margin-right: 15px;" type="color" value="{{value}}">
<input name="{{name}}" type="text" value="{{value}}">
</div>
<p class="oif-config-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="oif-dropdown-container {{#if disabled}} oif-disabled {{/if}}">
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<div class="oif-option-dropdown-container">
<select name="{{name}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
{{#each choices}}
<option value="{{value}}" {{#if (eq ../value value)}} selected {{/if}}> {{localize name}} </option>
{{/each}}
</select>
</div>
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<div class="oif-icon-container {{#if disabled}} oif-disabled {{/if}}">
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<div class="oif-option-icon-container">
<img class="oif-option-icon-img" src="{{value}}">
{{filePicker target=name type="image"}}
<input name="{{name}}" style="width: 455px;" type="text" value="{{value}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
</div>
<p class="oif-config-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<label class="oif-option-title">
<span>
{{localize title}}
{{#if disabled}}
<i class="oif-disabled-option-text"> {{localize disabledMessage}} </i>
{{/if}}
</span>
</label>

View File

@@ -0,0 +1,10 @@
<div class="oif-slider-container {{#if disabled}} oif-disabled {{/if}}">
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<div class="oif-option-slider-container">
<input name="{{name}}" style="width: 465px; margin-right: 15px;" type="range" min="{{range.min}}" max="{{range.max}}" step="{{range.step}}" value="{{value}}" {{#if disabled}} disabled {{/if}}>
<input name="{{name}}" style="width: 50px;" type="number" min="{{range.min}}" max="{{range.max}}" step="{{range.step}}" value="{{value}}" {{#if disabled}} disabled {{/if}}>
</div>
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="oif-string-container {{#if disabled}} oif-disabled {{/if}}">
<div class="oif-option-text-container">
{{> "oif.config.name"}}
<div class="oif-option-text-container">
<input name="{{name}}" style="width: 530px; margin-right: 15px;" type="text" value="{{value}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
</div>
<p class="oif-config-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,8 @@
<span class="oif-option-title">
<span>
{{localize title}}
{{#if disabled}}
<i class="oif-disabled-option-text"> {{localize disabledMessage}} </i>
{{/if}}
</span>
</span>

View File

@@ -0,0 +1,37 @@
<!-- Lighting -->
<h3> Light </h3>
<span>
<label for="Color"> Color </label>
<input type="color" name="Color" value="{{tag.light.color}}">
</span>
<hr>
<span>
<label for="Alpha"> Alpha </label>
<input type="number" name="Alpha" value="{{tag.light.alpha}}" min="0" max="1">
</span>
<hr>
<span>
<label for="AnimationType"> Animation Type </label>
<select name="AnimationType">
<option value="none" {{#if (eq tag.light.animationType "none")}} selected {{/if}}> None </option>
<option value="pulse" {{#if (eq tag.light.animationType "torch")}} selected {{/if}}> Torch </option>
</select>
</span>
<hr>
<!-- Icons -->
<h3> Icons </h3>
<label for="LitIcon"> Lit </label>
<span style="display: flex;">
<input type="text" name="LitIcon" value="{{tag.icons.lit}}"/>
{{filePicker type="image" target="LitIcon"}}
</span>
<hr>
<label for="UnitIcon"> Unlit </label>
<span style="display: flex;">
<input type="text" name="UnitIcon" value="{{tag.icons.unlit}}"/>
{{filePicker type="image" target="UnitIcon"}}
</span>

View File

@@ -0,0 +1,7 @@
<div class="oif-checkbox-container" {{#if disabled}} style="opacity: 0.5;" {{/if}}>
<input type="checkbox" id="{{referenceID}}" {{#if disabled}} disabled {{else}} {{#if value}} checked {{/if}} {{/if}}>
<div class="oif-option-text-container">
{{> "oif.settings.name"}}
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<div class="oif-dropdown-container" {{#if disabled}} style="opacity: 0.5;;" {{/if}}>
<div class="oif-option-text-container">
{{> "oif.settings.name"}}
<div class="oif-option-dropdown-container">
<select name="{{name}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
{{#each choices}}
<option value="{{value}}" {{#if selected}} selected {{/if}} {{#if disabled}}disabled{{/if}}> {{localize name}} {{#if disabled}} {{localize disabledMessage}} {{/if}} </option>
{{/each}}
</select>
</div>
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,11 @@
<label class="oif-option-title">
<span>
{{localize name}}
{{#if disabled}}
<i class="oif-disabled-option-text"> {{localize disabledMessage}} </i>
{{/if}}
{{#if requiresRestart}}
<i class="oif-disabled-option-text"> {{localize restartMessage}} </i>
{{/if}}
</span>
</label>

View File

@@ -0,0 +1,10 @@
<div class="oif-slider-container" {{#if disabled}} style="opacity: 0.5;" {{/if}}>
<div class="oif-option-text-container">
{{> "oif.settings.name"}}
<div class="oif-option-slider-container">
<input style="width: 465px; margin-right: 15px;" type="range" min="{{range.min}}" max="{{range.max}}" step="{{range.step}}" value="{{value}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
<input style="width: 50px;" type="number" min="{{range.min}}" max="{{range.max}}" step="{{range.step}}" value="{{value}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
</div>
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="oif-string-container" {{#if disabled}} style="opcaity: 0.5;" {{/if}}>
<div class="oif-option-text-container">
{{> "oif.settings.name"}}
<div class="oif-option-text-container">
<input style="width: 565px; margin-right: 15px;" type="text" value="{{value}}" id="{{referenceID}}" {{#if disabled}} disabled {{/if}}>
</div>
<p class="oif-option-hint"> {{localize hint}} </p>
</div>
</div>