diff --git a/README.md b/README.md new file mode 100644 index 0000000..fad724c --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Torch module for Foundry VTT + +This module provides a HUD toggle button for turning on and off a configurable radius of bright and dim light around you. This base function works regardless of game system. + +Additionally, in D&D5e only: +* The single HUD control will trigger the 'Dancing Lights' cantrip if you have it. +* Failing that, it will perforrm the 'Light' cantrip if you have that. +* Failing that, if you have torches, it consumes one, decrementing the quantity on each use. +* The button will show as disabled when you turn on the HUD if you have no torches left. (It doesn't currently disable the button while the HUD remains open, though, after you have extinguished your last remaining torch. Room for improvement.) + +## History + +This module was originally written by @Deuce. After several months of no activity, @lupestro eventually submitted the PR to get its features working reliably in FoundryVTT 0.8. After control transferred to the League, the PR was approved. The module is now somewhat actively maintained by @lupestro. + +## Changelog + +This has needed to be pieced together a bit, but here's what I've gleaned from the GIT history. + +* 1.2.0 - June 10, 2021 - (Lupestro) Updated for 0.8.6, but ensured it still functions in 0.7.x. +* 1.1.4 - October 21, 2020 - (Stephen Hurd) Marked as 0.7.5 compatible. +* 1.1.3 - October 18, 2020 - (Stephen Hurd) Fix spelling. +* 1.1.2 - October 18, 2020 - (Stephen Hurd) Fix JSON syntax. +* 1.1.1 - October 18, 2020 - (Stephen Hurd) Name adjustment. +* 1.1.0 - October 18, 2020 - (Jose E Lozano) Add Spanish, (Stephen Hurd) Fix bright/dim radius of Dancing Lights. +* 1.0.9 - May 28, 2020 - (Stephen Hurd) Marked as 0.6.0 compatible. +* 1.0.8 - May 19, 2020 - (Aymeric DeMoura) Add French, Marked as 0.5.8 compatible. +* 1.0.7 - April 29, 2020 - (Stephen Hurd) Add Chinese, fix torch inventory usage. +* 1.0.6 - April 18, 2020 - (Stephen Hurd) Fix dancing lights removal. +* 1.0.5 - April 18, 2020 - (Stephen Hurd) Remove socket code for dancing lights removal. +* 1.0.4 - April 18, 2020 - (Stephen Hurd) Update to mark as 0.5.4 compatible. +* 1.0.3 - April 15, 2020 - (MtnTiger) - Updated with API changes. +* 1.0.2 - January 22, 2020 - (Stephen Hurd) Update for 0.4.4. +* 1.0.1 - November 26, 2019 - (Stephen Hurd) - Use await on all promises. +* 1.0.0 - November 25, 2019 - (Stephen Hurd) - Add support for Dancing Lights. + +## License + + "THE BEER-WARE LICENSE" (Revision 42): (From torch.js in this module) + + wrote this file. As long as you retain this notice you can do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return. Stephen Hurd diff --git a/module.json b/module.json index f63fe5c..50f69e3 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "name": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "1.1.4", + "version": "1.2.0", "author": "Deuce", "languages": [ { @@ -30,8 +30,8 @@ "socket": true, "styles": [], "packs": [], - "manifest": "https://raw.githubusercontent.com/RealDeuce/torch/master/module.json", - "download": "https://raw.githubusercontent.com/RealDeuce/torch/master/torch.zip", - "minimumCoreVersion": "0.5.4", - "compatibleCoreVersion": "0.7.5" + "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/master/module.json", + "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/master/torch.zip", + "minimumCoreVersion": "0.7.5", + "compatibleCoreVersion": "0.8.6" } diff --git a/torch.js b/torch.js index b15a13e..2fd9e97 100644 --- a/torch.js +++ b/torch.js @@ -15,13 +15,17 @@ class Torch { let hoff = tkn.w; let c = tkn.center; let v = game.settings.get("torch", "dancingLightVision") - - await canvas.scene.createEmbeddedEntity("Token", [ - {"actorData":{}, "actorId":tkn.actor._id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x - hoff, "y":c.y - voff}, - {"actorData":{}, "actorId":tkn.actor._id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x, "y":c.y - voff}, - {"actorData":{}, "actorId":tkn.actor._id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x - hoff, "y":c.y}, - {"actorData":{}, "actorId":tkn.actor._id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x, "y":c.y}], - {"temporary":false, "renderSheet":false}); + let tokens = [ + {"actorData":{}, "actorId":tkn.actor.id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x - hoff, "y":c.y - voff}, + {"actorData":{}, "actorId":tkn.actor.id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x, "y":c.y - voff}, + {"actorData":{}, "actorId":tkn.actor.id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x - hoff, "y":c.y}, + {"actorData":{}, "actorId":tkn.actor.id, "actorLink":false, "bar1":{"attribute":""}, "bar2":{"attribute":""}, "brightLight":0, "brightSight":0, "dimLight":10, "dimSight":0, "displayBars":CONST.TOKEN_DISPLAY_MODES.NONE, "displayName":CONST.TOKEN_DISPLAY_MODES.HOVER, "disposition":CONST.TOKEN_DISPOSITIONS.FRIENDLY, "flags":{}, "height":1, "hidden":false, "img":"systems/dnd5e/icons/spells/light-air-fire-1.jpg", "lightAlpha":1, "lightAngle":360, "lockRotation":false, "mirrorX":false, "name":"Dancing Light", "randomimg":false, "rotation":0, "scale":0.25, "sightAngle":360, "vision":v, "width":1, "x":c.x, "y":c.y}]; + + if (canvas.scene.createEmbeddedDocuments) { // 0.8 + await canvas.scene.createEmbeddedDocuments("Token", tokens, {"temporary":false, "renderSheet":false}); + } else { + await canvas.scene.createEmbeddedEntity("Token", tokens, {"temporary":false, "renderSheet":false}); + } } /* @@ -29,16 +33,22 @@ class Torch { */ function firstGM() { let i; - - for (i=0; i= 4 && game.users.entities[i].active) - return game.users.entities[i].data._id; + if (game.users.contents) { // 0.8 + for (i=0; i= 4 && game.users.contents[i].active) + return game.users.contents[i].data._id; + } + } else { + for (i=0; i= 4 && game.users.entities[i].active) + return game.users.entities[i].data._id; + } } ui.notifications.error("No GM available for Dancing Lights!"); } async function sendRequest(req) { - req.sceneId = canvas.scene._id + req.sceneId = canvas.scene.id ? canvas.scene.id : canvas.scene._id; req.tokenId = app.object.id; if (!data.isGM) { @@ -85,7 +95,8 @@ class Torch { if (torches === null) { var itemToCheck = game.settings.get("torch", "gmInventoryItemName"); if (item.name.toLowerCase() === itemToCheck.toLowerCase()) { - if (item.data.quantity > 0) { + let quantity = typeof item.data.quantity !== "undefined" ? item.data.quantity : item.data.data.quantity; + if (quantity > 0) { torches = itemToCheck; return; } @@ -109,17 +120,18 @@ class Torch { */ async function useTorch() { let torch = -1; + let torchItem; - if (data.isGM && !game.settings.get("torch", "gmUsesInventory")) - return; if (game.system.id !== 'dnd5e') return; + if (data.isGM && !game.settings.get("torch", "gmUsesInventory")) + return; let actor = game.actors.get(data.actorId); if (actor === undefined) return; - // First, check for the light cantrip... - actor.data.items.forEach((item, offset) => { + // First, check for the cantrips... + actor.data.items.forEach((item) => { if (item.type === 'spell') { if (item.name === 'Light') { torch = -2; @@ -130,24 +142,29 @@ class Torch { return; } } - else { + else { var itemToCheck = game.settings.get("torch", "gmInventoryItemName"); - if (torch === -1 && item.name.toLowerCase() === itemToCheck.toLowerCase() && item.data.quantity > 0) { - torch = offset; + if (torch === -1 && item.name.toLowerCase() === itemToCheck.toLowerCase() && + (item.data.data ? item.data.data.quantity : item.data.quantity) > 0) { + torchItem = item; } } }); - if (torch < 0) + if (!torchItem) return; // Now, remove a torch from inventory... - await actor.updateOwnedItem({"_id": actor.data.items[torch]._id, "data.quantity": actor.data.items[torch].data.quantity - 1}); + if (torchItem.data.data) { //0.8 + await torchItem.update({"data.quantity": torchItem.data.data.quantity - 1}); + } else { + await actor.updateOwnedItem({"_id": torchItem._id, "data.quantity": torchItem.data.quantity - 1}); + } } // Don't let Dancing Lights have/use torches. :D if (data.name === 'Dancing Light' && - data.dimLight === 20 && - data.brightLight === 10) { + data.dimLight === 10 && + data.brightLight === 0) { return; } @@ -157,13 +174,14 @@ class Torch { let tbutton = $(`
`); let allowEvent = true; let ht = hasTorch(); - let oldTorch = app.object.getFlag("torch", "oldValue"); - let newTorch = app.object.getFlag("torch", "newValue"); + let tokenFlagHolder = app.object.document ? app.object.document : app.object; + let oldTorch = tokenFlagHolder.getFlag("torch", "oldValue"); + let newTorch = tokenFlagHolder.getFlag("torch", "newValue"); // Clear torch flags if light has been changed somehow. if (newTorch !== undefined && newTorch !== null && newTorch !== 'Dancing Lights' && (newTorch !== data.brightLight + '/' + data.dimLight)) { - await app.object.setFlag("torch", "oldValue", null); - await app.object.setFlag("torch", "newValue", null); + await tokenFlagHolder.setFlag("torch", "oldValue", null); + await tokenFlagHolder.setFlag("torch", "newValue", null); oldTorch = null; newTorch = null; } @@ -190,34 +208,38 @@ class Torch { let btn = $(ev.currentTarget.parentElement); let dimRadius = game.settings.get("torch", "dimRadius"); let brightRadius = game.settings.get("torch", "brightRadius"); - let oldTorch = app.object.getFlag("torch", "oldValue"); - let newTorch = app.object.getFlag("torch", "newValue"); + let oldTorch = tokenFlagHolder.getFlag("torch", "oldValue"); + let newTorch = tokenFlagHolder.getFlag("torch", "newValue"); ev.preventDefault(); ev.stopPropagation(); if (ev.ctrlKey) { // Forcing light off... data.brightLight = game.settings.get("torch", "offBrightRadius"); data.dimLight = game.settings.get("torch", "offDimRadius"); - await app.object.setFlag("torch", "oldValue", null); - await app.object.setFlag("torch", "newValue", null); + await tokenFlagHolder.setFlag("torch", "oldValue", null); + await tokenFlagHolder.setFlag("torch", "newValue", null); await sendRequest({"requestType": "removeDancingLights"}); btn.removeClass("active"); } else if (oldTorch === null || oldTorch === undefined) { // Turning light on... - await app.object.setFlag("torch", "oldValue", data.brightLight + '/' + data.dimLight); + await tokenFlagHolder.setFlag("torch", "oldValue", data.brightLight + '/' + data.dimLight); if (ht === 'Dancing Lights') { await createDancingLights(); - await app.object.setFlag("torch", "newValue", 'Dancing Lights'); + await tokenFlagHolder.setFlag("torch", "newValue", 'Dancing Lights'); } else { if (brightRadius > data.brightLight) data.brightLight = brightRadius; if (dimRadius > data.dimLight) data.dimLight = dimRadius; - await app.object.setFlag("torch", "newValue", data.brightLight + '/' + data.dimLight); + await tokenFlagHolder.setFlag("torch", "newValue", data.brightLight + '/' + data.dimLight); } btn.addClass("active"); - useTorch(); + // The token light data update must happen before we call useTorch(). + // Updating the quantity on the token's embedded torch item, which happens inside useTorch(), triggers a HUD refresh. + // If the token light data isn't updated before that happens, the fresh HUD won't reflect the torch state we just changed. + await tokenFlagHolder.update({brightLight: data.brightLight, dimLight: data.dimLight}); + await useTorch(); } else { // Turning light off... if (newTorch === 'Dancing Lights') { @@ -228,11 +250,12 @@ class Torch { data.brightLight = parseFloat(thereBeLight[0]); data.dimLight = parseFloat(thereBeLight[1]); } - await app.object.setFlag("torch", "newValue", null); - await app.object.setFlag("torch", "oldValue", null); + await tokenFlagHolder.setFlag("torch", "newValue", null); + await tokenFlagHolder.setFlag("torch", "oldValue", null); btn.removeClass("active"); + await tokenFlagHolder.update({brightLight: data.brightLight, dimLight: data.dimLight}); } - await app.object.update({brightLight: data.brightLight, dimLight: data.dimLight}); + console.log("Click completed"); }); } } @@ -241,21 +264,30 @@ class Torch { static async handleSocketRequest(req) { if (req.addressTo === undefined || req.addressTo === game.user._id) { let scn = game.scenes.get(req.sceneId); - let tkn = scn.data.tokens.find(({_id}) => _id === req.tokenId); + let tkn = scn.data.tokens.find((tokx) => tokx.id ? (tokx.id === req.tokenId) : (tokx._id === req.tokenId)); + let tknActorId = tkn.actor ? tkn.actor.id : tkn.actorId; let dltoks=[]; switch(req.requestType) { case 'removeDancingLights': scn.data.tokens.forEach(tok => { - if (tok.actorId === tkn.actorId && + if (tknActorId === (tok.actor ? tok.actor.id : tok.actorId) && tok.name === 'Dancing Light' && - tok.dimLight === 20 && - tok.brightLight === 10) { + 10 === (tok.data ? tok.data.dimLight : tok.dimLight) && + 0 === (tok.data ? tok.data.brightLight : tok.brightLight)) { //let dltok = canvas.tokens.get(tok._id); - dltoks.push(scn.getEmbeddedEntity("Token", tok._id)._id); + if (scn.getEmbeddedDocument) { // 0.8 + dltoks.push(scn.getEmbeddedDocument("Token", tok.id).id); + } else { + dltoks.push(scn.getEmbeddedEntity("Token", tok._id)._id); + } } }); - await scn.deleteEmbeddedEntity("Token", dltoks); + if (scn.deleteEmbeddedDocuments) { // 0.8 + await scn.deleteEmbeddedDocuments("Token", dltoks); + } else { + await scn.deleteEmbeddedEntity("Token", dltoks); + } break; } } diff --git a/torch.zip b/torch.zip index f00b92d..8ccb114 100644 Binary files a/torch.zip and b/torch.zip differ