From a7193987fb9310922fb96170f10b57e29cbc2b7c Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 26 Mar 2022 16:23:39 -0400 Subject: [PATCH 01/26] Code initially updated for v10, cleaned up request handling, streamlined, now v10 only. --- module.json | 6 +- torch.js | 310 ++++++++++++++++++++++------------------------------ 2 files changed, 132 insertions(+), 184 deletions(-) diff --git a/module.json b/module.json index db7dd0c..220678d 100644 --- a/module.json +++ b/module.json @@ -3,7 +3,7 @@ "title": "Torch", "description": "Torch HUD Controls", "version": "1.4.4", - "author": "Deuce", + "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], "languages": [ { "lang": "en", @@ -43,6 +43,6 @@ "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", "url": "https://github.com/League-of-Foundry-Developers/torch", - "minimumCoreVersion": "0.7.5", - "compatibleCoreVersion": "9" + "minimumCoreVersion": "10", + "compatibleCoreVersion": "10" } diff --git a/torch.js b/torch.js index 803787c..e45a989 100644 --- a/torch.js +++ b/torch.js @@ -31,12 +31,13 @@ const CTRL_REF_HTML = (turnOffLights, ctrlOnClick) => { ` } const NEEDED_PERMISSIONS = { - // Don't want to do yourself something you can't undo without a GM - so check for delete on create + // Don't want to do yourself something you can't undo without a GM - + // so check for delete on create 'createDancingLights': ['TOKEN_CREATE','TOKEN_DELETE'], 'removeDancingLights': ['TOKEN_DELETE'] } -let hasPermissionsFor = (user, requestType) => { +let isPermittedTo = (user, requestType) => { if (requestType in NEEDED_PERMISSIONS) { return NEEDED_PERMISSIONS[requestType].every( permission => { return user.can(permission); @@ -45,23 +46,11 @@ let hasPermissionsFor = (user, requestType) => { return true; } } - -// Breaking out light data into its own object is a FoundryVTT 9 feature change -let getLightRadii = (tokenData) => { - return { - bright: tokenData.light ? tokenData.light.bright : tokenData.brightLight, - dim: tokenData.light ? tokenData.light.dim : tokenData.dimLight - }; -} -let newLightRadii = (tokenData, bright, dim) => { - return tokenData.light - ? { "light.bright": bright, "light.dim": dim } - : { brightLight: bright, dimLight: dim }; -} class Torch { - static async createDancingLights(tokenId) { - let token = canvas.tokens.get(tokenId); + // Dancing lights support (including GM request) + + static async createDancingLights(scene, token) { let v = game.settings.get("torch", "dancingLightVision"); let dancingLight = { "actorData":{}, "actorId":token.actor.id, "actorLink":false, @@ -77,55 +66,71 @@ class Torch { "rotation":0, "scale":0.25, "mirrorX":false, "sightAngle":360, "vision":v, "width":1 }; - let voff = token.h; - let hoff = token.w; - let c = token.center; + let voff = scene.grid.size * token.height; + let hoff = scene.grid.size * token.width; + let c = { x: token.x + hoff / 2, y: token.y + voff / 2 }; let tokens = [ Object.assign({"x": c.x - hoff, "y": c.y - voff}, dancingLight), Object.assign({"x": c.x, "y": c.y - voff}, dancingLight), Object.assign({"x": c.x - hoff, "y": c.y }, dancingLight), Object.assign({"x": c.x, "y": c.y }, dancingLight) ]; + await scene.createEmbeddedDocuments( + "Token", tokens, { "temporary":false, "renderSheet":false }); + } + static async removeDancingLights (scene, reqToken) { + let dltoks=[]; + scene.tokens.forEach(token => { + // If the token is a dancing light owned by this actor + if (reqToken.actor.id === token.actor.id && + token.name === 'Dancing Light' + ) { + dltoks.push(scene.getEmbeddedDocument("Token", token.id).id); + } + }); + await scene.deleteEmbeddedDocuments("Token", dltoks); + } - if (canvas.scene.createEmbeddedDocuments) { // 0.8 or higher - await canvas.scene.createEmbeddedDocuments( - "Token", tokens, {"temporary":false, "renderSheet":false}); - } else { // 0.7 or lower - await canvas.scene.createEmbeddedEntity( - "Token", tokens, {"temporary":false, "renderSheet":false}); + /* + * Handle any requests that might have been remoted to a GM via a socket + */ + static async handleSocketRequest(req) { + if (req.addressTo === undefined || req.addressTo === game.user.id) { + let scene = game.scenes.get(req.sceneId); + let token = scene.tokens.get(req.tokenId); + switch(req.requestType) { + case 'removeDancingLights': + await Torch.removeDancingLights(scene, token); + break; + case 'createDancingLights': + await Torch.createDancingLights(scene, token); + break; + } } } /* - * Send a request to the GM to perform the operation or (if you are a GM) - * perform it yourself. + * Send a request to a user permitted to perform the operation or + * (if you are permitted) perform it yourself. */ static async sendRequest(tokenId, req) { - req.sceneId = canvas.scene.id ? canvas.scene.id : canvas.scene._id; + req.sceneId = canvas.scene.id; req.tokenId = tokenId; - if (hasPermissionsFor(game.user, req.requestType)) { + if (isPermittedTo(game.user, req.requestType)) { Torch.handleSocketRequest(req); + return true; } else { - let recipient; - if (game.users.contents) { // 0.8 and up - for (let i=0; i= 4 && - game.users.contents[i].active) - recipient = game.users.contents[i].data._id; - } - } else { // 0.7 and down - for (let i=0; i= 4 && - game.users.entities[i].active) - recipient = game.users.entities[i].data._id; - } - } + let recipient = game.users.contents.find( user => { + return user.active && isPermittedTo(user, req.requestType); + }); if (recipient) { - req.addressTo = recipient; + req.addressTo = recipient.id; game.socket.emit("module.torch", req); + return true; } else { ui.notifications.error("No GM available for Dancing Lights!"); + return false; } } } @@ -146,7 +151,7 @@ class Torch { game.settings.get("torch", "playerTorches"); return game.user.isGM ? 'GM' : playersControlTorches ? 'Player' : ''; } else { - let items = Array.from(game.actors.get(actorId).data.items); + let items = Array.from(game.actors.get(actorId).items); let interestingItems = items .filter( item => (item.type === 'spell' && @@ -166,9 +171,7 @@ class Torch { let torchItem = items.find( (item) => { return item.name.toLowerCase() === itemName.toLowerCase(); }); - let quantity = torchItem.data.data - ? torchItem.data.data.quantity : torchItem.data.quantity; - return quantity > 0 ? itemName : '0'; + return torchItem.system.quantity > 0 ? itemName : '0'; } // GM can always deliver light by fiat without an item return game.user.isGM ? 'GM' : ''; @@ -176,7 +179,7 @@ class Torch { } /* - * Track torch inventory if we are using a torch as our light source. + * Track consumable inventory if we are using a consumable as our light source. */ static async consumeTorch(actorId) { // Protect against all conditions where we should not consume a torch @@ -191,23 +194,12 @@ class Torch { return; // Now we can consume it - let torchItem = Array.from(game.actors.get(actorId).data.items) + let torchItem = Array.from(game.actors.get(actorId).items) .find( (item) => item.name.toLowerCase() === itemName.toLowerCase()); - if (torchItem) { - if (torchItem.data.data) { //0.8 and up - if (torchItem.data.data.quantity > 0) { - await torchItem.update( - {"data.quantity": torchItem.data.data.quantity - 1} - ); - } - } else { //0.7 and down - if (torchItem.data.quantity > 0) { - await game.actors.get(actorId).updateOwnedItem({ - "_id": torchItem._id, - "data.quantity": torchItem.data.quantity - 1 - }); - } - } + if (torchItem && torchItem.system.quantity > 0) { + await torchItem.update( + {"system.quantity": torchItem.system.quantity - 1} + ); } } @@ -216,39 +208,33 @@ class Torch { */ static async addTorchButton(tokenHUD, hudHtml, hudData) { - let tokenId = tokenHUD.object.id; - let tokenDoc = tokenHUD.object.document - ? tokenHUD.object.document : tokenHUD.object; - let tokenData = tokenDoc.data; + let token = tokenHUD.object.document; let itemName = game.system.id === 'dnd5e' ? game.settings.get("torch", "gmInventoryItemName") : ""; let torchDimRadius = game.settings.get("torch", "dimRadius"); let torchBrightRadius = game.settings.get("torch", "brightRadius"); - let currentRadius = getLightRadii(tokenData); - - // Don't let the tokens we create for Dancing Lights have or use torches. - if (tokenData.name === 'Dancing Light' - && currentRadius.dim === 10 && currentRadius.bright === 0 - ) { + + // Don't let the tokens we create for Dancing Lights have or use their own torches. + if (token.name === 'Dancing Light') { return; } - let lightSource = Torch.getLightSourceType(tokenData.actorId, itemName); + let lightSource = Torch.getLightSourceType(token.actor.id, itemName); if (lightSource !== '') { let tbutton = $(BUTTON_HTML); let allowEvent = true; - let oldTorch = tokenDoc.getFlag("torch", "oldValue"); - let newTorch = tokenDoc.getFlag("torch", "newValue"); + let oldTorch = token.getFlag("torch", "oldValue"); + let newTorch = token.getFlag("torch", "newValue"); let tokenTooBright = lightSource !== 'Dancing Lights' - && currentRadius.bright > torchBrightRadius - && currentRadius.dim > torchDimRadius; + && token.light.bright > torchBrightRadius + && token.light.dim > torchDimRadius; // Clear torch flags if light has been changed somehow. - let expectedTorch = currentRadius.bright + '/' + currentRadius.dim; + let expectedTorch = token.light.bright + '/' + token.light.dim; if (newTorch !== undefined && newTorch !== null && newTorch !== 'Dancing Lights' && newTorch !== expectedTorch) { - await tokenDoc.setFlag("torch", "oldValue", null); - await tokenDoc.setFlag("torch", "newValue", null); + await token.setFlag("torch", "oldValue", null); + await token.setFlag("torch", "newValue", null); oldTorch = null; newTorch = null; console.warn( @@ -259,8 +245,7 @@ class Torch { // If newTorch is still set, light hasn't changed. tbutton.addClass("active"); } - else if ( - lightSource === '0' || tokenTooBright) { + else if (lightSource === '0' || tokenTooBright) { let disabledIcon = $(DISABLED_ICON_HTML); tbutton.addClass("fa-stack"); tbutton.find('i').addClass('fa-stack-1x'); @@ -275,7 +260,7 @@ class Torch { ev.preventDefault(); ev.stopPropagation(); await Torch.clickedTorchButton( - buttonElement, ev.altKey, tokenId, tokenDoc, lightSource); + buttonElement, ev.altKey, token, lightSource); }); } } @@ -284,143 +269,106 @@ class Torch { /* * Called when the torch button is clicked */ - static async clickedTorchButton( - button, forceOff, tokenId, tokenDoc, lightSource - ) { + static async clickedTorchButton(button, forceOff, token, lightSource) { debugLog("Torch clicked"); let torchOnDimRadius = game.settings.get("torch", "dimRadius"); let torchOnBrightRadius = game.settings.get("torch", "brightRadius"); let torchOffDimRadius = game.settings.get("torch", "offDimRadius"); let torchOffBrightRadius = game.settings.get("torch", "offBrightRadius"); - let oldTorch = tokenDoc.getFlag("torch", "oldValue"); - let tokenData = tokenDoc.data; - let currentRadius = getLightRadii(tokenData); + let oldTorch = token.getFlag("torch", "oldValue"); if (forceOff) { // Forcing light off... - await tokenDoc.setFlag("torch", "oldValue", null); - await tokenDoc.setFlag("torch", "newValue", null); + await token.setFlag("torch", "oldValue", null); + await token.setFlag("torch", "newValue", null); await Torch.sendRequest( - tokenId, {"requestType": "removeDancingLights"}); + token.id, {"requestType": "removeDancingLights"}); button.removeClass("active"); - await tokenDoc.update(newLightRadii( - tokenData, torchOffBrightRadius, torchOffDimRadius - )); + await token.update({ + "light.bright": torchOffBrightRadius, + "light.dim": torchOffDimRadius + }); debugLog("Force torch off"); // Turning light on... } else if (oldTorch === null || oldTorch === undefined) { - if (currentRadius.bright === torchOnBrightRadius - && currentRadius.dim === torchOnDimRadius + if (token.light.bright === torchOnBrightRadius + && token.light.dim === torchOnDimRadius ) { - await tokenDoc.setFlag( + await token.setFlag( "torch", "oldValue", torchOffBrightRadius + '/' + torchOffDimRadius); console.warn(`Torch: Turning on torch that's already turned on?`); } else { - await tokenDoc.setFlag( + await token.setFlag( "torch", "oldValue", - currentRadius.bright + '/' + currentRadius.dim); + token.light.bright + '/' + token.light.dim); } if (lightSource === 'Dancing Lights') { - await Torch.sendRequest( - tokenId, {"requestType": "createDancingLights"}); - await tokenDoc.setFlag("torch", "newValue", 'Dancing Lights'); - debugLog("Torch dance on"); + if (await Torch.sendRequest( + token.id, {"requestType": "createDancingLights"}) + ) { + await token.setFlag("torch", "newValue", 'Dancing Lights'); + debugLog("Torch dance on"); + button.addClass("active"); + } else { + await token.setFlag("torch", "oldValue", null); + debugLog("Torch dance failed"); + } } else { let newBrightLight = - Math.max(torchOnBrightRadius, currentRadius.bright); + Math.max(torchOnBrightRadius, token.light.bright); let newDimLight = - Math.max(torchOnDimRadius, currentRadius.dim); - await tokenDoc.setFlag( + Math.max(torchOnDimRadius, token.light.dim); + await token.setFlag( "torch", "newValue", newBrightLight + '/' + newDimLight); - await tokenDoc.update(newLightRadii( - tokenData, newBrightLight, newDimLight - )); + await token.update({ + "light.bright": newBrightLight, + "light.dim": newDimLight + }); debugLog("Torch on"); + await Torch.consumeTorch(token.actor.id); } // Any token light data update must happen before we call // consumeTorch(), because the quantity change in consumeTorch() // triggers the HUD to re-render, which triggers addTorchButton again. // addTorchButton won't work right unless the change in light from // the click is already a "done deal". - button.addClass("active"); - await Torch.consumeTorch(tokenData.actorId); } else { // Turning light off... - let oldTorch = tokenDoc.getFlag("torch", "oldValue"); - let newTorch = tokenDoc.getFlag("torch", "newValue"); + let oldTorch = token.getFlag("torch", "oldValue"); + let newTorch = token.getFlag("torch", "newValue"); + let success = true; if (newTorch === 'Dancing Lights') { - await Torch.sendRequest( - tokenId, {"requestType": "removeDancingLights"}); - debugLog("Torch dance off"); + success = await Torch.sendRequest( + token.id, {"requestType": "removeDancingLights"}); + if (success) { + debugLog("Torch dance off"); + } else { + debugLog("Torch dance off failed"); + } } else { // Something got lost - avoiding getting stuck if (oldTorch === newTorch) { - await tokenDoc.update(newLightRadii( - tokenData, torchOffBrightRadius, torchOffDimRadius - )); + await token.update({ + "light.bright": torchOffBrightRadius, + "light.dim": torchOffDimRadius + }); } else { let thereBeLight = oldTorch.split('/'); - await tokenDoc.update(newLightRadii( - tokenData, - parseFloat(thereBeLight[0]), - parseFloat(thereBeLight[1]) - )); + await token.update({ + "light.bright": parseFloat(thereBeLight[0]), + "light.dim": parseFloat(thereBeLight[1]) + }); } debugLog("Torch off"); } - await tokenDoc.setFlag("torch", "newValue", null); - await tokenDoc.setFlag("torch", "oldValue", null); - button.removeClass("active"); - if (lightSource === "0" ){ - await canvas.tokens.hud.render(); - } - } - } - - /* - * Called from socket request and also directly when used by GM - */ - static async handleSocketRequest(req) { - if (req.addressTo === undefined || req.addressTo === game.user.id) { - let scene = game.scenes.get(req.sceneId); - let reqToken = scene.data.tokens.find((token) => { - return token.id - ? (token.id === req.tokenId) - : (token._id === req.tokenId); - }); - let actorId = reqToken.actor ? reqToken.actor.id : reqToken.actorId; - let dltoks=[]; - - switch(req.requestType) { - case 'removeDancingLights': - scene.data.tokens.forEach(token => { - let tokenData = token.data ? token.data : token; - let tokenActorId = token.actor - ? token.actor.id : token.actorId; - let currentRadius = getLightRadii(tokenData); - - // If the token is a dancing light owned by this actor - if (actorId === tokenActorId - && token.name === 'Dancing Light' - && 10 === currentRadius.dim - && 0 === currentRadius.bright - ) { - if (scene.getEmbeddedDocument) { // 0.8 or higher - dltoks.push(scene.getEmbeddedDocument("Token", token.id).id); - } else { // 0.7 or lower - dltoks.push(scene.getEmbeddedEntity("Token", token._id)._id); - } - } - }); - if (scene.deleteEmbeddedDocuments) { // 0.8 or higher - await scene.deleteEmbeddedDocuments("Token", dltoks); - } else { // 0.7 or lower - await scene.deleteEmbeddedEntity("Token", dltoks); - } - break; - case 'createDancingLights': - await Torch.createDancingLights(reqToken.id); + if (success) { + await token.setFlag("torch", "newValue", null); + await token.setFlag("torch", "oldValue", null); + button.removeClass("active"); + if (lightSource === "0" ){ + await canvas.tokens.hud.render(); + } } } } From 52fe62e7f6a24830c3efca9988db75ea75f2e30a Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 2 Apr 2022 09:08:25 -0400 Subject: [PATCH 02/26] Beginning detailed enhancement planning --- design/more-sources.md | 233 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 design/more-sources.md diff --git a/design/more-sources.md b/design/more-sources.md new file mode 100644 index 0000000..e72e9f2 --- /dev/null +++ b/design/more-sources.md @@ -0,0 +1,233 @@ +# Feature: Extending light sources + +## Light Sources in D&D + +The following equipment from the Player's Handbook and SRD can serve as a light source: + +| Item | Bright | Dim | Shape | Consumable | States | +|------|--------|-----| ----- | ----- | -------| +| Candle | 5 | 10 | round | item | unlit, lit | +| Bullseye Lantern | 60 | 120 | cone | oil | unlit, lit | +| Hooded Lantern | 30 (0) | 60 (5) | round | oil | unlit, open, closed | +| Lamp | 15 | 45 | round | oil | unlit, lit | +| Torch | 20 | 40 | round | item | unlit, lit | + +Equipment as light sources: +* are generally able to be held or placed, so they need to be treated like a touch spell. +* can only be controlled when held. +* are either consumed over time or are used with oil that is consumed over time + +The hooded lantern has two different "lit" states that need to be cycled. + +The following spells from the Player's Handbood and SRD are the only ones that are specified as light sources: + +| Spell | Bright | Dim | Shape | range | level | other effects +|------|--------|-----| ----- | --------| ---| ----- | +Continual Flame | 20 | 40 | sphere | touch | 2 | none +Dancing Lights | 0 | 10 | sphere | 120 ft | cantrip | none +Daylight | 60 | 120 | sphere | 60 ft | 3 | none +Fire Shield | 10 | 20 | sphere | self | 4 | flame damage +Flame Blade | 10 | 20 | sphere | self | 2 | melee weapon +Light | 20 | 40 | sphere | touch | cantrip | none +Moonbeam | 0 | 5 | cylinder | 120 ft | 2 | radiant damage +Produce Flame | 10 | 20 | sphere | self | cantrip | thrown weapon +Sunbeam | 30 | 60 | cylinder | self | 6 | radiant damage + +Spells have many of the same issues as equipment, along with a few others. +* The cylinder shape on some spells doesn't cause a problem for an overhead view. +* Spells that aren't cantrips consume spell slots. +* Their activity as a light source is only one facet of their action, and other modules cover the other facets, so you really benefit from casting them in the normal way, not from a flame button on the HUD. + +Here are some things that we can do: +* Held items and self-ranged cantrips just shed light from the selected token. +* When items are placed or spells are cast at range, light is shed from a synthetic token created for the purpose. +* We can wimp out on touch spells by treating them as range spells and manually keeping the synthetic token with the target. +* We can consume torches and candles and spell slots. +* We can ignore consumption impact for oil-based light sources. +* We can track the hood on a hooded lantern by having the on/off toggle become an open/hooded/off toggle. +* We can support Produce Flame if we ignore it's thrown behavior. + +Here are the things that we can't do: +* Attaching touch spell light source effects to their targets. +* Supporting spells with other effects as light sources. +* Tracking consumption of oil or wax over time. +* We can't provide support for directing the beam of the Bullseye Lantern + +Hence, we can support all five of the equipment-based light sources and the spells Continual Flame, Dancing Lights, Daylight, Light, and Produce Flame. We cannot support Fire Shield, Flame Blade, Moonbeam, or Sunbeam. Ten feasible light sources isn't half bad. + +Doing the easiest things first: +* In round 0, we enable the user to choose among the three things we already do support: Torch, Light, and Dancing Lights. +* In round 1, we can deliver "self-only" Candle, Lamp, Torch, Light, and Produce Flame with Candle and Torch consumable. +* In round 2, we can support the tri-state Hooded Lantern. +* In round 3, we can add support for placing the above light sources and picking them up. +* In round 4, we can support consuming spell slots, picking up Continual Flame and Daylight + +I think this is as far as we can reach with this module for D&D5e without profound complications. We can publish each rounnd as we complete it. + +## Proposed User Experience + +The ability to manually choose which spells and items for our HUD to control needs to be very easy to get to but not in the way. A user will have their "go-to" light source most of the time but may want to change it by circumnstance. + +### HUD changes - Light source selection + +I propose that we trigger a light source menu on a right click on the flame icon: +* The light source menu will be a single row of icons representing light sources that the character possesses. +* It will appear to the left of the flame icon, with physical items to the right and spells to the left. +* If the user only has one item, it will still show in the menu. +* If the user has no items, there will be no flame icon to right-click. +* Hovering any of the icons will display its name. +* Clicking one of the items will make it the new selected item. +* Clicking anywhere on the HUD or dismissing the HUD will make the menu disappear. +* Hovering the flame icon will show a tooltip identifying the current selected item. + +The application must keep the identity of the current light source as flagged state. +The selected light source and the list of available light sources will be collected when the HUD is opened. + +### HUD Changes - Placed vs. Held + +It isn't clear how we would transition between a light source being placed and held. Shift-clicking an active lamp icon could spawn an offset token for the placed item. Shift-clicking when the item token is adjacent could remove the token, absorbing the behavior back into the character token. + +### HUD Changes - Hooded Lantern + +In a later round, the behavior of the flame toggle icon will be dependent upon the light source chosen. For a hooded lantern, it will cycle through three states (unlit, open, closed) rather than two (unlit, lit). We will need to keep state for this, and may want to turn the implementation into an explicit state machine. + +## Development plan + + ### Prefactoring - Code structure + + We're adding a fair amount of complexity here. The goal of this round is to restructure the existing code to prepare for growth without adding any new features. + + Here are some concerns we could separate using classes or modules for the varying scopes: + * Concerns locked to the actor - + * What items - spells and equipment - does the actor have that are light sources? + * Handling equipment inventory on light source state changes + * Handling spell slots on light source state changes + * Concerns locked to the HUD - + * Flame button, light source menu + * clicks, events, and visible HUD changes related to light sources + * Concerns locked to the actor's main token in the scene - + * Light levels, original light levels + * Concerns locked to light source type - + * Bright/dim level, light shape, consumable, spell level, range, states + * Overall state - + * selected light source, current light state + +All of our state persists as flags on the actor's primary token for the scene, so all methods can be static. This means we're talking about pure modules of functions rather than stateful objects. + + +After some thought, it will be sufficient to break things up into three modules: + * Torch.js - this is the app. It has all of our hooks, our settings, our test setup, and our socket handling. It also contains our grand logic and calls the other modules. + * Canvas.js - this deals with the elements on the canvas, the actor and its primary token for the scene plus the secondary tokens for dropped and spell light sources. + * Hud.js - this handles the interaction with the torch button and the light source buttons on the token HUD. + +These all lived together comfortably while things were very simple, but the HUD is getting a bit bigger and the canvas is going to have to deal with a bit more variety. This is probably the minimum amount of modularization needed to keep things sensible. + +As we perform this initial refactoring, we may discover some things. Here are a couple we might anticipate: + * we might have to split the actor piece, which manages all the available light sources (including consumption), from its primary token, which holds all the state for this module + * we might have to separate the light sources out individually but I'm hoping we can keep treating the light sources as data and not code. + +### Round 0 - Choosing + +We have three sources we support right now. The user is currently stuck with a hierarchy based on what they have: +* Dancing Lights if they have it. +* Light if they have it. +* Torch if they have it. + +The goal of this round is to enhance the HUD to let the user select, among the light sources that they have, which one they want to use when they click the torch button. + +#### Step 1 - Light source information + +A method providing the character's light sources: +* A list of the light sources the user has - for each: + - item name + - type (spell or equipment), + - inventory (quantity or slots, -1 for unlimited) + - its icon. +* The list should be arranged in the following order: + - Unlimited equipment + - Unlimited spells + - Limited equipment with inventory + - Limited spells with inventory + - Limited equipment out of inventory (slash across) + - Limited spells out of inventory (slash across) + - Alphabetical order within each of the above categories + +#### Step 2 - The light source menu itself + +A menu so the user can select which light source to use: +* We need a horizontal menu of icon buttons + * Each item will appear as an icon of the same dimensions as the flame icon. + * Each item will have a tooltip with the name of the item. +* The menu will receive the list of light sources and the index of the currently selected one. +* Icons will be displayed in the delivered order, from right to left. +* If a light source has been selected, the icon for that light source is outlined in red. +* When the user clicks on one of the items, the menu delivers the index of the item selected. + +#### Step 3 - Basic HUD dynamics for the menu + +* When the user right clicks on the flame icon, the menu must extend to the left of the flame icon. + * Consider a brief transition for the rollout. +* When the user clicks one of the items in the menu, the menu rolls back into the flame icon. + * Consider a corresponding transition for the rollback. + +#### Step 4 - Application support + +* Necessary application state: + * The identity of the currently selected light source + * The activity state of the current light source (lit or not) + * The light level of the player token involved when no light is lit - collected just before lighting a source, restored when extinguishing it. +* Upon opening the HUD, if no light source has been selected, the application defaults to the first item in the list, which should be the least costly one to use. +* When the user right-clicks the flame icon, _if no light source is currently active_, the HUD displays the light source menu. +* When the user selects a light source, the light source they chose becomes the selected light source. +* If that light source is out of inventory, it appears with a slash through it and cannot be lit. + +#### System design considerations + +* Since we are setting up the HUD to only allow one active light source at a time, we can take certain liberties: + * We don't need to keep the active light source separate from the selected light source. + * For placed light sources, we only need to worry about extinguishing one kind. +* It would be useful to have a more positive indicator of light source status than inferring it from the "old" light level. + * This will allow for multi-state light sources (like the Hooded Lantern) in the future. +* Candles, Torches, and similar devices remain in inventory until extinguished. +* Spell slots are expended as soon as the spell is cast. + +#### Responding to external change +We cannot treat the light sources as a closed system - we must respond to external changes from the environment. +* Anticipated types of external change: + * Inventory exhaustion for other reasons - giving away torches, casting other spells of the same level. + * Removal of items from an actor + * Deletion of an actor's token and creation of a new one. + +* To mitigate these: + * The ability to extinguish a lit light source should be unaffected by the presence or condition of the light source in the actor's data. + * If present, equipment inventories should still be adjusted appropriately. + * The slash should still appear upon extinguishment if light source remains and the inventory is reduced to zero. + * Any active light source that "disappears" should immediately become unlit at the earliest convenient opportunity. + * It would be good if this happened before the HUD is reopened, so we may need an event notification. + * In addition, an available light source should be selected. + * Light sources will need to survive replacement of the primary token for an actor in the scene. + * Initially this will only affect Dancing Lights, but it will soon have company + * Since the actor remains the same, the current mechanism of deleting tokens named for the light source belonging to the same actor as the current primary token should work fine. + +### Round 1 - Choices + +Round 1 should just involve adding new light sources to the mix - Candle, Lamp, Produce Flame, Hooded without the hood. Hopefully, it will deliver some of what people are looking for without much more code to test. + +### Round 2 - The 'Hood + +Round 2 should add the third lighting state to the hooded lantern. + +### Round 3 - "I pick things up, I put things down" + +Round 3 should provide the UI for putting down and picking up light sources and casting placed spells other than Dancing Lights. Placed light sources get their own token. + +[Further design TBD] + +### Round 4 - Cast Party + +Round 4 should deal with the intricacies of spells cast at consumable levels, i.e. other than cantrips. If possible, we should piggyback on existing casting mechanics. For the user, this will add light sources from Continual Flame and Daylight. + +[Further design very much TBD] + + + \ No newline at end of file From 1757acf5cdae0524c57fe0f0060b27ad0012162e Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 2 Apr 2022 12:22:10 -0400 Subject: [PATCH 03/26] Split CHANGELOG from README and adjusted module.json to maintain separate active branches for supported release streams. --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 36 +++-------------------------------- module.json | 6 +++--- 3 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..68be5ee --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Change Log + +## Middle Kingdom - v10 branch + +### 2.0.0 - [TBD] + - [BREAKING] (Lupestro) This release supports v10 of Foundry - but only v10 and perhaps beyond. + +## Intermediate period - master branch + +### 1.4.4 - March 19, 2022 + - [BUGFIX] (Lupestro) Dancing Lights now work better for players - sends entire create / remove cycle to GM when users lack permissions on tokens. +### 1.4.3 - December 17, 2021 + - [CLEANUP] (Lupestro) Bumped official compatibility to Foundry 9, after testing with final test version. No code change. +### 1.4.2 - October 31, 2021 + - [FEATURE] (Lupestro) Now works in Foundry 9, but still works in Foundry 7 and 8. + - [BUGFIX] (Lupestro) Function in Foundry 7 is restored - it had broken in restructuring. + - [INTERNAL] (Lupestro) Established test foundation - explicit cases, worlds, automation, fluid dev->test. +### 1.4.1 - October 23, 2021 + - [BUGFIX] (Lupestro) Fixed bug in restructuring that broke features for non-DnD5e. +### 1.4.0 - October 23, 2021 + - [BUGFIX] (C-S-McFarland) Fix for bug when you have torch and light spell. + - [INTERNAL] (Lupestro) Major restructuring with cleanup of race conditions. +### 1.3.2 - June 29, 2021 + - [FEATURE] (Lupestro, zarmstrong, lozalojo) Spanish updates and URL in module.json +### 1.3.1 - June 29, 2021 + - [FEATURE] (Lupestro) Updated zh-tw translation from zeteticl and pt-br translation from rinnocenti to 100% of strings. Thanks y'all! +### 1.3.0 - June 25, 2021 + - [FEATURE] (Lupestro) Incorporated pending Taiwan Chinese and Brazilian Portuguese translations from zeteticl and rinnocenti. +### 1.2.1 - June 11, 2021 + - [CLEANUP] (Lupestro) Cleaned up console logging noise I had created +### 1.2.0 - June 10, 2021 - + - [FEATURE] (Lupestro) Updated for 0.8.6, but ensured it still functions in 0.7.x. + +## Old Kingdom - master branch + +Everything from here down has needed to be pieced together from unearthed inscriptions (the GIT history.) + +* 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. + diff --git a/README.md b/README.md index 8840335..6c87a4c 100644 --- a/README.md +++ b/README.md @@ -7,42 +7,11 @@ Additionally, in D&D5e only: * Failing that, it will perform 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. -## Changelog -This has needed to be pieced together a bit, but here's what I've gleaned from the GIT history. -* 1.4.4 - March 19, 2022 - Dancing Lights now work better for players - sends entire create / remove cycle to GM when users lack permissions on tokens. -* 1.4.3 - December 17, 2021 - Bumped official compatibility to Foundry 9, after testing with final test version. No code change. -* 1.4.2 - October 31, 2021 - (Lupestro) - - [Feature] Now works in Foundry 9, but still works in Foundry 7 and 8. - - [Bugfix] Function in Foundry 7 is restored - it had broken in restructuring. - - [Internal] Established test foundation - explicit cases, worlds, automation, fluid dev->test. -* 1.4.1 - October 23, 2021 - (Lupestro) Fixed bug in restructuring that broke features for non-DnD5e. -* 1.4.0 - October 23, 2021 - - - (C-S-McFarland) Fix for bug when you have torch and light spell. - - (Lupestro) Major restructuring with cleanup of race conditions. -* 1.3.2 - June 29, 2021 - (Lupestro, zarmstrong, lozalojo) Spanish updates and URL in module.json -* 1.3.1 - June 29, 2021 - (Lupestro) Updated zh-tw translation from zeteticl and pt-br translation from rinnocenti to 100% of strings. Thanks y'all! -* 1.3.0 - June 25, 2021 - (Lupestro) Incorporated pending Taiwan Chinese and Brazilian Portuguese translations from zeteticl and rinnocenti. -* 1.2.1 - June 11, 2021 - (Lupestro) Cleaned up console logging noise I had created -* 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. +## Changelog - now in [separate file](./CHANGELOG.md) ## Translation Status + The following is the current status of translation. Some features have arrived, introducing new strings, since translations were last done. | Language | Completion | Contributors | @@ -57,6 +26,7 @@ The following is the current status of translation. Some features have arrived, PRs for further translations will be dealt with promptly. While German, Japanese, and Korean are most especially desired - our translation story seems deeply incomplete without them - all others are welcome. It's only 16 strings so far, a satisfying afternoon, even for someone who's never committed to an open source project before, and your name will go into the readme right here next to the language. Fork, clone, update, _test locally_, commit, and then submit a PR. Holler for @lupestro on Discord if you need help getting started. + ## History This module was originally written by @Deuce. After it sustained several months of inactivity in 2021, @lupestro submitted a PR to get its features working reliably in FoundryVTT 0.8. Deuce agreed to transfer control to the League with @lupestro as maintainer, the changes were committed and a release was made from the League fork. All the PRs that were open at the time of the transfer are now committed and attention has turned to fulfilling some of the feature requests in a maintainable way while retaining the "one-button" character of the original module. diff --git a/module.json b/module.json index 220678d..6a04f10 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "name": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "1.4.4", + "version": "2.0.0", "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], "languages": [ { @@ -40,8 +40,8 @@ "socket": true, "styles": [], "packs": [], - "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", + "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", + "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", "url": "https://github.com/League-of-Foundry-Developers/torch", "minimumCoreVersion": "10", "compatibleCoreVersion": "10" From f5163ac605fb1a8453e34fd6d8b7ff68a190bcd5 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sun, 3 Apr 2022 17:00:16 -0400 Subject: [PATCH 04/26] First iteration of source variety --- hud.js | 124 +++++++++++ settings.js | 118 +++++++++++ socket.js | 64 ++++++ source-specs.js | 112 ++++++++++ sources.js | 92 +++++++++ token.js | 202 ++++++++++++++++++ torch.js | 536 ++++++++---------------------------------------- 7 files changed, 792 insertions(+), 456 deletions(-) create mode 100644 hud.js create mode 100644 settings.js create mode 100644 socket.js create mode 100644 source-specs.js create mode 100644 sources.js create mode 100644 token.js diff --git a/hud.js b/hud.js new file mode 100644 index 0000000..9c8d82a --- /dev/null +++ b/hud.js @@ -0,0 +1,124 @@ +const BUTTON_HTML = `
`; +const DISABLED_ICON_HTML = ``; +const SOURCE_MENU = `
`; +const SOURCE_MENU_ITEM = (img, tooltip) => { + return ``; +}; +//style="padding: 4px 2px" +//style="border: 1px solid var(--color-border-light-primary); padding:0" +//style="margin:0" + +export default class TokenHUD { + /* + * Add a torch button to the Token HUD - called from TokenHUD render hook + */ + static async addFlameButton( + token, + hudHtml, + forceLightSourceOff, + toggleLightSource, + togglelightHeld, + changeLightSource + ) { + let state = token.lightSourceState; + let disabled = token.currentLightSourceIsExhausted; + let allowEvent = !disabled; + let tbutton = $(BUTTON_HTML); + if (state === token.STATE_ON) { + tbutton.addClass("active"); + } else if (state === token.STATE_DIM) { + tbutton.addClass("active"); + } else if (disabled) { + let disabledIcon = $(DISABLED_ICON_HTML); + tbutton.addClass("fa-stack"); + tbutton.find("i").addClass("fa-stack-1x"); + disabledIcon.addClass("fa-stack-1x"); + tbutton.append(disabledIcon); + } + hudHtml.find(".col.left").prepend(tbutton); + tbutton.find("i").contextmenu(async (event) => { + event.preventDefault(); + event.stopPropagation(); + if (token.lightSourceState === token.STATE_OFF) { + TokenHUD.toggleSourceMenu(tbutton, token, changeLightSource); + } + }); + if (allowEvent) { + tbutton.find("i").click(async (event) => { + event.preventDefault(); + event.stopPropagation(); + if (!tbutton.next().hasClass("light-source-menu")) { + if (event.shiftKey) { + togglelightHeld(token); + } else if (event.altKey) { + await forceLightSourceOff(token); + TokenHUD.syncFlameButtonState(tbutton, token); + } else { + await toggleLightSource(token); + TokenHUD.syncFlameButtonState(tbutton, token); + } + } + }); + } + } + + static toggleSourceMenu(button, token, changeLightSource) { + // If we already have a menu, toggle it away + let maybeOldMenu = button.next(); + if (maybeOldMenu.hasClass("light-source-menu")) { + maybeOldMenu.remove(); + return; + } + // If we don't have a menu, show it + let menu = $(SOURCE_MENU); + let sources = token.ownedLightSources; + let currentSource = token.currentLightSource; + for (let source of sources) { + let child = $(SOURCE_MENU_ITEM(source.image, source.name)); + if (source.name === currentSource) { + child.css("border-color", "tomato"); + } + if (token.sourceIsExhausted(source.name)) { + child.css("opacity", "0.3"); + } + child.click(async (ev) => { + let menu = $(ev.currentTarget.parentElement); + await changeLightSource(token, source.name); + TokenHUD.syncDisabledState(button, token); + menu.remove(); + }); + menu.append(child); + } + button.after(menu); + } + + static syncDisabledState(tbutton, token) { + let oldSlash = tbutton.find(".fa-slash"); + let wasDisabled = oldSlash.length > 0; + let willBeDisabled = token.currentLightSourceIsExhausted; + if (!wasDisabled && willBeDisabled) { + let disabledIcon = $(DISABLED_ICON_HTML); + tbutton.addClass("fa-stack"); + tbutton.find("i").addClass("fa-stack-1x"); + disabledIcon.addClass("fa-stack-1x"); + tbutton.append(disabledIcon); + } else if (wasDisabled && !willBeDisabled) { + oldSlash.remove(); + tbutton.find("i").removeClass("fa-stack-1x"); + tbutton.removeClass("fa-stack"); + } + } + + static syncFlameButtonState(tButton, token) { + let state = token.lightSourceState; + if (state === token.STATE_ON) { + tButton.addClass("active"); + } else if (state === token.STATE_DIM) { + tButton.addClass("active"); + } else { + tButton.removeClass("active"); + } + } +} diff --git a/settings.js b/settings.js new file mode 100644 index 0000000..a0cf76b --- /dev/null +++ b/settings.js @@ -0,0 +1,118 @@ +// TODO: Add help for right-click (select source) and shift-click (toggle hold) +const CTRL_REF_HTML = (turnOffLights, ctrlOnClick) => { + return ` +

Torch

+
    +
  1. +

    ${turnOffLights}

    +
    ${ctrlOnClick}
    +
  2. +
+`; +}; + +export default class Settings { + static get playerTorches() { + return game.settings.get("torch", "playerTorches"); + } + static get gmUsesInventory() { + return game.system.id === "dnd5e" + ? game.settings.get("torch", "gmUsesInventory") + : false; + } + static get inventoryItemName() { + return game.system.id === "dnd5e" + ? game.settings.get("torch", "gmInventoryItemName") + : undefined; + } + static get litRadii() { + return { + bright: game.settings.get("torch", "brightRadius"), + dim: game.settings.get("torch", "dimRadius"), + }; + } + static get offRadii() { + return { + bright: game.settings.get("torch", "offBrightRadius"), + dim: game.settings.get("torch", "offDimRadius"), + }; + } + static get dancingLightsVision() { + return game.settings.get("torch", "dancingLightVision"); + } + + static get helpText() { + let turnOffLights = game.i18n.localize("torch.turnOffAllLights"); + let ctrlOnClick = game.i18n.localize("torch.holdCtrlOnClick"); + return CTRL_REF_HTML(turnOffLights, ctrlOnClick); + } + + static register() { + game.settings.register("torch", "playerTorches", { + name: game.i18n.localize("torch.playerTorches.name"), + hint: game.i18n.localize("torch.playerTorches.hint"), + scope: "world", + config: true, + default: true, + type: Boolean, + }); + if (game.system.id === "dnd5e") { + game.settings.register("torch", "gmUsesInventory", { + name: game.i18n.localize("torch.gmUsesInventory.name"), + hint: game.i18n.localize("torch.gmUsesInventory.hint"), + scope: "world", + config: true, + default: false, + type: Boolean, + }); + game.settings.register("torch", "gmInventoryItemName", { + name: game.i18n.localize("torch.gmInventoryItemName.name"), + hint: game.i18n.localize("torch.gmInventoryItemName.hint"), + scope: "world", + config: true, + default: "torch", + type: String, + }); + } + game.settings.register("torch", "brightRadius", { + name: game.i18n.localize("LIGHT.LightBright"), + hint: game.i18n.localize("torch.brightRadius.hint"), + scope: "world", + config: true, + default: 20, + type: Number, + }); + game.settings.register("torch", "dimRadius", { + name: game.i18n.localize("LIGHT.LightDim"), + hint: game.i18n.localize("torch.dimRadius.hint"), + scope: "world", + config: true, + default: 40, + type: Number, + }); + game.settings.register("torch", "offBrightRadius", { + name: game.i18n.localize("torch.offBrightRadius.name"), + hint: game.i18n.localize("torch.offBrightRadius.hint"), + scope: "world", + config: true, + default: 0, + type: Number, + }); + game.settings.register("torch", "offDimRadius", { + name: game.i18n.localize("torch.offDimRadius.name"), + hint: game.i18n.localize("torch.offDimRadius.hint"), + scope: "world", + config: true, + default: 0, + type: Number, + }); + game.settings.register("torch", "dancingLightVision", { + name: game.i18n.localize("torch.dancingLightVision.name"), + hint: game.i18n.localize("torch.dancingLightVision.hint"), + scope: "world", + config: true, + default: false, + type: Boolean, + }); + } +} diff --git a/socket.js b/socket.js new file mode 100644 index 0000000..9884e03 --- /dev/null +++ b/socket.js @@ -0,0 +1,64 @@ +import LightSource from "./sources.js"; + +export default class TorchSocket { + /* + * Handle any requests that might have been remoted to a GM via a socket + */ + static async handleSocketRequest(req) { + if (req.addressTo === undefined || req.addressTo === game.user.id) { + let scene = game.scenes.get(req.sceneId); + let token = scene.tokens.get(req.tokenId); + if (LightSource.supports(req.requestType)) { + await LightSource.perform(req.requestType, scene, token); + } else { + console.warning( + `Torch | --- Attempted unregistered socket action ${req.requestType}` + ); + } + } + } + + /* + * See if this light source supports a socket request for this action + */ + static requestSupported(action, lightSource) { + return LightSource.supports(`${action}:${lightSource}`); + } + + /* + * Send a request to a user permitted to perform the operation or + * (if you are permitted) perform it yourself. + */ + static async sendRequest(tokenId, action, lightSource) { + let req = { + requestType: `${action}:${lightSource}`, + sceneId: canvas.scene.id, + tokenId: tokenId, + }; + + if (LightSource.supports(req.requestType)) { + if (LightSource.isPermitted(game.user, req.requestType)) { + TorchSocket.handleSocketRequest(req); + return true; + } else { + let recipient = game.users.contents.find((user) => { + return ( + user.active && LightSource.isPermitted(user, req.requestType) + ); + }); + if (recipient) { + req.addressTo = recipient.id; + game.socket.emit("module.torch", req); + return true; + } else { + ui.notifications.error("No GM available for Dancing Lights!"); + return false; + } + } + } else { + console.warning( + `Torch | --- Requested unregistered socket action ${req.requestType}` + ); + } + } +} diff --git a/source-specs.js b/source-specs.js new file mode 100644 index 0000000..a0ed3ee --- /dev/null +++ b/source-specs.js @@ -0,0 +1,112 @@ +import Settings from "./settings.js"; + +const DND5E_LIGHT_SOURCES = { + Candle: { + name: "Candle", + light: [{ bright: 5, dim: 10 }], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + Torch: { + name: "Torch", + light: [{ bright: 20, dim: 40 }], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + Lamp: { + name: "Lamp", + light: [{ bright: 15, dim: 45 }], + shape: "sphere", + type: "equipment", + consumable: false, + states: 2, + }, + "Bullseye Lantern": { + name: "Bullseye Lantern", + light: [{ bright: 15, dim: 45 }], + shape: "cone", + type: "equipment", + consumable: false, + states: 2, + }, + "Hooded Lantern": { + name: "Hooded Lantern", + light: [ + { bright: 30, dim: 60 }, + { bright: 0, dim: 5 }, + ], + shape: "sphere", + type: "equipment", + consumable: false, + states: 3, + }, + Light: { + name: "Light", + light: [{ bright: 20, dim: 40 }], + shape: "sphere", + type: "cantrip", + consumable: false, + states: 2, + }, + "Dancing Lights": { + name: "Dancing Lights", + light: [{ bright: 0, dim: 10 }], + shape: "sphere", + type: "cantrip", + consumable: false, + states: 2, + }, +}; + + +export default class SourceSpecs { + static get lightSources() { + let itemName = Settings.inventoryItemName; + let sources = {}; + switch (game.system.id) { + case "dnd5e": + sources = Object.assign({}, DND5E_LIGHT_SOURCES); + if (!SourceSpecs.find(itemName, sources)) { + sources[itemName] = { + name: itemName, + light: [ + { + bright: Settings.litRadii.bright, + dim: Settings.litRadii.dim, + }, + ], + shape: "sphere", + type: "none", + consumable: false, + states: 2, + }; + } + break; + default: + sources["Default"] = { + name: "Default", + light: [ + { bright: Settings.litRadii.bright, dim: Settings.litRadii.dim }, + ], + shape: "sphere", + type: "none", + consumable: false, + states: 2, + }; + } + return sources; + } + static find(name, sources) { + for (let sourceName in sources) { + if (sourceName.toLowerCase() === name.toLowerCase()) { + return sources[sourceName]; + } + } + return false; + }; + +} diff --git a/sources.js b/sources.js new file mode 100644 index 0000000..d863d67 --- /dev/null +++ b/sources.js @@ -0,0 +1,92 @@ +const NEEDED_PERMISSIONS = { + // Don't want to do yourself something you can't undo without a GM - + // so check for delete on create + "create:Dancing Lights": ["TOKEN_CREATE", "TOKEN_DELETE"], + "delete:Dancing Lights": ["TOKEN_DELETE"], +}; + +export default class LightSource { + static ACTIONS = { + "create:Dancing Lights": LightSource.createDancingLights, + "delete:Dancing Lights": LightSource.removeDancingLights, + }; + static isPermitted(user, requestType) { + if (requestType in NEEDED_PERMISSIONS) { + return NEEDED_PERMISSIONS[requestType].every((permission) => { + return user.can(permission); + }); + } else { + return true; + } + } + + static supports(requestType) { + return requestType in LightSource.ACTIONS; + } + + static perform(requestType, scene, token) { + LightSource.ACTIONS[requestType](scene, token); + } + + // Dancing lights + + static async createDancingLights(scene, token) { + let v = game.settings.get("torch", "dancingLightVision"); + let dancingLight = { + actorData: {}, + actorId: token.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, + name: "Dancing Light", + randomimg: false, + rotation: 0, + scale: 0.25, + mirrorX: false, + sightAngle: 360, + vision: v, + width: 1, + }; + let voff = scene.grid.size * token.height; + let hoff = scene.grid.size * token.width; + let c = { x: token.x + hoff / 2, y: token.y + voff / 2 }; + let tokens = [ + Object.assign({ x: c.x - hoff, y: c.y - voff }, dancingLight), + Object.assign({ x: c.x, y: c.y - voff }, dancingLight), + Object.assign({ x: c.x - hoff, y: c.y }, dancingLight), + Object.assign({ x: c.x, y: c.y }, dancingLight), + ]; + await scene.createEmbeddedDocuments("Token", tokens, { + temporary: false, + renderSheet: false, + }); + } + + static async removeDancingLights(scene, reqToken) { + let dltoks = []; + scene.tokens.forEach((token) => { + // If the token is a dancing light owned by this actor + if ( + reqToken.actor.id === token.actor.id && + token.name === "Dancing Light" + ) { + dltoks.push(scene.getEmbeddedDocument("Token", token.id).id); + } + }); + await scene.deleteEmbeddedDocuments("Token", dltoks); + } +} diff --git a/token.js b/token.js new file mode 100644 index 0000000..745f14e --- /dev/null +++ b/token.js @@ -0,0 +1,202 @@ +import TorchSocket from "./socket.js"; +import Settings from "./settings.js"; +import SourceSpecs from "./source-specs.js"; + +let DEBUG = true; + +let debugLog = (...args) => { + if (DEBUG) { + console.log(...args); + } +}; + +let getAngle = (shape) => { + switch (shape) { + case "cone": + return 53.13; + case "sphere": + default: + return 360; + } +}; + +export default class TorchToken { + STATE_ON = "on"; + STATE_DIM = "dim"; + STATE_OFF = "off"; + token; + + constructor(token) { + this.token = token; + } + // Flags + get currentLightSource() { + let lightSource = this.token.getFlag("torch", "lightSource"); + let owned = this.ownedLightSources; + if (lightSource && owned.find((item) => item.name === lightSource)) + return lightSource; + let itemName = Settings.inventoryItemName; + let sourceData = itemName + ? owned.find((item) => item.name.toLowerCase() === itemName.toLowerCase()) + : undefined; + return itemName && !!sourceData + ? sourceData.name + : "Default" in owned + ? "Default" + : undefined; + } + async setCurrentLightSource(value) { + await this.token.setFlag("torch", "lightSource", value); + } + get lightSourceState() { + let state = this.token.getFlag("torch", "lightSourceState"); + return typeof state === "undefined" ? this.STATE_OFF : state; + } + get ownedLightSources() { + let allSources = SourceSpecs.lightSources; + let items = Array.from(game.actors.get(this.token.actorId).items).filter( + (item) => { + let itemSource = SourceSpecs.find(item.name, allSources); + if (item.type === "spell") { + return !!itemSource && itemSource.type === "cantrip"; + } else { + return !!itemSource && itemSource.type === "equipment"; + } + } + ); + if (items.length > 0) { + return items.map((item) => { + return Object.assign( + { image: item.img, quantity: item.quantity }, + allSources[item.name] + ); + }); + } else if ("Default" in allSources) { + return [allSources["Default"]]; + } + } + + get currentLightSourceIsExhausted() { + return this.sourceIsExhausted(this.currentLightSource); + } + + sourceIsExhausted(source) { + let allSources = SourceSpecs.lightSources; + if (allSources[source].consumable) { + // Now we can consume it + let torchItem = Array.from( + game.actors.get(this.token.actorId).items + ).find((item) => item.name.toLowerCase() === source.toLowerCase()); + return torchItem && torchItem.system.quantity === 0; + } + return false; + } + + /* Orchestrate State Management */ + async forceStateOff() { + // Need to deal with dancing lights + await this.token.setFlag("torch", "lightSourceState", this.STATE_OFF); + await this.turnOffSource(); + } + + async advanceState() { + let source = this.currentLightSource; + let state = this.lightSourceState; + let allSources = SourceSpecs.lightSources; + if (allSources[source].states === 3) { + state = + state === this.STATE_OFF + ? this.STATE_ON + : state === this.STATE_ON + ? this.STATE_DIM + : this.STATE_OFF; + } else { + state = state === this.STATE_OFF ? this.STATE_ON : this.STATE_OFF; + } + await this.token.setFlag("torch", "lightSourceState", state); + switch (state) { + case this.STATE_OFF: + await this.turnOffSource(); + break; + case this.STATE_ON: + await this.turnOnSource(); + break; + case this.STATE_DIM: + await this.dimSource(); + break; + default: + await this.turnOffSource(); + } + return state; + } + async turnOffSource() { + if (TorchSocket.requestSupported("delete", this.currentLightSource)) { + // separate token lighting + TorchSocket.sendRequest(this.token.id, "delete", this.currentLightSource); + } else { + // self lighting + let sourceData = SourceSpecs.lightSources[this.currentLightSource]; + await this.token.update({ + "light.bright": Settings.offRadii.bright, + "light.dim": Settings.offRadii.dim, + "light.angle": 360, + }); + if (sourceData.consumable && sourceData.type === "equipment") { + this.consumeSource(); + } + } + } + async turnOnSource() { + if (TorchSocket.requestSupported("create", this.currentLightSource)) { + // separate token lighting + TorchSocket.sendRequest(this.token.id, "create", this.currentLightSource); + } else { + // self lighting + let sourceData = SourceSpecs.lightSources[this.currentLightSource]; + await this.token.update({ + "light.bright": sourceData.light[0].bright, + "light.dim": sourceData.light[0].dim, + "light.angle": getAngle(sourceData.shape), + }); + if (sourceData.consumable && sourceData.type === "spell") { + this.consumeSource(); + } + } + } + + async dimSource() { + let sourceData = SourceSpecs.lightSources[this.currentLightSource]; + if (sourceData.states === 3) { + await this.token.update({ + "light.bright": sourceData.light[1].bright, + "light.dim": sourceData.light[1].dim, + "light.angle": getAngle(sourceData.shape), + }); + } + } + + async consumeSource() { + let sourceData = SourceSpecs.lightSources[this.currentLightSource]; + let torchItem = Array.from(game.actors.get(this.token.actorId).items).find( + (item) => item.name === sourceData.name + ); + if ( + torchItem && + sourceData.consumable && + (!game.user.isGM || Settings.gmUsesInventory) + ) { + if (sourceData.type === "spell" && torchItem.type === "spell") { + //TODO: Figure out how to consume spell levels - and whether we want to + } else if ( + sourceData.type === "equipment" && + torchItem.type !== "spell" + ) { + if (torchItem.system.quantity > 0) { + await torchItem.update({ + "system.quantity": torchItem.system.quantity - 1, + }); + } + } + } + } +} diff --git a/torch.js b/torch.js index e45a989..5113aa0 100644 --- a/torch.js +++ b/torch.js @@ -1,3 +1,8 @@ +import Settings from "./settings.js"; +import TorchSocket from "./socket.js"; +import TokenHUD from "./hud.js"; +import TorchToken from "./token.js"; + /* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): @@ -10,468 +15,87 @@ let DEBUG = true; let debugLog = (...args) => { - if (DEBUG) { - console.log (...args); - } -} - -const BUTTON_HTML = - `
`; -const DISABLED_ICON_HTML = - ``; -const CTRL_REF_HTML = (turnOffLights, ctrlOnClick) => { - return ` -

Torch

-
    -
  1. -

    ${turnOffLights}

    -
    ${ctrlOnClick}
    -
  2. -
-` -} -const NEEDED_PERMISSIONS = { - // Don't want to do yourself something you can't undo without a GM - - // so check for delete on create - 'createDancingLights': ['TOKEN_CREATE','TOKEN_DELETE'], - 'removeDancingLights': ['TOKEN_DELETE'] -} - -let isPermittedTo = (user, requestType) => { - if (requestType in NEEDED_PERMISSIONS) { - return NEEDED_PERMISSIONS[requestType].every( permission => { - return user.can(permission); - }) - } else { - return true; - } -} + if (DEBUG) { + console.log(...args); + } +}; class Torch { - - // Dancing lights support (including GM request) - - static async createDancingLights(scene, token) { - let v = game.settings.get("torch", "dancingLightVision"); - let dancingLight = { - "actorData":{}, "actorId":token.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, - "name":"Dancing Light", "randomimg":false, - "rotation":0, "scale":0.25, "mirrorX":false, - "sightAngle":360, "vision":v, "width":1 - }; - let voff = scene.grid.size * token.height; - let hoff = scene.grid.size * token.width; - let c = { x: token.x + hoff / 2, y: token.y + voff / 2 }; - let tokens = [ - Object.assign({"x": c.x - hoff, "y": c.y - voff}, dancingLight), - Object.assign({"x": c.x, "y": c.y - voff}, dancingLight), - Object.assign({"x": c.x - hoff, "y": c.y }, dancingLight), - Object.assign({"x": c.x, "y": c.y }, dancingLight) - ]; - await scene.createEmbeddedDocuments( - "Token", tokens, { "temporary":false, "renderSheet":false }); - } - static async removeDancingLights (scene, reqToken) { - let dltoks=[]; - scene.tokens.forEach(token => { - // If the token is a dancing light owned by this actor - if (reqToken.actor.id === token.actor.id && - token.name === 'Dancing Light' - ) { - dltoks.push(scene.getEmbeddedDocument("Token", token.id).id); - } - }); - await scene.deleteEmbeddedDocuments("Token", dltoks); - } - - /* - * Handle any requests that might have been remoted to a GM via a socket - */ - static async handleSocketRequest(req) { - if (req.addressTo === undefined || req.addressTo === game.user.id) { - let scene = game.scenes.get(req.sceneId); - let token = scene.tokens.get(req.tokenId); - switch(req.requestType) { - case 'removeDancingLights': - await Torch.removeDancingLights(scene, token); - break; - case 'createDancingLights': - await Torch.createDancingLights(scene, token); - break; - } - } - } - - /* - * Send a request to a user permitted to perform the operation or - * (if you are permitted) perform it yourself. - */ - static async sendRequest(tokenId, req) { - req.sceneId = canvas.scene.id; - req.tokenId = tokenId; - - if (isPermittedTo(game.user, req.requestType)) { - Torch.handleSocketRequest(req); - return true; - } else { - let recipient = game.users.contents.find( user => { - return user.active && isPermittedTo(user, req.requestType); - }); - if (recipient) { - req.addressTo = recipient.id; - game.socket.emit("module.torch", req); - return true; - } else { - ui.notifications.error("No GM available for Dancing Lights!"); - return false; - } - } - } - - /* - * Identify the type of light source we will be using. - * Outside of D&D5e, either a player or GM can call for "fiat-lux". - * Within DND5e, it invokes: - * - One of the spells if you've got it - first Dancing Lights then Light. - * - Otherwise, the specified torch item if you've got it. - * - An indicator if you ran out of torches. - * - Failing all of those, a player doesn't even get a button to click. - * - However, a GM can always call for "fiat-lux". - */ - static getLightSourceType(actorId, itemName) { - if (game.system.id !== 'dnd5e') { - let playersControlTorches = - game.settings.get("torch", "playerTorches"); - return game.user.isGM ? 'GM' : playersControlTorches ? 'Player' : ''; - } else { - let items = Array.from(game.actors.get(actorId).items); - let interestingItems = items - .filter( item => - (item.type === 'spell' && - (item.name === 'Light' || item.name === 'Dancing Lights')) || - (item.type !== 'spell' && - itemName.toLowerCase() === item.name.toLowerCase())) - .map( item => item.name); - - // Spells - if (interestingItems.includes('Dancing Lights')) - return 'Dancing Lights'; - if (interestingItems.includes('Light')) - return 'Light'; - - // Item if available - if (interestingItems.length > 0) { - let torchItem = items.find( (item) => { - return item.name.toLowerCase() === itemName.toLowerCase(); - }); - return torchItem.system.quantity > 0 ? itemName : '0'; - } - // GM can always deliver light by fiat without an item - return game.user.isGM ? 'GM' : ''; - } - } - - /* - * Track consumable inventory if we are using a consumable as our light source. - */ - static async consumeTorch(actorId) { - // Protect against all conditions where we should not consume a torch - if (game.system.id !== 'dnd5e') - return; - if (game.user.isGM && !game.settings.get("torch", "gmUsesInventory")) - return; - if (game.actors.get(actorId) === undefined) - return; - let itemName = game.settings.get("torch", "gmInventoryItemName"); - if (Torch.getLightSourceType(actorId, itemName) !== itemName) - return; - - // Now we can consume it - let torchItem = Array.from(game.actors.get(actorId).items) - .find( (item) => item.name.toLowerCase() === itemName.toLowerCase()); - if (torchItem && torchItem.system.quantity > 0) { - await torchItem.update( - {"system.quantity": torchItem.system.quantity - 1} - ); - } - } - - /* - * Add a torch button to the Token HUD - called from TokenHUD render hook - */ - static async addTorchButton(tokenHUD, hudHtml, hudData) { - - let token = tokenHUD.object.document; - let itemName = game.system.id === 'dnd5e' - ? game.settings.get("torch", "gmInventoryItemName") : ""; - let torchDimRadius = game.settings.get("torch", "dimRadius"); - let torchBrightRadius = game.settings.get("torch", "brightRadius"); - - // Don't let the tokens we create for Dancing Lights have or use their own torches. - if (token.name === 'Dancing Light') { - return; - } - - let lightSource = Torch.getLightSourceType(token.actor.id, itemName); - if (lightSource !== '') { - let tbutton = $(BUTTON_HTML); - let allowEvent = true; - let oldTorch = token.getFlag("torch", "oldValue"); - let newTorch = token.getFlag("torch", "newValue"); - let tokenTooBright = lightSource !== 'Dancing Lights' - && token.light.bright > torchBrightRadius - && token.light.dim > torchDimRadius; - - // Clear torch flags if light has been changed somehow. - let expectedTorch = token.light.bright + '/' + token.light.dim; - if (newTorch !== undefined && newTorch !== null && - newTorch !== 'Dancing Lights' && newTorch !== expectedTorch) { - await token.setFlag("torch", "oldValue", null); - await token.setFlag("torch", "newValue", null); - oldTorch = null; - newTorch = null; - console.warn( - `Torch: Resynchronizing - ${expectedTorch}, ${newTorch}`); - } - - if (newTorch !== undefined && newTorch !== null) { - // If newTorch is still set, light hasn't changed. - tbutton.addClass("active"); - } - else if (lightSource === '0' || tokenTooBright) { - let disabledIcon = $(DISABLED_ICON_HTML); - tbutton.addClass("fa-stack"); - tbutton.find('i').addClass('fa-stack-1x'); - disabledIcon.addClass('fa-stack-1x'); - tbutton.append(disabledIcon); - allowEvent = false; - } - hudHtml.find('.col.left').prepend(tbutton); - if (allowEvent) { - tbutton.find('i').click(async (ev) => { - let buttonElement = $(ev.currentTarget.parentElement); - ev.preventDefault(); - ev.stopPropagation(); - await Torch.clickedTorchButton( - buttonElement, ev.altKey, token, lightSource); - }); - } - } - } - - /* - * Called when the torch button is clicked - */ - static async clickedTorchButton(button, forceOff, token, lightSource) { - debugLog("Torch clicked"); - let torchOnDimRadius = game.settings.get("torch", "dimRadius"); - let torchOnBrightRadius = game.settings.get("torch", "brightRadius"); - let torchOffDimRadius = game.settings.get("torch", "offDimRadius"); - let torchOffBrightRadius = game.settings.get("torch", "offBrightRadius"); - let oldTorch = token.getFlag("torch", "oldValue"); - - if (forceOff) { // Forcing light off... - await token.setFlag("torch", "oldValue", null); - await token.setFlag("torch", "newValue", null); - await Torch.sendRequest( - token.id, {"requestType": "removeDancingLights"}); - button.removeClass("active"); - await token.update({ - "light.bright": torchOffBrightRadius, - "light.dim": torchOffDimRadius - }); - debugLog("Force torch off"); - - // Turning light on... - } else if (oldTorch === null || oldTorch === undefined) { - if (token.light.bright === torchOnBrightRadius - && token.light.dim === torchOnDimRadius - ) { - await token.setFlag( - "torch", "oldValue", - torchOffBrightRadius + '/' + torchOffDimRadius); - console.warn(`Torch: Turning on torch that's already turned on?`); - } else { - await token.setFlag( - "torch", "oldValue", - token.light.bright + '/' + token.light.dim); - } - if (lightSource === 'Dancing Lights') { - if (await Torch.sendRequest( - token.id, {"requestType": "createDancingLights"}) - ) { - await token.setFlag("torch", "newValue", 'Dancing Lights'); - debugLog("Torch dance on"); - button.addClass("active"); - } else { - await token.setFlag("torch", "oldValue", null); - debugLog("Torch dance failed"); - } - } else { - let newBrightLight = - Math.max(torchOnBrightRadius, token.light.bright); - let newDimLight = - Math.max(torchOnDimRadius, token.light.dim); - await token.setFlag( - "torch", "newValue", newBrightLight + '/' + newDimLight); - await token.update({ - "light.bright": newBrightLight, - "light.dim": newDimLight - }); - debugLog("Torch on"); - await Torch.consumeTorch(token.actor.id); - } - // Any token light data update must happen before we call - // consumeTorch(), because the quantity change in consumeTorch() - // triggers the HUD to re-render, which triggers addTorchButton again. - // addTorchButton won't work right unless the change in light from - // the click is already a "done deal". - - } else { // Turning light off... - let oldTorch = token.getFlag("torch", "oldValue"); - let newTorch = token.getFlag("torch", "newValue"); - let success = true; - if (newTorch === 'Dancing Lights') { - success = await Torch.sendRequest( - token.id, {"requestType": "removeDancingLights"}); - if (success) { - debugLog("Torch dance off"); - } else { - debugLog("Torch dance off failed"); - } - } else { - // Something got lost - avoiding getting stuck - if (oldTorch === newTorch) { - await token.update({ - "light.bright": torchOffBrightRadius, - "light.dim": torchOffDimRadius - }); - } else { - let thereBeLight = oldTorch.split('/'); - await token.update({ - "light.bright": parseFloat(thereBeLight[0]), - "light.dim": parseFloat(thereBeLight[1]) - }); - } - debugLog("Torch off"); - } - if (success) { - await token.setFlag("torch", "newValue", null); - await token.setFlag("torch", "oldValue", null); - button.removeClass("active"); - if (lightSource === "0" ){ - await canvas.tokens.hud.render(); - } - } - } - } + /* + * Add a torch button to the Token HUD - called from TokenHUD render hook + */ + static async addTorchButton(hud, hudHtml, hudData) { + let token = new TorchToken(hud.object.document); + let lightSources = token.ownedLightSources; + + // Don't let the tokens we create for light sources have or use their own light sources recursively. + if (hud.object.document.name in lightSources) return; + if (!game.user.isGM && !Settings.playerTorches) return; + if (!token.currentLightSource) return; + + /* Manage torch state */ + TokenHUD.addFlameButton( + token, + hudHtml, + Torch.forceSourceOff, + Torch.toggleLightSource, + Torch.toggleLightHeld, + Torch.changeLightSource + ); + } + + static async toggleLightSource(token) { + let newState = await token.advanceState(); + debugLog(`${token.currentLightSource} is now ${newState}`); + } + + static async forceSourceOff(token) { + await token.forceSourceOff(); + debugLog(`Forced ${token.currentLightSource} off`); + } + + static async toggleLightHeld(token) { + + } + + static async changeLightSource(token, name) { + await token.setCurrentLightSource(name); + } + + static setupQuenchTesting() { + console.log("Torch | --- In test environment - load test code..."); + import("./test/test-hook.js") + .then((obj) => { + try { + obj.hookTests(); + console.log("Torch | --- Tests ready"); + } catch (err) { + console.log("Torch | --- Error registering test code", err); + } + }) + .catch((err) => { + console.log("Torch | --- No test code found", err); + }); + } } -Hooks.on('ready', () => { - Hooks.on('renderTokenHUD', (app, html, data) => { - Torch.addTorchButton(app, html, data) - }); - Hooks.on('renderControlsReference', (app, html, data) => { - let turnOffLights = game.i18n.localize("torch.turnOffAllLights"); - let ctrlOnClick = game.i18n.localize("torch.holdCtrlOnClick"); - html.find('div').first().append( - CTRL_REF_HTML(turnOffLights, ctrlOnClick) - ); - }); - game.socket.on("module.torch", request => { - Torch.handleSocketRequest(request); - }); +Hooks.on("ready", () => { + Hooks.on("renderTokenHUD", (app, html, data) => { + Torch.addTorchButton(app, html, data); + }); + Hooks.on("renderControlsReference", (app, html, data) => { + html.find("div").first().append(Settings.helpText); + }); + game.socket.on("module.torch", (request) => { + TorchSocket.handleSocketRequest(request); + }); }); Hooks.once("init", () => { - // Only load and initialize test suite if we're in a test environment - if (game.world.data.name.startsWith("torch-test-")) { - console.log("Torch | --- In test environment - load test code...") - import('./test/test-hook.js') - .then(obj => { - try { - obj.hookTests(); - console.log("Torch | --- Tests ready"); - } catch (err) { - console.log("Torch | --- Error registering test code", err); - } - }) - .catch(err => { console.log("Torch | --- No test code found", err); }); - } - - game.settings.register("torch", "playerTorches", { - name: game.i18n.localize("torch.playerTorches.name"), - hint: game.i18n.localize("torch.playerTorches.hint"), - scope: "world", - config: true, - default: true, - type: Boolean - }); - if (game.system.id === 'dnd5e') { - game.settings.register("torch", "gmUsesInventory", { - name: game.i18n.localize("torch.gmUsesInventory.name"), - hint: game.i18n.localize("torch.gmUsesInventory.hint"), - scope: "world", - config: true, - default: false, - type: Boolean - }); - game.settings.register("torch", "gmInventoryItemName", { - name: game.i18n.localize("torch.gmInventoryItemName.name"), - hint: game.i18n.localize("torch.gmInventoryItemName.hint"), - scope: "world", - config: true, - default: "torch", - type: String - }); - } - game.settings.register("torch", "brightRadius", { - name: game.i18n.localize("LIGHT.LightBright"), - hint: game.i18n.localize("torch.brightRadius.hint"), - scope: "world", - config: true, - default: 20, - type: Number - }); - game.settings.register("torch", "dimRadius", { - name: game.i18n.localize("LIGHT.LightDim"), - hint: game.i18n.localize("torch.dimRadius.hint"), - scope: "world", - config: true, - default: 40, - type: Number - }); - game.settings.register("torch", "offBrightRadius", { - name: game.i18n.localize("torch.offBrightRadius.name"), - hint: game.i18n.localize("torch.offBrightRadius.hint"), - scope: "world", - config: true, - default: 0, - type: Number - }); - game.settings.register("torch", "offDimRadius", { - name: game.i18n.localize("torch.offDimRadius.name"), - hint: game.i18n.localize("torch.offDimRadius.hint"), - scope: "world", - config: true, - default: 0, - type: Number - }); - game.settings.register("torch", "dancingLightVision", { - name: game.i18n.localize("torch.dancingLightVision.name"), - hint: game.i18n.localize("torch.dancingLightVision.hint"), - scope: "world", - config: true, - default: false, - type: Boolean - }); + // Only load and initialize test suite if we're in a test environment + if (game.world.data.name.startsWith("torch-test-")) { + Torch.setupQuenchTesting(); + } + Settings.register(); }); console.log("Torch | --- Module loaded"); From 230cff93504fb23d7d5411e7b5dc13364d3e5504 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sun, 3 Apr 2022 20:54:36 -0400 Subject: [PATCH 05/26] Added a CSS file - cleaned out style attributes - added changelog blurb --- CHANGELOG.md | 31 ++++++++++++++++++++++++++++++- hud.js | 15 ++++++--------- module.json | 2 +- test/vttlink.sh | 5 +++++ token.js | 12 +++++++----- torch.css | 20 ++++++++++++++++++++ 6 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 torch.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 68be5ee..9fd0552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,36 @@ ## Middle Kingdom - v10 branch ### 2.0.0 - [TBD] - - [BREAKING] (Lupestro) This release supports v10 of Foundry - but only v10 and perhaps beyond. + - [BREAKING] (Lupestro) This release supports v10 of Foundry - but only v10 and hopefully some way well beyond it. + - [FEATURE] (Lupestro) Now supports selection among a variety of light sources on right-click menu from torch button - a long time coming, I know. + * Bullseye Lantern has cone beam on token facing. + * Hooded Lantern toggles on-dim-off on click. + * Candle and Torch consume inventory, indicate when exhausted. + * Limitations: + * Aside from existing Dancing Lights behavior, light sources remain carried by the actor + * One actor may have only one light source active per scene at a time. + * Right now, aside from a generalized actor-token light capability, we only support specific light sources in DnD5e today, but we're poised for growth there. + * No support **_yet_** for setting a light source down and stepping away. + - There are subtleties once these things have identity. + - Can somebody else turn them on? Do they cease to belong to your actor? + - What are the rules for interacting with placed light sources? + - How do you avoid turning off the wrong one? + - Do they remain placed after being turned off? + - Where do you have to be to turn them off? + - There's stuff to work out and I wanted to get this out there first. + - We can use the existing logic supporting Dancing Lights as the basis for a very limited implementation with one active light source per actor per scene, but would that be satisfying? We're getting into "piles" territory. + * We probably won't get too deep into spells beyond the two cantrips we support + - The PHB only lists 7 other spells with explicit light effects + - All the spells except Continual Flame and Daylight have other effects - weapon, damage, etc - that you'd want a more sophisticated module to deliver. + - We could offer the Produce Flame cantrip as a half-sized Light cantrip without its thrown weapon effect, but would that be satisfying? + - Anything that consumes spell slots (including Continual Flame and Daylight) should probably be invoked as a normal spell rather than a light source anyway. + * We might do better to integrate light into other modules dealing with objects than to let this get too sophisticated. + + - [INTERNAL] (Lupestro) Separated concerns into multiple js files and a CSS file + * The additional UI and the more complex state finally made it necessary. + * Separate root, hud, token, settings, light sources, socket comms are much easier to follow. + * This will make planned future work much easier as well. + * The mere thimble of HTML needed is fine sitting in the top of the hud.js for now. ## Intermediate period - master branch diff --git a/hud.js b/hud.js index 9c8d82a..25300f5 100644 --- a/hud.js +++ b/hud.js @@ -1,14 +1,11 @@ const BUTTON_HTML = `
`; -const DISABLED_ICON_HTML = ``; -const SOURCE_MENU = `
`; +const DISABLED_ICON_HTML = ``; +const SOURCE_MENU = `
`; const SOURCE_MENU_ITEM = (img, tooltip) => { - return ``; }; -//style="padding: 4px 2px" -//style="border: 1px solid var(--color-border-light-primary); padding:0" -//style="margin:0" export default class TokenHUD { /* @@ -78,10 +75,10 @@ export default class TokenHUD { for (let source of sources) { let child = $(SOURCE_MENU_ITEM(source.image, source.name)); if (source.name === currentSource) { - child.css("border-color", "tomato"); + child.addClass("active"); } if (token.sourceIsExhausted(source.name)) { - child.css("opacity", "0.3"); + child.addClass("exhausted"); } child.click(async (ev) => { let menu = $(ev.currentTarget.parentElement); diff --git a/module.json b/module.json index 6a04f10..0fbddc2 100644 --- a/module.json +++ b/module.json @@ -38,7 +38,7 @@ ], "esmodules": ["./torch.js"], "socket": true, - "styles": [], + "styles": ["./torch.css"], "packs": [], "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", diff --git a/test/vttlink.sh b/test/vttlink.sh index c58124d..1d4b00d 100755 --- a/test/vttlink.sh +++ b/test/vttlink.sh @@ -7,6 +7,11 @@ clonedir="$(dirname $(cd "$(dirname "$0")"; pwd -P))" targetdir=$(cd "$(dirname "$1"/data/modules/torch/torch.js)"; pwd -P) echo From repo in: $clonedir echo To module in: $targetdir +# Replace module.json with link +rm $targetdir/module.json +ln -s $clonedir/module.json $targetdir/module.json +rm $targetdir/torch.css +ln -s $clonedir/torch.css $targetdir/torch.css # Replace javascript in root directory with link for file in $clonedir/*.js do diff --git a/token.js b/token.js index 745f14e..d7be56d 100644 --- a/token.js +++ b/token.js @@ -39,11 +39,13 @@ export default class TorchToken { let sourceData = itemName ? owned.find((item) => item.name.toLowerCase() === itemName.toLowerCase()) : undefined; - return itemName && !!sourceData - ? sourceData.name - : "Default" in owned - ? "Default" - : undefined; + if (itemName &&!!sourceData) { + return sourceData.name; + } + if (owned.length > 0) { + return owned[0].name; + } + return; } async setCurrentLightSource(value) { await this.token.setFlag("torch", "lightSource", value); diff --git a/torch.css b/torch.css new file mode 100644 index 0000000..6d4f105 --- /dev/null +++ b/torch.css @@ -0,0 +1,20 @@ +.control-icon.torch i.fa-slash { + position: absolute; + color: tomato; +} +.control-icon.torch + .light-source-menu { + padding: 4px 2px; +} +.light-source-menu-item { + border: 1px solid var(--color-border-light-primary); + padding: 0; +} +.light-source-menu-item img { + margin: 0; +} +.light-source-menu-item.active { + border-color: tomato; +} +.light-source-menu-item.exhausted { + opacity: 0.3; +} \ No newline at end of file From b94f972805b882de044491e67bfc31832fa2a7fd Mon Sep 17 00:00:00 2001 From: Lupestro Date: Mon, 4 Apr 2022 17:00:36 -0400 Subject: [PATCH 06/26] Default light source name is now "Self" (for systems not otherwise supported) --- source-specs.js | 4 ++-- token.js | 4 ++-- zipit.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source-specs.js b/source-specs.js index a0ed3ee..7e742ed 100644 --- a/source-specs.js +++ b/source-specs.js @@ -87,8 +87,8 @@ export default class SourceSpecs { } break; default: - sources["Default"] = { - name: "Default", + sources["Self"] = { + name: "Self", light: [ { bright: Settings.litRadii.bright, dim: Settings.litRadii.dim }, ], diff --git a/token.js b/token.js index d7be56d..5975afd 100644 --- a/token.js +++ b/token.js @@ -73,8 +73,8 @@ export default class TorchToken { allSources[item.name] ); }); - } else if ("Default" in allSources) { - return [allSources["Default"]]; + } else if ("Self" in allSources) { + return [allSources["Self"]]; } } diff --git a/zipit.sh b/zipit.sh index 35e6742..9aa5306 100755 --- a/zipit.sh +++ b/zipit.sh @@ -1,4 +1,4 @@ #!/bin/sh rm torch.zip -cd .. && zip -x\*.git\* -r torch/torch.zip torch -x \*.git\* \*.sh \*test\* \*.vscode\* +cd .. && zip -x\*.git\* -r torch/torch.zip torch -x \*.git\* \*.sh \*test\* \*.vscode\* \*design\* From 1b546aa1325a78a6b23775e9ae3147abeaa06a96 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Mon, 4 Apr 2022 17:59:44 -0400 Subject: [PATCH 07/26] Light source menu touch-ups --- token.js | 7 ++++++- torch.css | 14 +++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/token.js b/token.js index 5975afd..bac44dd 100644 --- a/token.js +++ b/token.js @@ -74,7 +74,12 @@ export default class TorchToken { ); }); } else if ("Self" in allSources) { - return [allSources["Self"]]; + return [ + Object.assign( + { image: "/icons/svg/light.svg", quantity: 1 }, + allSources["Self"] + ) + ]; } } diff --git a/torch.css b/torch.css index 6d4f105..060d1a0 100644 --- a/torch.css +++ b/torch.css @@ -3,18 +3,22 @@ color: tomato; } .control-icon.torch + .light-source-menu { - padding: 4px 2px; + padding: 2px 2px 4px; + width: 30px; } -.light-source-menu-item { + +.control-icon.torch + .light-source-menu .light-source-menu-item { border: 1px solid var(--color-border-light-primary); padding: 0; + margin: 0; } -.light-source-menu-item img { + +.control-icon.torch + .light-source-menu button.light-source-menu-item img { margin: 0; } -.light-source-menu-item.active { +.control-icon.torch + .light-source-menu .light-source-menu-item.active { border-color: tomato; } -.light-source-menu-item.exhausted { +.control-icon.torch + .light-source-menu .light-source-menu-item.exhausted { opacity: 0.3; } \ No newline at end of file From 560155fe609494a7703be1dcbfa2d637877eceff Mon Sep 17 00:00:00 2001 From: Lupestro Date: Tue, 5 Apr 2022 08:54:10 -0400 Subject: [PATCH 08/26] Expanding system support - just the easy stuff so far --- source-specs.js | 217 ++++++++++++++++++++++++++++++++++++++++++++---- token.js | 3 + 2 files changed, 204 insertions(+), 16 deletions(-) diff --git a/source-specs.js b/source-specs.js index 7e742ed..5f1bfd5 100644 --- a/source-specs.js +++ b/source-specs.js @@ -62,29 +62,214 @@ const DND5E_LIGHT_SOURCES = { }, }; +const SWADE_LIGHT_SOURCES = { + "Candle": { + name: "Candle", + light: [{bright: 0, dim: 2}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + "Flashlight": { + name: "Flashlight", + light: [{bright: 10, dim: 10}], + shape: "beam", + type: "equipment", + consumable: false, + states: 2 + }, + "Lantern": { + name: "Lantern", + light: [{bright: 4, dim: 4}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 2, + }, + "Torch": { + name: "Torch", + light: [{bright: 4, dim: 4}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + } +}; + +const ED4E_LIGHT_SOURCES = { + "Candle": { + name: "Candle", + light: [{bright: 0, dim: 3}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + "Lantern (Hooded)": { + name: "Lantern (Hooded)", + light: [{bright: 10, dim: 10}, {bright: 0, dim: 10}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 3 + }, + "Lantern (Bullseye)": { + name: "Lantern (Bullseye)", + light: [{bright: 20, dim: 20}], + shape: "beam", + type: "equipment", + consumable: false, + states: 2, + }, + "Torch": { + name: "Torch", + light: [{bright: 10, dim: 10}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + } +}; + +const PF2E_LIGHT_SOURCES = { + "Candle": { + name: "Candle", + light: [{bright: 0, dim: 10}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + "Lantern (Hooded)": { + name: "Lantern (Hooded)", + light: [{bright: 30, dim: 60}, {bright: 0, dim: 5}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 3 + }, + "Lantern (Bull's Eye)": { + name: "Lantern (Bull's Eye)", + light: [{bright: 60, dim: 120}], + shape: "cone", + type: "equipment", + consumable: false, + states: 2, + }, + "Torch": { + name: "Torch", + light: [{bright: 20, dim: 40}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + } +}; + +const PF1_LIGHT_SOURCES = { + "Candle": { + name: "Candle", + light: [{bright: 0, dim: 5}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + }, + "Lamp": { + name: "Lamp", + light: [{bright: 15, dim: 30}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 2 + }, + "Lantern": { + name: "Lantern", + light: [{bright: 30, dim: 60}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 2, + }, + "Bullseye Lantern": { + name: "Bullseye Lantern", + light: [{bright: 60, dim: 120}], + shape: "cone", + type: "equipment", + consumable: false, + states: 2, + }, + "Hooded Lantern": { + name: "Hooded Lantern", + light: [{bright: 30, dim: 60}], + shape: "sphere", + type: "equipment", + consumable: false, + states: 2, + }, + "Miner's Lantern": { + name: "Miner's Lantern", + light: [{bright: 30, dim: 60}], + shape: "cone", + type: "equipment", + consumable: false, + states: 2, + }, + "Torch": { + name: "Torch", + light: [{bright: 20, dim: 40}], + shape: "sphere", + type: "equipment", + consumable: true, + states: 2, + } +}; export default class SourceSpecs { + static augmentSources(sourceSet) { + let sources = Object.assign({}, sourceSet); + let inventoryItem = Settings.inventoryItemName; + if (!inventoryItem) { + return sources; + } else { + if (!SourceSpecs.find(inventoryItem, sources)) { + sources[inventoryItem] = { + name: inventoryItem, + light: [ + { + bright: Settings.litRadii.bright, + dim: Settings.litRadii.dim, + }, + ], + shape: "sphere", + type: "none", + consumable: false, + states: 2, + }; + } + } + return sources; + } + static get lightSources() { let itemName = Settings.inventoryItemName; let sources = {}; switch (game.system.id) { case "dnd5e": - sources = Object.assign({}, DND5E_LIGHT_SOURCES); - if (!SourceSpecs.find(itemName, sources)) { - sources[itemName] = { - name: itemName, - light: [ - { - bright: Settings.litRadii.bright, - dim: Settings.litRadii.dim, - }, - ], - shape: "sphere", - type: "none", - consumable: false, - states: 2, - }; - } + sources = SourceSpecs.augmentSources(DND5E_LIGHT_SOURCES); + break; + case "swade": + sources = SourceSpecs.augmentSources(SWADE_LIGHT_SOURCES); + break; + case "earthdawn4e": + sources = SourceSpecs.augmentSources(ED4E_LIGHT_SOURCES); + break; + case "pf2e": + sources = SourceSpecs.augmentSources(PF2E_LIGHT_SOURCES); + break; + case "pf1": + sources = SourceSpecs.augmentSources(PF1_LIGHT_SOURCES); break; default: sources["Self"] = { diff --git a/token.js b/token.js index bac44dd..a0e5618 100644 --- a/token.js +++ b/token.js @@ -14,12 +14,15 @@ let getAngle = (shape) => { switch (shape) { case "cone": return 53.13; + case "beam": + return 3; case "sphere": default: return 360; } }; +// GURPS.recurselist(game.actors.get(this.token.actorId).system.equipment.carried,(item) => { console.log("Name: ", item.name, ", Count: ",item.count); }); export default class TorchToken { STATE_ON = "on"; STATE_DIM = "dim"; From 07f81f6282969090c28b019e61d9aca799c7f38f Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 9 Apr 2022 10:30:45 -0400 Subject: [PATCH 09/26] Added design for extension for more systems --- design/more-systems.md | 145 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 design/more-systems.md diff --git a/design/more-systems.md b/design/more-systems.md new file mode 100644 index 0000000..158f851 --- /dev/null +++ b/design/more-systems.md @@ -0,0 +1,145 @@ +# Feature - Adding support for more systems + +Currently, Torch only explicitly supports light sources from D&D 5e, with a "self-only" lighting option for other systems configurable in the settings. + +With the enhancement to the selection of sources, Torch supports all the common light sources for D&D 5e. We would like extend this to other systems. Recall that, for D&D 5e, we started with the following tables: + +| Item | Bright | Dim | Shape | Consumable | States | +|------|--------|-----| ----- | ----- | -------| +| Candle | 5 | 10 | round | item | unlit, lit | +| Bullseye Lantern | 60 | 120 | cone | oil | unlit, lit | +| Hooded Lantern | 30 (0) | 60 (5) | round | oil | unlit, open, closed | +| Lamp | 15 | 45 | round | oil | unlit, lit | +| Torch | 20 | 40 | round | item | unlit, lit | + +| Spell | Bright | Dim | Shape | range | level | other effects +|------|--------|-----| ----- | --------| ---| ----- | +Continual Flame | 20 | 40 | sphere | touch | 2 | none +Dancing Lights | 0 | 10 | sphere | 120 ft | cantrip | none +Daylight | 60 | 120 | sphere | 60 ft | 3 | none +Fire Shield | 10 | 20 | sphere | self | 4 | flame damage +Flame Blade | 10 | 20 | sphere | self | 2 | melee weapon +Light | 20 | 40 | sphere | touch | cantrip | none +Moonbeam | 0 | 5 | cylinder | 120 ft | 2 | radiant damage +Produce Flame | 10 | 20 | sphere | self | cantrip | thrown weapon +Sunbeam | 30 | 60 | cylinder | self | 6 | radiant damage + +Eventually, the additional capabilities and requirements of spells being beyond the scope of Torch, we chose to support the five standard pieces of lighting equipment, plus the Dancing Lights and Light cantrips. + +The above format of table will prove useful in characterizing light sources in other systems as well. + +To better support other systems: + +* We would like to extend support as installed to "core book" lighting equipment in as many systems as possible. +* We would like to make the Torch module extensible to let GMs supply additional light sources for the game. + +In experimentation, we have found that several, but not all, game systems deliver their equipment as embedded items in the actor, in precisely the same manner as D&D 5e. For instance, the name is in `item.name` and how many the user has is in `item.system.quantity`. For these systems, which include Savage Worlds, and both editions of Pathfinder, support can be as simple as supplying a table like the ones above for the sources in the system. + +However, not all systems handle equipment in the same way. For instance, Earthdawn 4e uses "amount" rather than "quantity". This is a relatively simple case. GURPS doesn't use items for its equipment at all. It models a hierarchy of containers, attached as actor system data, and provides a function to walk it: +```javascript +GURPS.recurselist(game.actors.get(this.token.actorId).system.equipment.carried,(item) => { + console.log("Name: ", item.name, ", Count: ",item.count); +}); +``` + +Therefore, a change to support more systems will need to abstract navigation of equipment and the collection and setting of quantity based on the system you are using. + +Once we are sure we can use tables to supply light sources for each game, and set those into place, the second issue for this design is how to let the GM supply a batch of light sources. I am inclined to support json or yaml input for the tabular portion. We can add an item to the data structure identifying the item ruleset to apply for a particular system. Initial rulesets might be "std","ed4e","gurps". + +The remaining sections of this document will sketch out the details of how this might work. + +## The light source table + +The light source table drives the whole mechanism. It declaratively defines what light sources Torch will supply to the game. Since we will be supporting "free" cantrips but not spells that consume slots, we don't need to be concerned about spell levels. +```typescript +interface LightSourceTable { + system: string; // The name of the system for which the table applies + topology: 'standard' | 'GURPS'; + quantityField: string; // The name of the field holding the inventory of the light source + sources: LightSource[]; // The light sources in the table +} +interface LightSource { + name: string; // Light source name - preferably in the case used in the game system. + type: 'spell' | 'equipment'; // Gives a hint where to look for the light source. + consumable: boolean; // Whether inventory is consumed for every use. + states: number; // The number of states involved - one unlit state and one or more lit states. + light: LightData[]; // A Foundry LightData object for each lit state in order. +} +``` +The topology field tells Torch which set of rules to apply to find and use light sources within the actor. +* The **standard** topology uses owned Item objects for both equipment and spells, with the quantity for equipment in a field within the Item's `system` data. +* The **GURPS** topology uses the Actor's `system` data for equipment with a hierarchy of containers and special functions to walk them. +* Other topologies may apply for other systems. + +Because we are using LightData objects, we can specify anything about the light source that the token can carry. States cycle the token through the light specs in order. + +## System light source topology +Because we are already using this structure with standard topology for D&D5e light sources, the only additional structure needed to support additional systems in the code is the isolation of the few functions that are sensitive to the topology of equipment and spells in the system. These functions form the Topology interface, implemented by a class for each topology. When the Light Source data is loaded, the runtime structure can carry the topology instance as a property. + +```typescript +interface SystemTopology { + hasEquipment(name: string): boolean; + hasCantrip(name: string): boolean; + getInventory(name:string) : number; + await decrementInventory(name:string) : number; +} +class StandardTopology implements SystemTopology { + constructor(itemName: string); + hasEquipment(name: string): boolean; + hasCantrip(name: string): boolean; + getInventory(name:string) : number; + await decrementInventory(name:string) : number; + +} +class GURPSTopology implements SystemTopology { + constructor(itemName: string); + hasEquipment(name: string): boolean; + hasCantrip(name: string): boolean; + getInventory(name:string) : number; + await decrementInventory(name:string) : number; +} +``` +## Settings and extensibility + +Since the light source table is just data, users should be able to extend Torch's reach without having to code anything if we provide a way via settings for GMs to supply supplementary tables that override the behavior of light sources already defined or define new light sources. These may be light sources from game system supplements, they may support a new system, or they may be light sources the GM has defined. + +### Effect of changes on existing settings + +Today, we support the GM setting up bright/dim levels for torch on and torch off configurations. These levels will be used as a light source wherever other light sources aren't defined, which today means anywhere but D&D 5e. + +* Some systems, like those based on FATE or PBTA or 2d6 (like City of Mists), simply aren't built around lists of equipment. If you would reasonably have a flashlight, you do. Special abilities like spells are based on character traits and storytelling. +* For other systems, we have had neither light sources set up by default nor the ability to extend them. + +This capability still needs to be supported. In order to support the full range of light properties, though, the simple bright/dim settings should be overridable through light source data. Conceptually, there is a table for the "default" system, with one predefined, non-consumable light source named "Self". Its light values come from the bright/dim settings. An extension table can change the properties of "Self" and add other default light sources. + +The `Player Torches` setting lets the GM control whether players can turn their light sources on and off. I see no reason to remove this setting. Some GMs may want to use the absence of light to control what users can see and have concerns about users trying to out-maneuver them. It's not my style of game-play, but "not my circus, not my monkeys." I'll let it stand. + +Today, there are two settings specific to D&D 5e: +* `GM Uses Inventory` - whether the GM depletes inventory when using a light source +* `GM Inventory Item Name` - the name of the light source to use. The bright and dim levels in the settings will be used for this light source, rather than the settings from the game system, and the inventory of the named source will be affected by use. + +Since we will now be extending our item support beyond D&D: +* `GM Uses Inventory` needs to no longer be D&D specific, but appear regardless of system and apply wherever we have an item table that enables us to use inventory. +* We should also have a setting `Player Uses inventory` so that GMs can set up a game where nobody has to fiddle with the quantities. This is a requested feature. [[Issue #11](https://github.com/League-of-Foundry-Developers/torch/issues/11)] +* The `GM Inventory Item Name*` setting was created before we allowed user light source choices, and it will always be awkward. The ability to extend with a table will subsume the need for this. We should remove it. +* The `Dancing Lights Vision` setting is *not* marked as dnd5e-specific today, but that is clearly an oversight, as it refers to a specific D&D 5e cantrip. It should become the *only* DND5e specific setting in the updated module. + +This covers what needs to change for the existing settings. + +### New settings + +**Light Source Configuration File** + +Settings are inherently per-game. It seems reasonable to have a setting that specifies the location of a file that contains all the extended light sources for the game. Since any one game is only ever in one system, the file will contain a single `LightSourceTable` object, as described above, in JSON or YAML format. + +This will be a single file selection setting. I don't think we need to offer the user a way to specify more than one such file. This allows a lot of extensibility while keeping the settings page relatively well contained. + +We will supply clear help on the structure of the file in the module's `README.md`. + +### Extensibility needing code + +This will allow nearly all of the extensibility needed by the module without adding code. Where there is a game system with a topology we do not yet support, the additional topology class would need to be written and the topology name added to our list of recognized topologies. Happily, when we support topologies in the manner stated above, this should be a very small and isolated code change. + +Users can request the addition of a system topology in an issue, perhaps with a macro to demonstrate finding all the equipment on their system, or (better yet) offer an implementation in a PR. All of the topologies and their discriminator will be supported together in the topology.js module, so they would just be adding to that. + +We can describe briefly in the `README.md` what users need to do to support such a system as well. From fd219c07735864987eb613c6c578083886162d44 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 16 Apr 2022 19:15:38 -0400 Subject: [PATCH 10/26] Now supports configurable sources in disparate systems. --- design/more-systems.md | 15 ++ hud.js | 10 +- lang/cn.json | 14 +- lang/en.json | 14 +- lang/es.json | 10 +- lang/fr.json | 12 +- lang/pt-BR.json | 10 +- lang/zh-TW.json | 14 +- library.js | 161 +++++++++++++++++++ sources.js => request.js | 13 +- settings.js | 101 ++++++------ socket.js | 14 +- source-specs.js | 297 ----------------------------------- sources.json | 290 ++++++++++++++++++++++++++++++++++ test/common-library-tests.js | 143 +++++++++++++++++ test/common-token-tests.js | 61 +++++++ test/test-hook.js | 10 +- test/userLights.json | 46 ++++++ test/vttlink.sh | 10 ++ token.js | 217 ++++++++++--------------- topology.js | 120 ++++++++++++++ torch.js | 13 +- 22 files changed, 1063 insertions(+), 532 deletions(-) create mode 100644 library.js rename sources.js => request.js (85%) delete mode 100644 source-specs.js create mode 100644 sources.json create mode 100644 test/common-library-tests.js create mode 100644 test/common-token-tests.js create mode 100644 test/userLights.json create mode 100644 topology.js diff --git a/design/more-systems.md b/design/more-systems.md index 158f851..81a134f 100644 --- a/design/more-systems.md +++ b/design/more-systems.md @@ -143,3 +143,18 @@ This will allow nearly all of the extensibility needed by the module without add Users can request the addition of a system topology in an issue, perhaps with a macro to demonstrate finding all the equipment on their system, or (better yet) offer an implementation in a PR. All of the topologies and their discriminator will be supported together in the topology.js module, so they would just be adding to that. We can describe briefly in the `README.md` what users need to do to support such a system as well. + +# Testing + +For these changes, we will want to do more automated testing. For the new V10 work, we broke the work that used to be combined in a single module into several, each with a single purpose. This opens up an opportunity for testing their behavior individually. + +* The `token` module can be tested independently of the HUD by setting flags, advancing through states, and measuring the token behavior. +* The `taxonomy` module can be tested independently by observing its effect on consuming resources in various systems. +* The `library` module can be tested independently by providing it with JSON files and verifying it delivers the right overlays of data. +* The `socket` and `request` modules can be tested by invoking them to perform actions via a GM. +* Admittedly, the best way to test `settings` is to actually run the settings in Foundry and measure that the right things get set and retrieved. +* The `torch` and `hud` modules, unfortunately, cannot be tested entirely independently, as they are too tied into everything. + +So we've got a lot we can test before we start trying to run the whole gamut of capabilities via the settings and the HUD. + + diff --git a/hud.js b/hud.js index 25300f5..88775c4 100644 --- a/hud.js +++ b/hud.js @@ -20,7 +20,7 @@ export default class TokenHUD { changeLightSource ) { let state = token.lightSourceState; - let disabled = token.currentLightSourceIsExhausted; + let disabled = token.lightSourceIsExhausted(token.currentLightSource); let allowEvent = !disabled; let tbutton = $(BUTTON_HTML); if (state === token.STATE_ON) { @@ -77,9 +77,9 @@ export default class TokenHUD { if (source.name === currentSource) { child.addClass("active"); } - if (token.sourceIsExhausted(source.name)) { - child.addClass("exhausted"); - } + if (token.lightSourceIsExhausted(source.name)) { + child.addClass("exhausted"); + } child.click(async (ev) => { let menu = $(ev.currentTarget.parentElement); await changeLightSource(token, source.name); @@ -94,7 +94,7 @@ export default class TokenHUD { static syncDisabledState(tbutton, token) { let oldSlash = tbutton.find(".fa-slash"); let wasDisabled = oldSlash.length > 0; - let willBeDisabled = token.currentLightSourceIsExhausted; + let willBeDisabled = token.lightSourceIsExhausted(token.currentLightSource); if (!wasDisabled && willBeDisabled) { let disabledIcon = $(DISABLED_ICON_HTML); tbutton.addClass("fa-stack"); diff --git a/lang/cn.json b/lang/cn.json index 3288f55..120a8f0 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -7,10 +7,14 @@ "torch.dimRadius.hint": "发出多少格单位的微光。", "torch.gmUsesInventory.name": "使用库存", "torch.gmUsesInventory.hint": "当GM开关火炬时,将使用角色库存。", - "torch.offBrightRadius.name": "关闭时发出亮光", - "torch.offBrightRadius.hint": "关闭时发出多少格单位的亮光。", - "torch.offDimRadius.name": "关闭时发出微光。", - "torch.offDimRadius.hint": "关闭时发出多少格单位的微光。", + "torch.gmInventoryItemName.name": "Inventory item name to use", + "torch.gmInventoryItemName.hint": "Name of the inventory item to use if \"GM Uses Inventory\" is set.", "torch.turnOffAllLights": "强制亮/微光设置为OFF", - "torch.holdCtrlOnClick": "按住 Ctrl 点击" + "torch.holdCtrlOnClick": "按住 Ctrl 点击", + "torch.dancingLightVision.name": "Give Dancing Lights Vision.", + "torch.dancingLightVision.hint": "When enabled, each dancing light has vision so can be used to reveal areas of the map players can't actually see.", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" } diff --git a/lang/en.json b/lang/en.json index f5efe10..56642b6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,5 +1,6 @@ { - "I18N.MAINTAINERS": ["Deuce"], + "I18N.LANGUAGE": "English", + "I18N.MAINTAINERS": ["Lupestro"], "torch.playerTorches.name": "Player Torches", "torch.playerTorches.hint": "Allow players to toggle their own torches.", @@ -9,12 +10,13 @@ "torch.gmUsesInventory.hint": "If set, when the GM toggles a torch, it will use the actors inventory.", "torch.gmInventoryItemName.name": "Inventory item name to use", "torch.gmInventoryItemName.hint": "Name of the inventory item to use if \"GM Uses Inventory\" is set.", - "torch.offBrightRadius.name": "Emit bright when off", - "torch.offBrightRadius.hint": "How many grid units of bright light to emit when light is toggled off.", - "torch.offDimRadius.name": "Emit dim when off.", - "torch.offDimRadius.hint": "How many grid units of dim light to emit when light is toggled off.", "torch.turnOffAllLights": "Force bright and dim to configured \"off\" values.", "torch.holdCtrlOnClick": "Hold Ctrl on Click", "torch.dancingLightVision.name": "Give Dancing Lights Vision.", - "torch.dancingLightVision.hint": "When enabled, each dancing light has vision so can be used to reveal areas of the map players can't actually see." + "torch.dancingLightVision.hint": "When enabled, each dancing light has vision so can be used to reveal areas of the map players can't actually see.", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + } diff --git a/lang/es.json b/lang/es.json index b7658f6..7df46dc 100644 --- a/lang/es.json +++ b/lang/es.json @@ -10,12 +10,12 @@ "torch.gmUsesInventory.hint": "Si se activa, cuando el GM activa una antorcha, se usará el inventario del actor seleccionado", "torch.gmInventoryItemName.name": "Nombre del objeto en el inventario", "torch.gmInventoryItemName.hint": "Nombre del objeto en el inventario cuando se active \"GM Usa el Inventario\"", - "torch.offBrightRadius.name": "Emisión brillante en apagado", - "torch.offBrightRadius.hint": "Número de unidades en la rejilla en las que se emite luz brillante cuando está apagada", - "torch.offDimRadius.name": "Emisión tenue en apagado", - "torch.offDimRadius.hint": "Número de unidades en la rejilla en las que se emite luz tenue cuando está apagada", "torch.turnOffAllLights": "Forzar a valores \"off\" tanto en la emisión brillante como en la tenue", "torch.holdCtrlOnClick": "Mantenga pulsado CTRL al hacer clic", "torch.dancingLightVision.name": "Dar visión a las luces danzantes", - "torch.dancingLightVision.hint": "Cuando esté habilitado, cada luz danzante tendrá visión, de modo que se pueda usar para revelar áreas del mapa que los jugadores no puedan ver" + "torch.dancingLightVision.hint": "Cuando esté habilitado, cada luz danzante tendrá visión, de modo que se pueda usar para revelar áreas del mapa que los jugadores no puedan ver", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" } diff --git a/lang/fr.json b/lang/fr.json index 0e7fd7e..e743562 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -10,10 +10,12 @@ "torch.gmUsesInventory.hint": "Si actif, lorsque une torche est allumée, elle sera déduite de l'inventaire du joueur cible.", "torch.gmInventoryItemName.name": "Nom de l'objet à déduire de l'inventaire", "torch.gmInventoryItemName.hint": "Préciser le nom de l'objet à déduire (si l'option est cochée) de l'inventaire des joueurs.", - "torch.offBrightRadius.name": "Lumière faible (torche éteinte)", - "torch.offBrightRadius.hint": "Détermine l'étendue (en unité de grille) de la lumière faible émise (torche éteinte).", - "torch.offDimRadius.name": "Lumière vive (torche éteinte)", - "torch.offDimRadius.hint": "Détermine l'étendue (en unité de grille) de la lumière vive émise (torche éteinte).", "torch.turnOffAllLights": "Éteindre l'ensemble des lumières (vives et faibles).", - "torch.holdCtrlOnClick": "Maintenir Ctrl lors du clic." + "torch.holdCtrlOnClick": "Maintenir Ctrl lors du clic.", + "torch.dancingLightVision.name": "Give Dancing Lights Vision.", + "torch.dancingLightVision.hint": "When enabled, each dancing light has vision so can be used to reveal areas of the map players can't actually see.", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" } diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 2bba578..9fb4b8f 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -9,12 +9,12 @@ "torch.gmUsesInventory.hint": "se ligado, quando o GM acende uma tocha, isso usará o inventário do jogador.", "torch.gmInventoryItemName.name": "Nome do item no inventário a ser usado", "torch.gmInventoryItemName.hint": "Nome do item do iventário a ser usado se o \"MJ Usa o Inventário\" estiver ligado.", - "torch.offBrightRadius.name": "Emissão de luz plena quando desligado", - "torch.offBrightRadius.hint": "Quantas unidades de luz plena é emitida quando a luz for desligada.", - "torch.offDimRadius.name": "Emissão de luz reduzida quando desligado", - "torch.offDimRadius.hint": "Quantas unidades de luz reduzida é emitida quando a luz for desligada.", "torch.turnOffAllLights": "Força luz plena e reduzida aos valores configurados em \"desligada\".", "torch.holdCtrlOnClick": "Segure Ctrl ao Clicar", "torch.dancingLightVision.name": "Dar Visão de Luz Dançantes.", - "torch.dancingLightVision.hint": "Quando ativado, cada luz dançante que tiver visão pode ser usado para revelar areas do mapa que os jogadores não podem ver realmente." + "torch.dancingLightVision.hint": "Quando ativado, cada luz dançante que tiver visão pode ser usado para revelar areas do mapa que os jogadores não podem ver realmente.", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" } diff --git a/lang/zh-TW.json b/lang/zh-TW.json index 144668e..e4be284 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -7,14 +7,14 @@ "torch.dimRadius.hint": "發出多少格單位的微光。", "torch.gmUsesInventory.name": "使用庫存", "torch.gmUsesInventory.hint": "當GM開關火炬時,將使用角色庫存。", - "torch.offBrightRadius.name": "關閉時發出亮光", - "torch.offBrightRadius.hint": "關閉時發出多少格單位的亮光。", - "torch.offDimRadius.name": "關閉時發出微光。", - "torch.offDimRadius.hint": "關閉時發出多少格單位的微光。", - "torch.turnOffAllLights": "強制亮/微光設置為OFF", - "torch.holdCtrlOnClick": "按住 Ctrl 點擊", "torch.gmInventoryItemName.name": "使用庫存物品的名稱", "torch.gmInventoryItemName.hint": "如果啓用了\"GM使用庫存\",則使用庫存物品的名稱。", + "torch.turnOffAllLights": "強制亮/微光設置為OFF", + "torch.holdCtrlOnClick": "按住 Ctrl 點擊", "torch.dancingLightVision.name": "賦予舞動燈火視野。", - "torch.dancingLightVision.hint": "啓用後,每個舞動燈火都擁有視野,因此可用來顯示玩家實際上無法看到的地圖區域。" + "torch.dancingLightVision.hint": "啓用後,每個舞動燈火都擁有視野,因此可用來顯示玩家實際上無法看到的地圖區域。", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" } diff --git a/library.js b/library.js new file mode 100644 index 0000000..9a08de9 --- /dev/null +++ b/library.js @@ -0,0 +1,161 @@ +import getTopology from './topology.js'; + +/* Library of light sources to use for this system */ + +export default class SourceLibrary { + static commonLibrary; + library; + constructor(library) { // Only invoke through static factory method load() + this.library = library; + } + + static async load(systemId, selfBright, selfDim, selfItem, userLibrary) { + // The common library is cached - to update it, you must reload the game. + if (!SourceLibrary.commonLibrary) { + SourceLibrary.commonLibrary = await fetch('/modules/torch/sources.json') + .then( response => { return response.json(); }); + } + // The user library reloads every time you open the HUD to permit cut and try. + let mergedLibrary = userLibrary ? await fetch(userLibrary) + .then( response => { return response.json(); }) + .then( userData => { return mergeLibraries (userData, SourceLibrary.commonLibrary); }) + .catch(reason => { + console.warn("Failed loading user library: ", reason); + }) : mergeLibraries ({}, SourceLibrary.commonLibrary); + // All local changes here take place against the merged data, which is a copy, + // not against the common or user libraries. + if (mergedLibrary[systemId]) { + mergedLibrary[systemId].topology = getTopology( + mergedLibrary[systemId].topology, + mergedLibrary[systemId].quantity); + let library = new SourceLibrary(mergedLibrary[systemId]); + let targetLightSource = library.getLightSource(selfItem); + if (targetLightSource) { + targetLightSource.states = 2; + targetLightSource.light = [{ + bright: selfBright, + dim: selfDim, + angle: 360 + }]; + } + return library; + } else { + mergedLibrary["Default"].topology = getTopology( + mergedLibrary["Default"].topology, + mergedLibrary["Default"].quantity); + + let defaultLibrary = mergedLibrary["default"]; + defaultLibrary["Self"].light.bright = selfBright; + defaultLibrary["Self"].light.dim = selfDim; + defaultLibrary["Self"].image = defaultLibrary.topology.getImage(defaultLibrary["Self"]); + return new SourceLibrary(defaultLibrary); + } + } + + /* Instance methods */ + get lightSources() { + return this.library.sources; + } + getLightSource(name) { + if (name) { + for (let sourceName in this.library.sources) { + if (sourceName.toLowerCase() === name.toLowerCase()) { + return this.library.sources[sourceName]; + } + } + } + return; + } + getInventory(actorId, lightSourceName) { + let source = this.getLightSource(lightSourceName); + return this.library.topology.getInventory(actorId, source); + } + async _presetInventory(actorId, lightSourceName, quantity) { // For testing + let source = this.getLightSource(lightSourceName); + return this.library.topology.setInventory(actorId, source, quantity); + } + async decrementInventory(actorId, lightSourceName) { + let source = this.getLightSource(lightSourceName); + return this.library.topology.decrementInventory(actorId, source); + } + getImage(actorId, lightSourceName) { + let source = this.getLightSource(lightSourceName); + return this.library.topology.getImage(actorId, source); + } + actorHasLightSource(actorId, lightSourceName) { + let source = this.getLightSource(lightSourceName); + return this.library.topology.actorHasLightSource(actorId, source); + } + actorLightSources(actorId) { + let result = [] + for (let source in this.library.sources) { + if (this.library.topology.actorHasLightSource(actorId, this.library.sources[source])) { + let actorSource = Object.assign({ + image: this.library.topology.getImage(actorId, this.library.sources[source]) + }, this.library.sources[source]); + result.push(actorSource); + } + } + return result; + } +} + +/* + * Create a merged copy of two libraries. + */ +let mergeLibraries = function (userLibrary, commonLibrary) { + let mergedLibrary = {} + + // Merge systems - system properties come from common library unless the system only exists in user library + for (let system in commonLibrary) { + mergedLibrary[system] = { + system: commonLibrary[system].system, + topology: commonLibrary[system].topology, + quantity: commonLibrary[system].quantity, + sources: {} + } + } + if (userLibrary) { + for (let system in userLibrary) { + if (!(system in commonLibrary)) { + mergedLibrary[system] = { + system: userLibrary[system].system, + topology: userLibrary[system].topology, + quantity: userLibrary[system].quantity, + sources: {} + } + } + } + } + + // Merge sources - source properties in user library override properties in common library + for (let system in mergedLibrary) { + if (userLibrary && (system in userLibrary)) { + for (let source in userLibrary[system].sources) { + let userSource = userLibrary[system].sources[source]; + mergedLibrary[system].sources[source] = { + "name": userSource["name"], + "type": userSource["type"], + "consumable": userSource["consumable"], + "states": userSource["states"], + "light": Object.assign({}, userSource["light"] ) + }; + } + } + if (system in commonLibrary) { + for (let source in commonLibrary[system].sources) { + if (!userLibrary || !(system in userLibrary) || !(source in userLibrary[system].sources)) { + let commonSource = commonLibrary[system].sources[source]; + mergedLibrary[system].sources[source] = { + "name": commonSource["name"], + "type": commonSource["type"], + "consumable": commonSource["consumable"], + "states": commonSource["states"], + "light": Object.assign({}, commonSource["light"] ) + } + } + } + } + } + return mergedLibrary; +}; diff --git a/sources.js b/request.js similarity index 85% rename from sources.js rename to request.js index d863d67..b48a323 100644 --- a/sources.js +++ b/request.js @@ -1,3 +1,6 @@ +// Note: requests.js operates statically purely at the Foundry level, so scene and +// token are Foundry objects. Token is *not* Torch's light source token object. + const NEEDED_PERMISSIONS = { // Don't want to do yourself something you can't undo without a GM - // so check for delete on create @@ -5,10 +8,10 @@ const NEEDED_PERMISSIONS = { "delete:Dancing Lights": ["TOKEN_DELETE"], }; -export default class LightSource { +export default class TorchRequest { static ACTIONS = { - "create:Dancing Lights": LightSource.createDancingLights, - "delete:Dancing Lights": LightSource.removeDancingLights, + "create:Dancing Lights": TorchRequest.createDancingLights, + "delete:Dancing Lights": TorchRequest.removeDancingLights, }; static isPermitted(user, requestType) { if (requestType in NEEDED_PERMISSIONS) { @@ -21,11 +24,11 @@ export default class LightSource { } static supports(requestType) { - return requestType in LightSource.ACTIONS; + return requestType in TorchRequest.ACTIONS; } static perform(requestType, scene, token) { - LightSource.ACTIONS[requestType](scene, token); + TorchRequest.ACTIONS[requestType](scene, token); } // Dancing lights diff --git a/settings.js b/settings.js index a0cf76b..5d51b88 100644 --- a/settings.js +++ b/settings.js @@ -25,22 +25,24 @@ export default class Settings { ? game.settings.get("torch", "gmInventoryItemName") : undefined; } - static get litRadii() { + static get lightRadii() { return { bright: game.settings.get("torch", "brightRadius"), dim: game.settings.get("torch", "dimRadius"), }; } - static get offRadii() { - return { - bright: game.settings.get("torch", "offBrightRadius"), - dim: game.settings.get("torch", "offDimRadius"), - }; - } static get dancingLightsVision() { return game.settings.get("torch", "dancingLightVision"); } + static get userUsesInventory() { + return game.settings.get("torch", "playerUsesInventory"); + } + + static get gameLightSources() { + return game.settings.get("torch", "gameLightSources"); + } + static get helpText() { let turnOffLights = game.i18n.localize("torch.turnOffAllLights"); let ctrlOnClick = game.i18n.localize("torch.holdCtrlOnClick"); @@ -56,63 +58,64 @@ export default class Settings { default: true, type: Boolean, }); - if (game.system.id === "dnd5e") { - game.settings.register("torch", "gmUsesInventory", { - name: game.i18n.localize("torch.gmUsesInventory.name"), - hint: game.i18n.localize("torch.gmUsesInventory.hint"), - scope: "world", - config: true, - default: false, - type: Boolean, - }); - game.settings.register("torch", "gmInventoryItemName", { - name: game.i18n.localize("torch.gmInventoryItemName.name"), - hint: game.i18n.localize("torch.gmInventoryItemName.hint"), - scope: "world", - config: true, - default: "torch", - type: String, - }); - } - game.settings.register("torch", "brightRadius", { - name: game.i18n.localize("LIGHT.LightBright"), - hint: game.i18n.localize("torch.brightRadius.hint"), + game.settings.register("torch", "gmUsesInventory", { + name: game.i18n.localize("torch.gmUsesInventory.name"), + hint: game.i18n.localize("torch.gmUsesInventory.hint"), scope: "world", config: true, - default: 20, - type: Number, + default: false, + type: Boolean, }); - game.settings.register("torch", "dimRadius", { - name: game.i18n.localize("LIGHT.LightDim"), - hint: game.i18n.localize("torch.dimRadius.hint"), + game.settings.register("torch", "playerUsesInventory", { + name: game.i18n.localize("torch.playerUsesInventory.name"), + hint: game.i18n.localize("torch.playerUsesInventory.hint"), scope: "world", config: true, - default: 40, - type: Number, + default: true, + type: Boolean, }); - game.settings.register("torch", "offBrightRadius", { - name: game.i18n.localize("torch.offBrightRadius.name"), - hint: game.i18n.localize("torch.offBrightRadius.hint"), + game.settings.register("torch", "gmInventoryItemName", { + name: game.i18n.localize("torch.gmInventoryItemName.name"), + hint: game.i18n.localize("torch.gmInventoryItemName.hint"), scope: "world", config: true, - default: 0, - type: Number, + default: "torch", + type: String, + }); + game.settings.register("torch", "gameLightSources", { + name: game.i18n.localize("torch.gameLightSources.name"), + hint: game.i18n.localize("torch.gameLightSources.hint"), + filePicker: "any", + scope: "world", + config: true, + default: "", + type: String, }); - game.settings.register("torch", "offDimRadius", { - name: game.i18n.localize("torch.offDimRadius.name"), - hint: game.i18n.localize("torch.offDimRadius.hint"), + game.settings.register("torch", "brightRadius", { + name: game.i18n.localize("LIGHT.Bright"), + hint: game.i18n.localize("torch.brightRadius.hint"), scope: "world", config: true, - default: 0, + default: 20, type: Number, }); - game.settings.register("torch", "dancingLightVision", { - name: game.i18n.localize("torch.dancingLightVision.name"), - hint: game.i18n.localize("torch.dancingLightVision.hint"), + game.settings.register("torch", "dimRadius", { + name: game.i18n.localize("LIGHT.Dim"), + hint: game.i18n.localize("torch.dimRadius.hint"), scope: "world", config: true, - default: false, - type: Boolean, + default: 40, + type: Number, }); + if (game.system.id === "dnd5e") { + game.settings.register("torch", "dancingLightVision", { + name: game.i18n.localize("torch.dancingLightVision.name"), + hint: game.i18n.localize("torch.dancingLightVision.hint"), + scope: "world", + config: true, + default: false, + type: Boolean, + }); + } } } diff --git a/socket.js b/socket.js index 9884e03..0efffcf 100644 --- a/socket.js +++ b/socket.js @@ -1,4 +1,4 @@ -import LightSource from "./sources.js"; +import TorchRequest from "./request.js"; export default class TorchSocket { /* @@ -8,8 +8,8 @@ export default class TorchSocket { if (req.addressTo === undefined || req.addressTo === game.user.id) { let scene = game.scenes.get(req.sceneId); let token = scene.tokens.get(req.tokenId); - if (LightSource.supports(req.requestType)) { - await LightSource.perform(req.requestType, scene, token); + if (TorchRequest.supports(req.requestType)) { + await TorchRequest.perform(req.requestType, scene, token); } else { console.warning( `Torch | --- Attempted unregistered socket action ${req.requestType}` @@ -22,7 +22,7 @@ export default class TorchSocket { * See if this light source supports a socket request for this action */ static requestSupported(action, lightSource) { - return LightSource.supports(`${action}:${lightSource}`); + return TorchRequest.supports(`${action}:${lightSource}`); } /* @@ -36,14 +36,14 @@ export default class TorchSocket { tokenId: tokenId, }; - if (LightSource.supports(req.requestType)) { - if (LightSource.isPermitted(game.user, req.requestType)) { + if (TorchRequest.supports(req.requestType)) { + if (TorchRequest.isPermitted(game.user, req.requestType)) { TorchSocket.handleSocketRequest(req); return true; } else { let recipient = game.users.contents.find((user) => { return ( - user.active && LightSource.isPermitted(user, req.requestType) + user.active && TorchRequest.isPermitted(user, req.requestType) ); }); if (recipient) { diff --git a/source-specs.js b/source-specs.js deleted file mode 100644 index 5f1bfd5..0000000 --- a/source-specs.js +++ /dev/null @@ -1,297 +0,0 @@ -import Settings from "./settings.js"; - -const DND5E_LIGHT_SOURCES = { - Candle: { - name: "Candle", - light: [{ bright: 5, dim: 10 }], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - Torch: { - name: "Torch", - light: [{ bright: 20, dim: 40 }], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - Lamp: { - name: "Lamp", - light: [{ bright: 15, dim: 45 }], - shape: "sphere", - type: "equipment", - consumable: false, - states: 2, - }, - "Bullseye Lantern": { - name: "Bullseye Lantern", - light: [{ bright: 15, dim: 45 }], - shape: "cone", - type: "equipment", - consumable: false, - states: 2, - }, - "Hooded Lantern": { - name: "Hooded Lantern", - light: [ - { bright: 30, dim: 60 }, - { bright: 0, dim: 5 }, - ], - shape: "sphere", - type: "equipment", - consumable: false, - states: 3, - }, - Light: { - name: "Light", - light: [{ bright: 20, dim: 40 }], - shape: "sphere", - type: "cantrip", - consumable: false, - states: 2, - }, - "Dancing Lights": { - name: "Dancing Lights", - light: [{ bright: 0, dim: 10 }], - shape: "sphere", - type: "cantrip", - consumable: false, - states: 2, - }, -}; - -const SWADE_LIGHT_SOURCES = { - "Candle": { - name: "Candle", - light: [{bright: 0, dim: 2}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - "Flashlight": { - name: "Flashlight", - light: [{bright: 10, dim: 10}], - shape: "beam", - type: "equipment", - consumable: false, - states: 2 - }, - "Lantern": { - name: "Lantern", - light: [{bright: 4, dim: 4}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 2, - }, - "Torch": { - name: "Torch", - light: [{bright: 4, dim: 4}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - } -}; - -const ED4E_LIGHT_SOURCES = { - "Candle": { - name: "Candle", - light: [{bright: 0, dim: 3}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - "Lantern (Hooded)": { - name: "Lantern (Hooded)", - light: [{bright: 10, dim: 10}, {bright: 0, dim: 10}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 3 - }, - "Lantern (Bullseye)": { - name: "Lantern (Bullseye)", - light: [{bright: 20, dim: 20}], - shape: "beam", - type: "equipment", - consumable: false, - states: 2, - }, - "Torch": { - name: "Torch", - light: [{bright: 10, dim: 10}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - } -}; - -const PF2E_LIGHT_SOURCES = { - "Candle": { - name: "Candle", - light: [{bright: 0, dim: 10}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - "Lantern (Hooded)": { - name: "Lantern (Hooded)", - light: [{bright: 30, dim: 60}, {bright: 0, dim: 5}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 3 - }, - "Lantern (Bull's Eye)": { - name: "Lantern (Bull's Eye)", - light: [{bright: 60, dim: 120}], - shape: "cone", - type: "equipment", - consumable: false, - states: 2, - }, - "Torch": { - name: "Torch", - light: [{bright: 20, dim: 40}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - } -}; - -const PF1_LIGHT_SOURCES = { - "Candle": { - name: "Candle", - light: [{bright: 0, dim: 5}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - }, - "Lamp": { - name: "Lamp", - light: [{bright: 15, dim: 30}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 2 - }, - "Lantern": { - name: "Lantern", - light: [{bright: 30, dim: 60}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 2, - }, - "Bullseye Lantern": { - name: "Bullseye Lantern", - light: [{bright: 60, dim: 120}], - shape: "cone", - type: "equipment", - consumable: false, - states: 2, - }, - "Hooded Lantern": { - name: "Hooded Lantern", - light: [{bright: 30, dim: 60}], - shape: "sphere", - type: "equipment", - consumable: false, - states: 2, - }, - "Miner's Lantern": { - name: "Miner's Lantern", - light: [{bright: 30, dim: 60}], - shape: "cone", - type: "equipment", - consumable: false, - states: 2, - }, - "Torch": { - name: "Torch", - light: [{bright: 20, dim: 40}], - shape: "sphere", - type: "equipment", - consumable: true, - states: 2, - } -}; - -export default class SourceSpecs { - static augmentSources(sourceSet) { - let sources = Object.assign({}, sourceSet); - let inventoryItem = Settings.inventoryItemName; - if (!inventoryItem) { - return sources; - } else { - if (!SourceSpecs.find(inventoryItem, sources)) { - sources[inventoryItem] = { - name: inventoryItem, - light: [ - { - bright: Settings.litRadii.bright, - dim: Settings.litRadii.dim, - }, - ], - shape: "sphere", - type: "none", - consumable: false, - states: 2, - }; - } - } - return sources; - } - - static get lightSources() { - let itemName = Settings.inventoryItemName; - let sources = {}; - switch (game.system.id) { - case "dnd5e": - sources = SourceSpecs.augmentSources(DND5E_LIGHT_SOURCES); - break; - case "swade": - sources = SourceSpecs.augmentSources(SWADE_LIGHT_SOURCES); - break; - case "earthdawn4e": - sources = SourceSpecs.augmentSources(ED4E_LIGHT_SOURCES); - break; - case "pf2e": - sources = SourceSpecs.augmentSources(PF2E_LIGHT_SOURCES); - break; - case "pf1": - sources = SourceSpecs.augmentSources(PF1_LIGHT_SOURCES); - break; - default: - sources["Self"] = { - name: "Self", - light: [ - { bright: Settings.litRadii.bright, dim: Settings.litRadii.dim }, - ], - shape: "sphere", - type: "none", - consumable: false, - states: 2, - }; - } - return sources; - } - static find(name, sources) { - for (let sourceName in sources) { - if (sourceName.toLowerCase() === name.toLowerCase()) { - return sources[sourceName]; - } - } - return false; - }; - -} diff --git a/sources.json b/sources.json new file mode 100644 index 0000000..41f3b5b --- /dev/null +++ b/sources.json @@ -0,0 +1,290 @@ +{ + "dnd5e": { + "system": "dnd5e", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { "bright": 5, "dim": 10, "angle": 360 } + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { "bright": 20, "dim": 40, "angle": 360 } + ] + }, + "Lamp": { + "name": "Lamp", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + { "bright": 15, "dim": 45, "angle": 360 } + ] + }, + "Bullseye Lantern": { + "name": "Bullseye Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + { "bright": 15, "dim": 45, "angle": 53 } + ] + }, + "Hooded Lantern": { + "name": "Hooded Lantern", + "type": "equipment", + "consumable": false, + "states": 3, + "light": [ + { "bright": 30, "dim": 60, "angle": 360 }, + { "bright": 0, "dim": 5, "angle": 360 } + ] + }, + "Light": { + "name": "Light", + "type": "cantrip", + "consumable": false, + "states": 2, + "light": [ + { "bright": 20, "dim": 40, "angle": 360 } + ] + }, + "Dancing Lights": { + "name": "Dancing Lights", + "type": "cantrip", + "consumable": false, + "states": 2, + "light": [ + { "bright": 0, "dim": 10, "angle": 360 } + ] + } + } + }, + "swade": { + "system": "swade", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 0, "dim": 2, "angle": 360} + ] + }, + "Flashlight": { + "name": "Flashlight", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 10, "dim": 10, "angle": 3} + ] + }, + "Lantern": { + "name": "Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 4, "dim": 4, "angle": 360} + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 4, "dim": 4, "angle": 360} + ] + } + } + }, + "pf1": { + "system": "pf1", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 0, "dim": 5, "angle": 360} + ] + }, + "Lamp": { + "name": "Lamp", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 15, "dim": 30, "angle": 360} + ] + }, + "Lantern": { + "name": "Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 30, "dim": 60, "angle": 360} + ] + }, + "Bullseye Lantern": { + "name": "Bullseye Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 60, "dim": 120, "angle": 53} + ] + }, + "Hooded Lantern": { + "name": "Hooded Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 30, "dim": 60, "angle": 360} + ] + }, + "Miner's Lantern": { + "name": "Miner's Lantern", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 30, "dim": 60, "angle": 53} + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 20, "dim": 40, "angle": 360} + ] + } + } + }, + "pf2e": { + "system": "pf2e", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 0, "dim": 10, "angle": 360} + ] + }, + "Lantern (Hooded)": { + "name": "Lantern (Hooded)", + "type": "equipment", + "consumable": false, + "states": 3, + "light": [ + {"bright": 30, "dim": 60, "angle": 360}, + {"bright": 0, "dim": 5, "angle": 360} + ] + }, + "Lantern (Bull's Eye)": { + "name": "Lantern (Bull's Eye)", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 60, "dim": 120, "angle": 53} + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 20, "dim": 40, "angle": 360} + ] + } + } + }, + "earthdawn4e": { + "system": "earthdawn4e", + "topology": "standard", + "quantity" : "amount", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 0, "dim": 3, "angle": 360} + ] + }, + "Lantern (Hooded)": { + "name": "Lantern (Hooded)", + "type": "equipment", + "consumable": false, + "states": 3, + "light": [ + {"bright": 10, "dim": 10, "angle": 360}, + {"bright": 0, "dim": 10, "angle": 360} + ] + }, + "Lantern (Bullseye)": { + "name": "Lantern (Bullseye)", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + {"bright": 20, "dim": 20, "angle": 3} + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + {"bright": 10, "dim": 10, "angle": 360} + ] + } + } + }, + "default": { + "system": "default", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Self": { + "name": "Self", + "type": "none", + "consumable": false, + "states": 2, + "light": [ + {"bright": 40, "dim":20, "angle": 360} + ] + } + } + } +} diff --git a/test/common-library-tests.js b/test/common-library-tests.js new file mode 100644 index 0000000..5da75c7 --- /dev/null +++ b/test/common-library-tests.js @@ -0,0 +1,143 @@ +import SourceLibrary from '../library.js'; +export let torchCommonLibraryTests = (context) => { + const {describe, it, assert, afterEach} = context; + describe('Torch Common Library Tests', () => { + describe('Library Loading tests', () => { + afterEach(async() => { + SourceLibrary.commonLibrary = undefined; + }); + it('library load for D&D 5e without a user library', async () => { + assert.notOk(SourceLibrary.commonLibrary, "no common library loaded initially"); + let library = await SourceLibrary.load('dnd5e', 10, 50, 'Torch'); + assert.ok(SourceLibrary.commonLibrary, "common library loaded"); + let candle = library.getLightSource("candle"); + assert.equal(candle.light[0].bright, 5, "common candle bright light level"); + assert.equal(candle.light[0].dim, 10, "common candle dim light level"); + let torch = library.getLightSource("torch"); + assert.equal(torch.light[0].bright, 10, "torch bright light level from settings"); + assert.equal(torch.light[0].dim, 50, "torch dim light level from settings"); + let phantom = library.getLightSource("phantom torch"); + assert.notOk(phantom,"No phantom torch defined as expected"); + }); + it('library load for D&D 5e with no user inventory item set', async () => { + assert.notOk(SourceLibrary.commonLibrary, "no common library loaded initially"); + let library = await SourceLibrary.load('dnd5e', 50, 10, undefined); + assert.ok(SourceLibrary.commonLibrary, "common library loaded"); + let candle = library.getLightSource("candle"); + assert.equal(candle.light[0].bright, 5, "common candle bright light level"); + assert.equal(candle.light[0].dim, 10, "common candle dim light level"); + assert.notOk(candle.light[0].alpha, "no candle alph level set"); + let torch = library.getLightSource("torch"); + assert.equal(torch.light[0].bright, 20, "common torch bright light level"); + assert.equal(torch.light[0].dim, 40, "common torch dim light level"); + }); + it('library load for D&D 5e with a user library', async () => { + assert.notOk(SourceLibrary.commonLibrary, "no common library loaded initially"); + let library = await SourceLibrary.load('dnd5e', 10, 50, 'Torch', './userLights.json'); + assert.ok(SourceLibrary.commonLibrary, "common library loaded"); + let candle = library.getLightSource("candle"); + assert.equal(candle.light[0].bright, 10, "user candle bright light level"); + assert.equal(candle.light[0].dim, 15, "user candle dim light level"); + assert.equal(candle.light[0].alpha, 0.5, "user candle has alpha defined"); + let torch = library.getLightSource("torch"); + assert.equal(torch.light[0].bright, 10, "torch bright light level from settings"); + assert.equal(torch.light[0].dim, 50, "torch dim light level from settings"); + let phantom = library.getLightSource("phantom torch"); + assert.ok(phantom, "The phantom torch light source exists in the data"); + assert.equal(phantom.light[0].bright, 5, "user phantom torch bright light level"); + assert.equal(phantom.light[0].dim, 20, "user phantom torch bright light level"); + }); + it('library load for GURPS with a user library with a GURPS flashlight', async () => { + assert.notOk(SourceLibrary.commonLibrary, "no common library loaded initially"); + let library = await SourceLibrary.load('test', 50, 10, 'Torch', './userLights.json'); + assert.ok(SourceLibrary.commonLibrary, "common library loaded"); + let candle = library.getLightSource("candle"); + assert.notOk(candle, "No candle defined as expected"); + let selfSource = library.getLightSource("self"); + assert.notOk(selfSource, "No self light source defined as expected"); + let flashlight = library.getLightSource("flashlight"); + assert.ok(flashlight, "Flashlight defined as expected"); + assert.equal(flashlight.light[0].bright, 10, "user flashlight light level"); + assert.equal(flashlight.light[0].dim, 0, "user flashlight light level"); + assert.equal(flashlight.light[0].angle, 3, "user flashlight light angle"); + }); + }); + if (game.system.id === 'dnd5e') { + describe('Library topology tests for D&D5e actors', () => { + it('Actor inventory settings for Versatile in D&D5e with a user library', async () => { + let versatile = game.actors.getName("Versatile"); + assert.ok(versatile, "Actor Versatile found"); + let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); + let torches = library.getInventory(versatile.id, "Torch"); + assert.ok(typeof torches === "number", "Count of torches has a numeric value"); + let candles = library.getInventory(versatile.id, "Candle"); + assert.ok(typeof candles === "number", "Count of candles has a numeric value"); + let lights = library.getInventory(versatile.id, "Light"); + assert.ok(typeof lights === "undefined", "Light cantrip doesn't have inventory"); + let lamps = library.getInventory(versatile.id, "Lamp"); + assert.ok(typeof lamps === "undefined", "Lamp doesn't have inventory"); + await library._presetInventory(versatile.id, "Torch", 2); + let before = library.getInventory(versatile.id, "Torch"); + await library.decrementInventory(versatile.id, "Torch"); + let afterFirst = library.getInventory(versatile.id, "Torch"); + await library.decrementInventory(versatile.id, "Torch"); + let afterSecond = library.getInventory(versatile.id, "Torch"); + await library.decrementInventory(versatile.id, "Torch"); + let afterThird = library.getInventory(versatile.id, "Torch"); + assert.equal(before, 2, "Started with set value"); + assert.equal(afterFirst, 1, "Decremented to one"); + assert.equal(afterSecond, 0, "Decremented to zero"); + assert.equal(afterThird, 0, "Remained at zero"); + }); + it('Actor image settings for Versatile in D&D5e with a user library', async () => { + let versatile = game.actors.getName("Versatile"); + assert.ok(versatile, "Actor Versatile found"); + let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); + let torchImage = library.getImage(versatile.id, "Torch"); + assert.ok(torchImage, "Torch should have a defined image"); + assert.notEqual(torchImage,"", "Torch image has a reasonable value"); + let candleImage = library.getImage(versatile.id, "Candle"); + assert.ok(candleImage, "Candle should have a defined image"); + assert.notEqual(candleImage,"", "Candle image has a reasonable value"); + let lampImage = library.getImage(versatile.id, "Lamp"); + assert.ok(lampImage, "Lamp should have a defined image"); + assert.notEqual(lampImage,"", "Lamp image has a reasonable value"); + let lightImage = library.getImage(versatile.id, "Light"); + assert.ok(lightImage, "Light cantrip should have a defined image"); + assert.notEqual(lightImage,"", "Light cantrip image has a reasonable value"); + }); + it('Actor light sources for Lightbearer, Breaker, Empty, and Bic in D&D5e with a user library', async () => { + let breaker = game.actors.getName("Breaker"); + assert.ok(breaker, "Actor Breaker found"); + let empty = game.actors.getName("Empty"); + assert.ok(empty, "Actor Empty found"); + let bearer = game.actors.getName("Torchbearer"); + assert.ok(bearer, "Actor Torchbearer found"); + let everythingBut = game.actors.getName("Bic"); + assert.ok(everythingBut, "Actor Bic found"); + let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); + + let breakerSources = library.actorLightSources(breaker.id); + assert.equal (breakerSources.length, 2, "Breaker has two known light sources"); + assert.notEqual(breakerSources[0].name, breakerSources[1].name, "Breaker's sources are different"); + assert.ok (["Torch", "Dancing Lights"].includes(breakerSources[0].name), "Breaker's first source is expected"); + assert.ok (["Torch", "Dancing Lights"].includes(breakerSources[1].name), "Breaker's second source is expected"); + assert.equal(library.actorHasLightSource(breaker.id, "Dancing Lights"), true, "Breaker is reported as having Dancing Lights"); + + let bearerSources = library.actorLightSources(bearer.id); + assert.equal(bearerSources.length, 1, "Torchbearer has precisely one light source"); + assert.equal(bearerSources[0].name, "Torch", "Torchbearer's light source is Torch, as eqpected"); + assert.equal(library.actorHasLightSource(bearer.id, "Torch"), true, "Bearer is reported as having the Torch light source"); + + let emptySources = library.actorLightSources(empty.id); + assert.equal (emptySources.length, 0, "Empty truly has no known light sources"); + assert.equal(library.actorHasLightSource(empty.id, "Candle"), false, "Empty is reported as not having the candle light source"); + + let everythingButSources = library.actorLightSources(everythingBut.id); + assert.equal(everythingButSources.length, 0, "Bic has no known light sources, even though it has ways of casting light"); + assert.equal(library.actorHasLightSource(everythingBut.id, "Candle"), false, "Empty is reported as not having the candle light source"); + }); + }); + } + }); +}; diff --git a/test/common-token-tests.js b/test/common-token-tests.js new file mode 100644 index 0000000..67e38fc --- /dev/null +++ b/test/common-token-tests.js @@ -0,0 +1,61 @@ +import TorchToken from '../token.js'; +import SourceLibrary from '../library.js'; + +let initiateWith = async function(name, item, count, assert) { + let foundryToken = game.scenes.active.tokens.getName(name); + assert.ok(foundryToken, "Token for "+ name + " found in scene"); + let library = await SourceLibrary.load("dnd5e", 10, 20); + assert.ok(library, "Library successfully created"); + library._presetInventory(foundryToken.actor.id, item, count); + return new TorchToken(foundryToken, library); +} +export let torchCommonTokenTests = (context) => { + const {describe, it, assert, afterEach} = context; + describe('Torch Common Token Tests', () => { + if (game.system.id === 'dnd5e') { + describe('Token tests for D&D5e actors, scene, and tokens', () => { + afterEach(async() => { + SourceLibrary.commonLibrary = undefined; + }); + it('Light source selection', async () => { + let token = await initiateWith("Versatile", "Torch", 1, assert); + let sources = token.ownedLightSources; + let currentSource = token.currentLightSource; + assert.ok(sources, "Owned light sources came back in one piece"); + assert.ok(currentSource, "The token has a current source"); + }); + it('Cycle of token states - torch', async () => { + let token = await initiateWith("Versatile", "Torch", 1, assert); + await token.forceStateOff() + assert.equal(token.lightSourceState, token.STATE_OFF, "Token is off"); + await token.setCurrentLightSource("Torch"); + let exhausted = token.lightSourceIsExhausted("Torch"); + assert.equal(exhausted, false, "Torches are not exhausted when we start"); + await token.advanceState(); + assert.equal(token.lightSourceState, token.STATE_ON, "Token is on"); + await token.advanceState(); + assert.equal(token.lightSourceState, token.STATE_OFF, "Token is off"); + exhausted = token.lightSourceIsExhausted("Torch"); + assert.equal(exhausted, true, "Torches are exhausted when we're done"); + + }); + it('Cycle of token states - hooded lantern', async () => { + let token = await initiateWith("Versatile", "Torch", 1, assert); + await token.forceStateOff() + assert.equal(token.lightSourceState, token.STATE_OFF, "Token is off"); + await token.setCurrentLightSource("Hooded Lantern"); + let exhausted = token.lightSourceIsExhausted("Hooded Lantern"); + assert.equal(exhausted, false, "Hooded Lanterns are not exhausted when we start"); + await token.advanceState(); + assert.equal(token.lightSourceState, token.STATE_ON, "Token is on"); + await token.advanceState(); + assert.equal(token.lightSourceState, token.STATE_DIM, "Token is dim"); + await token.advanceState(); + assert.equal(token.lightSourceState, token.STATE_OFF, "Token is off"); + exhausted = token.lightSourceIsExhausted("Hooded Lantern"); + assert.equal(exhausted, false, "Hooded Lanterns are not exhausted when we're done either"); + }); + }); + } + }); +}; diff --git a/test/test-hook.js b/test/test-hook.js index 01bc986..c339dee 100644 --- a/test/test-hook.js +++ b/test/test-hook.js @@ -1,12 +1,18 @@ import { torchGenericBasicTests } from './generic-basic-tests.js'; import { torchDnD5eBasicTests } from './dnd5e-basic-tests.js'; +import { torchCommonLibraryTests } from './common-library-tests.js'; +import { torchCommonTokenTests } from './common-token-tests.js'; export function hookTests() { if (game.world.data.name.startsWith("torch-test-")) { // Belt and braces check console.log("Torch | Registering tests..."); + quench.registerBatch("torch.common.library", torchCommonLibraryTests, { displayName: "Torch: Common Library Tests" }); + quench.registerBatch("torch.common.token", torchCommonTokenTests, { displayName: "Torch: Common Token Tests" }); if (game.system.id === 'dnd5e') { - quench.registerBatch("torch.dnd5e.basic", torchDnD5eBasicTests, { displayName: "Torch: D&D 5e Basic Test" }); + //quench.registerBatch("torch.dnd5e.library", torchDnD5eLibraryTests, { displayName: "Torch: D&D 5e Library Tests" }) + //quench.registerBatch("torch.dnd5e.basic", torchDnD5eBasicTests, { displayName: "Torch: D&D 5e Basic Test" }); } else { - quench.registerBatch("torch.generic.basic", torchGenericBasicTests, { displayName: "Torch: Generic Basic Test" }); + //quench.registerBatch("torch.generic.library", torchGenericLibraryTests, { displayName: "Torch: Generic Library Tests" }) + //quench.registerBatch("torch.generic.basic", torchGenericBasicTests, { displayName: "Torch: Generic Basic Test" }); } } } diff --git a/test/userLights.json b/test/userLights.json new file mode 100644 index 0000000..b66932d --- /dev/null +++ b/test/userLights.json @@ -0,0 +1,46 @@ +{ + "dnd5e": { + "system": "dnd5e", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Phantom Torch": { + "name": "Phantom Torch", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { "bright": 5, "dim": 20, "angle": 360, "color": "#ff9329", "alpha": 0.6 } + ] + }, + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { + "bright": 10, "dim": 15, "angle": 360, "color": "#ff9329", "alpha": 0.5, + "animation": { "type": "torch", "speed": 5, "intensity": 5, "reverse": false } + } + ] + } + } + }, + "test": { + "system": "test", + "topology": "gurps", + "quantity": "quantity", + "sources": { + "Flashlight": { + "name": "Flashlight", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { "bright": 10, "dim": 0, "angle": 3, "color": "#ffd6aa", "alpha": 1.0 } + ] + } + } + } +} diff --git a/test/vttlink.sh b/test/vttlink.sh index 1d4b00d..c061ede 100755 --- a/test/vttlink.sh +++ b/test/vttlink.sh @@ -12,6 +12,8 @@ rm $targetdir/module.json ln -s $clonedir/module.json $targetdir/module.json rm $targetdir/torch.css ln -s $clonedir/torch.css $targetdir/torch.css +rm $targetdir/sources.json +ln -s $clonedir/sources.json $targetdir/sources.json # Replace javascript in root directory with link for file in $clonedir/*.js do @@ -31,3 +33,11 @@ do fi ln -s $file $targetdir/test/$(basename $file) done +for file in $clonedir/lang/*.json +do + echo test/$(basename $file) + if [ -f "$targetdir/lang/$(basename $file)" ]; then + rm $targetdir/lang/$(basename $file) + fi + ln -s $file $targetdir/lang/$(basename $file) +done diff --git a/token.js b/token.js index a0e5618..7e77ade 100644 --- a/token.js +++ b/token.js @@ -1,119 +1,95 @@ import TorchSocket from "./socket.js"; import Settings from "./settings.js"; -import SourceSpecs from "./source-specs.js"; let DEBUG = true; -let debugLog = (...args) => { +const debugLog = (...args) => { if (DEBUG) { console.log(...args); } }; -let getAngle = (shape) => { - switch (shape) { - case "cone": - return 53.13; - case "beam": - return 3; - case "sphere": - default: - return 360; +const getLightUpdates = function(lightSettings) { + let result = {} + for (let setting in lightSettings) { + result["light." + setting] = lightSettings[setting]; } -}; - -// GURPS.recurselist(game.actors.get(this.token.actorId).system.equipment.carried,(item) => { console.log("Name: ", item.name, ", Count: ",item.count); }); + return result; +} export default class TorchToken { STATE_ON = "on"; STATE_DIM = "dim"; STATE_OFF = "off"; - token; + _token; + _library; + _ownedSources; - constructor(token) { - this.token = token; - } - // Flags - get currentLightSource() { - let lightSource = this.token.getFlag("torch", "lightSource"); - let owned = this.ownedLightSources; - if (lightSource && owned.find((item) => item.name === lightSource)) - return lightSource; - let itemName = Settings.inventoryItemName; - let sourceData = itemName - ? owned.find((item) => item.name.toLowerCase() === itemName.toLowerCase()) - : undefined; - if (itemName &&!!sourceData) { - return sourceData.name; - } - if (owned.length > 0) { - return owned[0].name; - } - return; + constructor(token, library) { + this._token = token; + this._library = library; + this._ownedSources = library.actorLightSources(this._token.actorId); } - async setCurrentLightSource(value) { - await this.token.setFlag("torch", "lightSource", value); + + get ownedLightSources() { + return this._ownedSources; } + get lightSourceState() { - let state = this.token.getFlag("torch", "lightSourceState"); + let state = this._token.getFlag("torch", "lightSourceState"); return typeof state === "undefined" ? this.STATE_OFF : state; } - get ownedLightSources() { - let allSources = SourceSpecs.lightSources; - let items = Array.from(game.actors.get(this.token.actorId).items).filter( - (item) => { - let itemSource = SourceSpecs.find(item.name, allSources); - if (item.type === "spell") { - return !!itemSource && itemSource.type === "cantrip"; - } else { - return !!itemSource && itemSource.type === "equipment"; - } - } - ); - if (items.length > 0) { - return items.map((item) => { - return Object.assign( - { image: item.img, quantity: item.quantity }, - allSources[item.name] - ); - }); - } else if ("Self" in allSources) { - return [ - Object.assign( - { image: "/icons/svg/light.svg", quantity: 1 }, - allSources["Self"] + + get currentLightSource() { + // The one we saved + let lightSource = this._token.getFlag("torch", "lightSource"); + if (lightSource && this._ownedSources.find( + (item) => item.name === lightSource + ) + ) { + return lightSource; + } + // The one the GM asked for + let itemName = Settings.inventoryItemName; + let namedSource = itemName + ? this._ownedSources.find( + (item) => item.name.toLowerCase() === itemName.toLowerCase() ) - ]; + : undefined; + if (itemName &&!!namedSource) { + return namedSource.name; } + // The top one on the list + if (this._ownedSources.length > 0) { + return this._ownedSources[0].name; + } + // Nothing + return; } - get currentLightSourceIsExhausted() { - return this.sourceIsExhausted(this.currentLightSource); + async setCurrentLightSource(value) { + await this._token.setFlag("torch", "lightSource", value); } - sourceIsExhausted(source) { - let allSources = SourceSpecs.lightSources; - if (allSources[source].consumable) { - // Now we can consume it - let torchItem = Array.from( - game.actors.get(this.token.actorId).items - ).find((item) => item.name.toLowerCase() === source.toLowerCase()); - return torchItem && torchItem.system.quantity === 0; + lightSourceIsExhausted(source) { + if (this._library.getLightSource(source).consumable) { + let inventory = this._library.getInventory(this._token.actorId, source); + return inventory === 0; } return false; } /* Orchestrate State Management */ + async forceStateOff() { // Need to deal with dancing lights - await this.token.setFlag("torch", "lightSourceState", this.STATE_OFF); - await this.turnOffSource(); + await this._token.setFlag("torch", "lightSourceState", this.STATE_OFF); + await this._turnOffSource(); } async advanceState() { let source = this.currentLightSource; let state = this.lightSourceState; - let allSources = SourceSpecs.lightSources; - if (allSources[source].states === 3) { + if (this._library.getLightSource(source).states === 3) { state = state === this.STATE_OFF ? this.STATE_ON @@ -123,88 +99,65 @@ export default class TorchToken { } else { state = state === this.STATE_OFF ? this.STATE_ON : this.STATE_OFF; } - await this.token.setFlag("torch", "lightSourceState", state); + await this._token.setFlag("torch", "lightSourceState", state); switch (state) { case this.STATE_OFF: - await this.turnOffSource(); + await this._turnOffSource(); break; case this.STATE_ON: - await this.turnOnSource(); + await this._turnOnSource(); break; case this.STATE_DIM: - await this.dimSource(); + await this._dimSource(); break; default: - await this.turnOffSource(); + await this._turnOffSource(); } return state; } - async turnOffSource() { + + // Private internal methods + + async _turnOffSource() { if (TorchSocket.requestSupported("delete", this.currentLightSource)) { // separate token lighting - TorchSocket.sendRequest(this.token.id, "delete", this.currentLightSource); + TorchSocket.sendRequest(this._token.id, "delete", this.currentLightSource); } else { - // self lighting - let sourceData = SourceSpecs.lightSources[this.currentLightSource]; - await this.token.update({ - "light.bright": Settings.offRadii.bright, - "light.dim": Settings.offRadii.dim, - "light.angle": 360, - }); - if (sourceData.consumable && sourceData.type === "equipment") { - this.consumeSource(); + // self lighting - to turn off, use light settings from prototype token + let protoToken = game.actors.get(this._token.actorId).prototypeToken; + await this._token.update(getLightUpdates(protoToken.light)); + let source = this._library.getLightSource(this.currentLightSource); + if (source.consumable) { + await this._consumeSource(source); } } } - async turnOnSource() { + + async _turnOnSource() { if (TorchSocket.requestSupported("create", this.currentLightSource)) { // separate token lighting - TorchSocket.sendRequest(this.token.id, "create", this.currentLightSource); + TorchSocket.sendRequest(this._token.id, "create", this.currentLightSource); } else { // self lighting - let sourceData = SourceSpecs.lightSources[this.currentLightSource]; - await this.token.update({ - "light.bright": sourceData.light[0].bright, - "light.dim": sourceData.light[0].dim, - "light.angle": getAngle(sourceData.shape), - }); - if (sourceData.consumable && sourceData.type === "spell") { - this.consumeSource(); - } + let source = this._library.getLightSource(this.currentLightSource); + await this._token.update(getLightUpdates(source.light[0])); } } - async dimSource() { - let sourceData = SourceSpecs.lightSources[this.currentLightSource]; - if (sourceData.states === 3) { - await this.token.update({ - "light.bright": sourceData.light[1].bright, - "light.dim": sourceData.light[1].dim, - "light.angle": getAngle(sourceData.shape), - }); + async _dimSource() { + let source = this._library.getLightSource(this.currentLightSource); + if (source.states === 3) { + await this._token.update(getLightUpdates(source.light[1])); } } - async consumeSource() { - let sourceData = SourceSpecs.lightSources[this.currentLightSource]; - let torchItem = Array.from(game.actors.get(this.token.actorId).items).find( - (item) => item.name === sourceData.name - ); - if ( - torchItem && - sourceData.consumable && - (!game.user.isGM || Settings.gmUsesInventory) - ) { - if (sourceData.type === "spell" && torchItem.type === "spell") { - //TODO: Figure out how to consume spell levels - and whether we want to - } else if ( - sourceData.type === "equipment" && - torchItem.type !== "spell" - ) { - if (torchItem.system.quantity > 0) { - await torchItem.update({ - "system.quantity": torchItem.system.quantity - 1, - }); + async _consumeSource(source) { + if ((game.user.isGM && Settings.gmUsesInventory) || + (!game.user.isGM && Settings.userUsesInventory)) { + let count = this._library.getInventory(this._token.actorId, source.name); + if (count ? count > 0 : false) { + if (!game.user.isGM || Settings.gmUsesInventory) { + await this._library.decrementInventory(this._token.actorId, source.name); } } } diff --git a/topology.js b/topology.js new file mode 100644 index 0000000..7466bc6 --- /dev/null +++ b/topology.js @@ -0,0 +1,120 @@ +export default function getTopology(type, quantityField) { + const TOPOLOGIES = { + "standard": StandardLightTopology, + "gurps": GURPSLightTopology + } + if (type in TOPOLOGIES) { + return new TOPOLOGIES[type](quantityField); + } else { + return new DefaultLightTopology(); + } +} + +const DEFAULT_IMAGE_URL = "/icons/svg/light.svg"; + +/* Topologies to use */ + +class StandardLightTopology { + quantityField = "quantity"; + constructor(quantityField) { + this.quantityField = quantityField ?? "quantity"; + } + _findMatchingItem(actorId, lightSourceName) { + return Array.from(game.actors.get(actorId).items).find( + (item) => item.name.toLowerCase() === lightSourceName.toLowerCase() + ); + } + + actorHasLightSource(actorId, lightSource) { + return !!this._findMatchingItem(actorId, lightSource.name); + } + + getImage (actorId, lightSource) { + let item = this._findMatchingItem(actorId, lightSource.name); + return item ? item.img : DEFAULT_IMAGE_URL; + } + + getInventory (actorId, lightSource) { + if (!lightSource.consumable) return; + let item = this._findMatchingItem(actorId, lightSource.name); + return item ? item.system[this.quantityField] : undefined; + } + + async decrementInventory (actorId, lightSource) { + if (!lightSource.consumable) return; + let item = this._findMatchingItem(actorId, lightSource.name); + if (item && item.system[this.quantityField] > 0) { + let fieldsToUpdate = {}; + fieldsToUpdate["system." + this.quantityField] = item.system[this.quantityField] - 1; + return item.update(fieldsToUpdate); + } else { + return Promise.resolve(); + } + } + async setInventory (actorId, lightSource, count) { + if (!lightSource.consumable) return; + let item = this._findMatchingItem(actorId, lightSource.name); + let fieldsToUpdate = {}; + fieldsToUpdate["system." + this.quantityField] = count; + return item.update(fieldsToUpdate); + } + } + + + class GURPSLightTopology { + quantityField = "quantity"; + constructor(quantityField) { + this.quantityField = quantityField ?? "quantity"; + } + + _findMatchingItem(actorId, lightSourceName) { + GURPS.recurselist(game.actors.get(actorId).system.equipment.carried,(item, key) => { + if (item.name.toLowerCaase() === lightSourceName.toLowerCase()) { + return [item, key]; + } + }); + return [undefined, undefined]; + } + + actorHasLightSource(actorId, lightSource) { + let [item] = this._findMatchingItem (actorId, lightSource.name); + return !!item; + } + + getImage (/*actorId, lightSource*/) { + // We always use the same image because the system doesn't supply them + return DEFAULT_IMAGE_URL; + } + + getInventory (actorId, lightSource) { + let [item] = this._findMatchingItem (actorId, lightSource.name); + return item ? item.count : undefined; + } + + async decrementInventory (actorId, lightSource) { + let [item, key] = this._findMatchingItem (actorId, lightSource.name); + if (item && item.count > 0) { + game.actors.get(actorId).updateEqtCount(key, item.count - 1 ); + } else { + return Promise.resolve(); + } + } + } + + class DefaultLightTopology { + quantityField = "quantity"; + constructor(/*quantityField*/) {} + actorHasLightSource(actorId, lightSource) { + return lightSource.name === "Self"; + } + getImage (actorId, lightSource) { + return DEFAULT_IMAGE_URL; + } + getInventory (actorId, lightSource) { + return 1; + } + async decrementInventory (actorId, lightSource) { + return Promise.resolve(); + } + } + \ No newline at end of file diff --git a/torch.js b/torch.js index 5113aa0..3beef5b 100644 --- a/torch.js +++ b/torch.js @@ -2,6 +2,7 @@ import Settings from "./settings.js"; import TorchSocket from "./socket.js"; import TokenHUD from "./hud.js"; import TorchToken from "./token.js"; +import SourceLibrary from "./library.js"; /* * ---------------------------------------------------------------------------- @@ -24,10 +25,18 @@ class Torch { * Add a torch button to the Token HUD - called from TokenHUD render hook */ static async addTorchButton(hud, hudHtml, hudData) { - let token = new TorchToken(hud.object.document); + let library = await SourceLibrary.load( + game.system.id, + Settings.lightRadii.bright, + Settings.lightRadii.dim, + Settings.inventoryItemName, + Settings.gameLightSources, + ); + let token = new TorchToken(hud.object.document, library); let lightSources = token.ownedLightSources; - // Don't let the tokens we create for light sources have or use their own light sources recursively. + // Don't let the tokens we create for light sources have or use their own + // light sources recursively. if (hud.object.document.name in lightSources) return; if (!game.user.isGM && !Settings.playerTorches) return; if (!token.currentLightSource) return; From 1222892d6ad1f727b47ec4062c1d543c63af3f6d Mon Sep 17 00:00:00 2001 From: Lupestro Date: Mon, 18 Apr 2022 16:43:39 -0400 Subject: [PATCH 11/26] Fixes for default and GURPS systems - some v10 issues with GURPS prevent testing consumables. --- library.js | 11 ++--- settings.js | 26 +++++------ sources.json | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++- token.js | 6 +-- topology.js | 7 +-- 5 files changed, 148 insertions(+), 28 deletions(-) diff --git a/library.js b/library.js index 9a08de9..23f0958 100644 --- a/library.js +++ b/library.js @@ -40,14 +40,13 @@ export default class SourceLibrary { } return library; } else { - mergedLibrary["Default"].topology = getTopology( - mergedLibrary["Default"].topology, - mergedLibrary["Default"].quantity); + mergedLibrary["default"].topology = getTopology( + mergedLibrary["default"].topology, + mergedLibrary["default"].quantity); let defaultLibrary = mergedLibrary["default"]; - defaultLibrary["Self"].light.bright = selfBright; - defaultLibrary["Self"].light.dim = selfDim; - defaultLibrary["Self"].image = defaultLibrary.topology.getImage(defaultLibrary["Self"]); + defaultLibrary.sources["Self"].light[0].bright = selfBright; + defaultLibrary.sources["Self"].light[0].dim = selfDim; return new SourceLibrary(defaultLibrary); } } diff --git a/settings.js b/settings.js index 5d51b88..5c8d24c 100644 --- a/settings.js +++ b/settings.js @@ -16,14 +16,14 @@ export default class Settings { return game.settings.get("torch", "playerTorches"); } static get gmUsesInventory() { - return game.system.id === "dnd5e" - ? game.settings.get("torch", "gmUsesInventory") - : false; + return game.settings.get("torch", "gmUsesInventory"); } + static get userUsesInventory() { + return game.settings.get("torch", "playerUsesInventory"); + } + static get inventoryItemName() { - return game.system.id === "dnd5e" - ? game.settings.get("torch", "gmInventoryItemName") - : undefined; + return game.settings.get("torch", "gmInventoryItemName"); } static get lightRadii() { return { @@ -31,18 +31,16 @@ export default class Settings { dim: game.settings.get("torch", "dimRadius"), }; } - static get dancingLightsVision() { - return game.settings.get("torch", "dancingLightVision"); - } - - static get userUsesInventory() { - return game.settings.get("torch", "playerUsesInventory"); - } - static get gameLightSources() { return game.settings.get("torch", "gameLightSources"); } + static get dancingLightsVision() { + return game.system.id === "dnd5e" + ? game.settings.get("torch", "dancingLightVision") + : false; + } + static get helpText() { let turnOffLights = game.i18n.localize("torch.turnOffAllLights"); let ctrlOnClick = game.i18n.localize("torch.holdCtrlOnClick"); diff --git a/sources.json b/sources.json index 41f3b5b..0fbb940 100644 --- a/sources.json +++ b/sources.json @@ -271,9 +271,133 @@ } } }, + "gurps": { + "system": "gurps", + "topology": "gurps", + "quantity": "amount", + "sources": { + "Candle, Tallow": { + "name": "Candle, Tallow", + "type": "equipment", + "consumable": "true", + "states": 2, + "light": [ + {"bright": 0, "dim": 2, "angle": 360, "color": "#ff9329", "alpha": 0.6 } + ] + }, + "Flashlight, Heavy": { + "name": "Flashlight, Heavy", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 30, "dim": 30, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Mini Flashlight": { + "name": "Mini Flashlight", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 4, "dim": 5, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Micro Flashlight": { + "name": "Micro Flashlight", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 1, "dim": 1, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Survival Flashlight": { + "name": "Survival Flashlight", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 1, "dim": 1, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Lantern": { + "name": "Lantern", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 4, "dim": 5, "angle": 360, "color": "#ff9329", "alpha": 1 } + ] + }, + "Torch": { + "name": "Torch", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 9, "dim": 10, "angle": 360, "color": "#ff9329", "alpha": 1 } + ] + }, + "Bull's-Eye Lantern": { + "name": "Bull's-Eye Lantern", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 9, "dim": 10, "angle": 53, "color": "#ff9329", "alpha": 1 } + ] + }, + "Electric Lantern, Small": { + "name": "Electric Lantern, Small", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 3, "dim": 3, "angle": 360, "color": "#ff9329", "alpha": 1 } + ] + }, + "Electric Lantern, Large": { + "name": "Electric Lantern, Large", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 4, "dim": 5, "angle": 360, "color": "#ff9329", "alpha": 1 } + ] + }, + "Small Tactical Light": { + "name": "Small Tactical Light", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 22, "dim": 25, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Large Tactical Light": { + "name": "Large Tactical Light", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 95, "dim": 100, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + }, + "Floodlight": { + "name": "Floodlight", + "type": "equipment", + "consumable": "false", + "states": 2, + "light": [ + {"bright": 190, "dim": 200, "angle": 3, "color": "#ffd6aa", "alpha": 1 } + ] + } + } + }, "default": { "system": "default", - "topology": "standard", + "topology": "none", "quantity" : "quantity", "sources": { "Self": { diff --git a/token.js b/token.js index 7e77ade..b166bac 100644 --- a/token.js +++ b/token.js @@ -155,10 +155,8 @@ export default class TorchToken { if ((game.user.isGM && Settings.gmUsesInventory) || (!game.user.isGM && Settings.userUsesInventory)) { let count = this._library.getInventory(this._token.actorId, source.name); - if (count ? count > 0 : false) { - if (!game.user.isGM || Settings.gmUsesInventory) { - await this._library.decrementInventory(this._token.actorId, source.name); - } + if (count) { + await this._library.decrementInventory(this._token.actorId, source.name); } } } diff --git a/topology.js b/topology.js index 7466bc6..51af7a4 100644 --- a/topology.js +++ b/topology.js @@ -68,12 +68,13 @@ class StandardLightTopology { } _findMatchingItem(actorId, lightSourceName) { + let result = [undefined, undefined]; GURPS.recurselist(game.actors.get(actorId).system.equipment.carried,(item, key) => { - if (item.name.toLowerCaase() === lightSourceName.toLowerCase()) { - return [item, key]; + if (item.name.toLowerCase() === lightSourceName.toLowerCase()) { + result = [item, key]; } }); - return [undefined, undefined]; + return result; } actorHasLightSource(actorId, lightSource) { From 8f31f14ec4d7fa050227e1f458874eb44f97794c Mon Sep 17 00:00:00 2001 From: Lupestro Date: Mon, 13 Jun 2022 11:10:40 -0400 Subject: [PATCH 12/26] Updating manifest for v10 required changes --- module.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/module.json b/module.json index 0fbddc2..8b9a5e3 100644 --- a/module.json +++ b/module.json @@ -1,5 +1,5 @@ { - "name": "torch", + "id": "torch", "title": "Torch", "description": "Torch HUD Controls", "version": "2.0.0", @@ -43,6 +43,9 @@ "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", "url": "https://github.com/League-of-Foundry-Developers/torch", - "minimumCoreVersion": "10", - "compatibleCoreVersion": "10" + "compatibility": { + "minimum": 10, + "verified": 10, + "maximum": 10 + } } From c1f2f4a1025a8641c3d54218c1984271588abbe6 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Tue, 21 Jun 2022 21:46:57 -0400 Subject: [PATCH 13/26] Updated topology for GURPS - now tracks inventory correctly. --- topology.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/topology.js b/topology.js index 51af7a4..a7294d3 100644 --- a/topology.js +++ b/topology.js @@ -68,13 +68,8 @@ class StandardLightTopology { } _findMatchingItem(actorId, lightSourceName) { - let result = [undefined, undefined]; - GURPS.recurselist(game.actors.get(actorId).system.equipment.carried,(item, key) => { - if (item.name.toLowerCase() === lightSourceName.toLowerCase()) { - result = [item, key]; - } - }); - return result; + let actor = game.actors.get(actorId); + return actor.findEquipmentByName(lightSourceName); } actorHasLightSource(actorId, lightSource) { From d6212e2743f9e480bba25851c73cfc416783a708 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 23 Jul 2022 13:05:39 -0400 Subject: [PATCH 14/26] Preparing for first release for v10 --- CHANGELOG.md | 22 ++++--------- README.md | 85 +++++++++++++++++++++++++++++++++++++++++-------- lang/cn.json | 4 ++- lang/en.json | 5 +-- lang/es.json | 4 ++- lang/fr.json | 4 ++- lang/pt-BR.json | 4 ++- lang/zh-TW.json | 4 ++- module.json | 5 +-- settings.js | 4 +-- 10 files changed, 100 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd0552..79fbefa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,36 +3,26 @@ ## Middle Kingdom - v10 branch ### 2.0.0 - [TBD] - - [BREAKING] (Lupestro) This release supports v10 of Foundry - but only v10 and hopefully some way well beyond it. + - [BREAKING] (Lupestro) This release supports v10 of Foundry, and not v0.7, v0.8, or v9. - [FEATURE] (Lupestro) Now supports selection among a variety of light sources on right-click menu from torch button - a long time coming, I know. * Bullseye Lantern has cone beam on token facing. * Hooded Lantern toggles on-dim-off on click. * Candle and Torch consume inventory, indicate when exhausted. * Limitations: - * Aside from existing Dancing Lights behavior, light sources remain carried by the actor + * Aside from existing Dancing Lights behavior in dnd5e, light sources remain carried by the actor * One actor may have only one light source active per scene at a time. - * Right now, aside from a generalized actor-token light capability, we only support specific light sources in DnD5e today, but we're poised for growth there. - * No support **_yet_** for setting a light source down and stepping away. - - There are subtleties once these things have identity. - - Can somebody else turn them on? Do they cease to belong to your actor? - - What are the rules for interacting with placed light sources? - - How do you avoid turning off the wrong one? - - Do they remain placed after being turned off? - - Where do you have to be to turn them off? - - There's stuff to work out and I wanted to get this out there first. - - We can use the existing logic supporting Dancing Lights as the basis for a very limited implementation with one active light source per actor per scene, but would that be satisfying? We're getting into "piles" territory. + * No support for setting a light source down and stepping away. * We probably won't get too deep into spells beyond the two cantrips we support - The PHB only lists 7 other spells with explicit light effects - All the spells except Continual Flame and Daylight have other effects - weapon, damage, etc - that you'd want a more sophisticated module to deliver. - We could offer the Produce Flame cantrip as a half-sized Light cantrip without its thrown weapon effect, but would that be satisfying? - Anything that consumes spell slots (including Continual Flame and Daylight) should probably be invoked as a normal spell rather than a light source anyway. - * We might do better to integrate light into other modules dealing with objects than to let this get too sophisticated. - + - [FEATURE] (Lupestro) Now supports specific light sources for a modest variety of systems, with extensibility to override settings or add light sources through a JSON file. - [INTERNAL] (Lupestro) Separated concerns into multiple js files and a CSS file * The additional UI and the more complex state finally made it necessary. - * Separate root, hud, token, settings, light sources, socket comms are much easier to follow. + * Separate root, hud, token, settings, light sources, topologies, and socket comms are much easier to follow. * This will make planned future work much easier as well. - * The mere thimble of HTML needed is fine sitting in the top of the hud.js for now. + * The mere thimbleful of HTML needed is fine sitting in the top of the hud.js for now. ## Intermediate period - master branch diff --git a/README.md b/README.md index 6c87a4c..8b4c970 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,65 @@ # 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. +This module provides a HUD toggle button for turning on and off a configurable radius of bright and dim light around you. This default source ("Self") is provided for any system for which we don't supply more specialized capabilities. -Additionally, in D&D5e only: -* The single HUD control will trigger the 'Dancing Lights' cantrip if you have it. -* Failing that, it will perform 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. +However, we now have a variety of core light sources configured for D&D 5th Ed, Savage Worlds, Pathfinder 1st and 2nd editions, GURPS, and Earthdawn. You can select which light source the button will manipulate by right-clicking and clicking the desired source from the menu of buttons that appear for light sources in the actor's inventory or spell list. + +Despite all that we have added, this module continues to follow the philosophy of being controllable in play from a single toggle button on the token HUD, and quickly configurable from there. + +Out of the box, the following are available: + +| System | Sources | +|--------|---------| +| dnd5e | Candle, Torch, Lamp, Bullseye Lantern, Hooded Lantern, Light, Dancing Lights +| swade | Candle, Flashlight, Lantern, Torch +| pf1 | Candle, Lamp, Lantern, Bullseye Lantern, Hooded Lantern, Miner's Lantern, Torch +| pf2e | Candle, Lantern (Hooded), Lantern (Bull's Eye), Torch +| earthdawn4e | Candle, Lantern (Hooded), Lantern (Bullseye), Torch +| gurps | "Candle, Tallow", "Flashlight, Heavy", "Mini Flashlight", "Micro Flashlight", "Survival Flashlight", "Lantern", "Torch", "Bull's-Eye Lantern", "Electric Lantern, Small", "Electric Lantern, Large", "Small Tactical Light", "Large Tactical Light", "Floodlight" + +This module just sheds light from the location of a player token upon demand based upon equipment inventory. It is recommended *not* to use this module for spells or equipment that have other capabilities you intend to use, like performing damage or setting down the equipment, but to rely upon other common approaches, like active effects or item piles, for those. + +Because the light source to use is now user-configurable, we no longer select a light source for you based on fallbacks. As it stands, if you do not explicitly select your light source, it will pick any among the light sources you have equipped, in no particular order. +## Customizing light sources + +You can supersede these settings or supply settings for your own light sources for any system with a JSON file, which you can deliver through the "Additional Light Sources" setting. +```json +{ + "dnd5e": { + "system": "dnd5e", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Candle": { + "name": "Candle", + "type": "equipment", + "consumable": true, + "states": 2, + "light": [ + { + "bright": 10, "dim": 15, "angle": 360, "color": "#ff9329", "alpha": 0.5, + "animation": { "type": "torch", "speed": 5, "intensity": 5, "reverse": false } + } + ] + } + } + }, + ... +} +``` +The key in the top-level hash is the id of the system. + +We support two `topology` values at present: +* In `standard` topology, equipment are `Item` objects with a property to track how many you have. Set `quantity` to the property name used by the system to count the inventory of the item. +* In `gurps` topology, light sources are collected under a property of the actor and require GURPS-specific functions to manipulate inventory. Set `quantity` to `amount`. + +A source with `"consumable": false`, like a lamp or a cantrip, doesn't deduct one from inventory with every use. A source with `"consumable": true`, like a torch or a candle, will have a quantity that is reduced as you use them and will become unavailable when the quantity drops to zero. If you find tracking inventory a complete distraction from your game, you can turn this feature off using the "GM Uses Inventory" and "Player Uses Inventory" settings. + +`states` specifies how many states the light source toggles through. For on/off sources, `states` is 2. For high/low/off sources, like hooded lanterns, states is 3. + +The `light` array contains a hash of light properties for each "on" state, so most things will have one hash between the brackets, but a hooded lantern will have two. The hash can contain any combination of valid token light properties, providing the same degree of configurability as the token's Light tab. + +To use this module with game systems using different topologies will require code but very little code. Define the new topology in topology.js. PRs gratefully accepted. :) ## Changelog - now in [separate file](./CHANGELOG.md) @@ -16,20 +69,24 @@ The following is the current status of translation. Some features have arrived, | Language | Completion | Contributors | | -------- | ---------- | ------------ | -| en | `[##########]` 16/16 (100%) | deuce | -| zh-cn | `[#######---]` 12/16 (75%) | xticime | -| es | `[##########]` 14/16 (100%) | lozalojo | -| fr | `[#########-]` 14/16 (87%) | Aymeeric | -| pt-br | `[##########]` 16/16 (100%) | rinnocenti | -| zh-tw | `[##########]` 16/16 (100%) | zeteticl | +| en | `[##################]` 18/18 (100%) | deuce, lupestro | +| zh-cn | `[##########--------]` 10/18 (56%) | xticime | +| es | `[############------]` 12/18 (67%) | lozalojo | +| fr | `[##########--------]` 10/18 (56%) | Aymeeric | +| pt-br | `[############------]` 12/18 (67%) | rinnocenti | +| zh-tw | `[############------]` 12/18 (67%) | zeteticl | PRs for further translations will be dealt with promptly. While German, Japanese, and Korean are most especially desired - our translation story seems deeply incomplete without them - all others are welcome. -It's only 16 strings so far, a satisfying afternoon, even for someone who's never committed to an open source project before, and your name will go into the readme right here next to the language. Fork, clone, update, _test locally_, commit, and then submit a PR. Holler for @lupestro on Discord if you need help getting started. +It's only 18 strings so far, a satisfying afternoon, even for someone who's never committed to an open source project before, and your name will go into the readme right here next to the language. Fork, clone, update, _test locally_, commit, and then submit a PR. Holler for @lupestro on Discord if you need help getting started. ## History -This module was originally written by @Deuce. After it sustained several months of inactivity in 2021, @lupestro submitted a PR to get its features working reliably in FoundryVTT 0.8. Deuce agreed to transfer control to the League with @lupestro as maintainer, the changes were committed and a release was made from the League fork. All the PRs that were open at the time of the transfer are now committed and attention has turned to fulfilling some of the feature requests in a maintainable way while retaining the "one-button" character of the original module. +This module was originally written by @Deuce. After it sustained several months of inactivity in 2021, @lupestro submitted a PR to get its features working reliably in FoundryVTT 0.8. Deuce agreed to transfer control to the League with @lupestro as maintainer, the changes were committed and a release was made from the League fork. + +All the PRs that were open at the time of the transfer are now committed and attention has turned to fulfilling some of the feature requests in a maintainable way while retaining the "one-button" character of the original module. + +With Foundry VTT V10, the module code was rewritten, broken into several JS files to prepare for implementation of the most asked for features, still behind the single button, supported by a couple of new settings. This mechanism is now complete, pending possible requests for additional equipment topologies. ## License diff --git a/lang/cn.json b/lang/cn.json index 120a8f0..5c5c0da 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -16,5 +16,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/lang/en.json b/lang/en.json index 56642b6..e50ff3a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -17,6 +17,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" - + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/lang/es.json b/lang/es.json index 7df46dc..6f37afd 100644 --- a/lang/es.json +++ b/lang/es.json @@ -17,5 +17,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/lang/fr.json b/lang/fr.json index e743562..19b9902 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -17,5 +17,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 9fb4b8f..8901593 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -16,5 +16,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/lang/zh-TW.json b/lang/zh-TW.json index e4be284..9a66d3c 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -16,5 +16,7 @@ "torch.playerUsesInventory.name": "Player Uses Inventory", "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied" + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" } diff --git a/module.json b/module.json index 8b9a5e3..5f4c96d 100644 --- a/module.json +++ b/module.json @@ -1,5 +1,6 @@ { "id": "torch", + "name": "torch", "title": "Torch", "description": "Torch HUD Controls", "version": "2.0.0", @@ -43,9 +44,9 @@ "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", "url": "https://github.com/League-of-Foundry-Developers/torch", + "minimumCoreVersion": 10, "compatibility": { "minimum": 10, - "verified": 10, - "maximum": 10 + "verified": 10 } } diff --git a/settings.js b/settings.js index 5c8d24c..f4a4cba 100644 --- a/settings.js +++ b/settings.js @@ -90,7 +90,7 @@ export default class Settings { type: String, }); game.settings.register("torch", "brightRadius", { - name: game.i18n.localize("LIGHT.Bright"), + name: game.i18n.localize("torch.brightRadius.name"), hint: game.i18n.localize("torch.brightRadius.hint"), scope: "world", config: true, @@ -98,7 +98,7 @@ export default class Settings { type: Number, }); game.settings.register("torch", "dimRadius", { - name: game.i18n.localize("LIGHT.Dim"), + name: game.i18n.localize("torch.dimRadius.name"), hint: game.i18n.localize("torch.dimRadius.hint"), scope: "world", config: true, From 9a0d5d4cfc662dedd9dfd79d915a6ec530fc13d7 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 23 Jul 2022 13:08:58 -0400 Subject: [PATCH 15/26] First release for v10 --- torch.zip | Bin 12932 -> 22738 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/torch.zip b/torch.zip index 0d235004ba0bf23afea772272362c5f0b7f29b20..5ec69e4eaf84079c8f45d99a8a9247e706c1702c 100644 GIT binary patch literal 22738 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 literal 12932 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 S008~@tp7~+wbA}|_WuBOZ8b^& From d06fdb2c8e66cc9f9af1d9b2054fe880caf5dbe9 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 23 Jul 2022 17:29:23 -0400 Subject: [PATCH 16/26] Context menu wasn't coming up after exhausing sources --- CHANGELOG.md | 5 ++++- hud.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fbefa..d2f996f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## Middle Kingdom - v10 branch -### 2.0.0 - [TBD] +### 2.0.1 - July 24, 2022 + - [BUGFIX] Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. + +### 2.0.0 - July 23, 2022 - [BREAKING] (Lupestro) This release supports v10 of Foundry, and not v0.7, v0.8, or v9. - [FEATURE] (Lupestro) Now supports selection among a variety of light sources on right-click menu from torch button - a long time coming, I know. * Bullseye Lantern has cone beam on token facing. diff --git a/hud.js b/hud.js index 88775c4..1043bf3 100644 --- a/hud.js +++ b/hud.js @@ -32,7 +32,7 @@ export default class TokenHUD { tbutton.addClass("fa-stack"); tbutton.find("i").addClass("fa-stack-1x"); disabledIcon.addClass("fa-stack-1x"); - tbutton.append(disabledIcon); + tbutton.prepend(disabledIcon); } hudHtml.find(".col.left").prepend(tbutton); tbutton.find("i").contextmenu(async (event) => { @@ -100,7 +100,7 @@ export default class TokenHUD { tbutton.addClass("fa-stack"); tbutton.find("i").addClass("fa-stack-1x"); disabledIcon.addClass("fa-stack-1x"); - tbutton.append(disabledIcon); + tbutton.prepend(disabledIcon); } else if (wasDisabled && !willBeDisabled) { oldSlash.remove(); tbutton.find("i").removeClass("fa-stack-1x"); From 189901baac21d43c706040f8f25e55b236d5eeeb Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 23 Jul 2022 18:09:44 -0400 Subject: [PATCH 17/26] Making Dancing Lights respond to configured settings. --- CHANGELOG.md | 1 + request.js | 29 ++++++++++++++++------------- socket.js | 5 +++-- token.js | 8 ++++---- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f996f..e705713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 2.0.1 - July 24, 2022 - [BUGFIX] Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. + - [BUGFIX] Making Dancing Lights respond to configured settings. ### 2.0.0 - July 23, 2022 - [BREAKING] (Lupestro) This release supports v10 of Foundry, and not v0.7, v0.8, or v9. diff --git a/request.js b/request.js index b48a323..3970c77 100644 --- a/request.js +++ b/request.js @@ -27,13 +27,13 @@ export default class TorchRequest { return requestType in TorchRequest.ACTIONS; } - static perform(requestType, scene, token) { - TorchRequest.ACTIONS[requestType](scene, token); + static perform(requestType, scene, token, lightSettings) { + TorchRequest.ACTIONS[requestType](scene, token, lightSettings); } // Dancing lights - static async createDancingLights(scene, token) { + static async createDancingLights(scene, token, lightSettings) { let v = game.settings.get("torch", "dancingLightVision"); let dancingLight = { actorData: {}, @@ -41,26 +41,29 @@ export default class TorchRequest { 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, + light: lightSettings, lockRotation: false, name: "Dancing Light", randomimg: false, rotation: 0, - scale: 0.25, + sight: { + bright: 0, + dim: 0, + angle: 360 + }, + texture: { + src: "systems/dnd5e/icons/spells/light-air-fire-1.jpg", + scaleX: 0.25, + scaleY: 0.25, + rotation: 0 + }, mirrorX: false, - sightAngle: 360, vision: v, width: 1, }; @@ -79,7 +82,7 @@ export default class TorchRequest { }); } - static async removeDancingLights(scene, reqToken) { + static async removeDancingLights(scene, reqToken, lightSettings) { let dltoks = []; scene.tokens.forEach((token) => { // If the token is a dancing light owned by this actor diff --git a/socket.js b/socket.js index 0efffcf..581125b 100644 --- a/socket.js +++ b/socket.js @@ -9,7 +9,7 @@ export default class TorchSocket { let scene = game.scenes.get(req.sceneId); let token = scene.tokens.get(req.tokenId); if (TorchRequest.supports(req.requestType)) { - await TorchRequest.perform(req.requestType, scene, token); + await TorchRequest.perform(req.requestType, scene, token, req.lightSettings); } else { console.warning( `Torch | --- Attempted unregistered socket action ${req.requestType}` @@ -29,11 +29,12 @@ export default class TorchSocket { * Send a request to a user permitted to perform the operation or * (if you are permitted) perform it yourself. */ - static async sendRequest(tokenId, action, lightSource) { + static async sendRequest(tokenId, action, lightSource, lightSettings) { let req = { requestType: `${action}:${lightSource}`, sceneId: canvas.scene.id, tokenId: tokenId, + lightSettings: lightSettings }; if (TorchRequest.supports(req.requestType)) { diff --git a/token.js b/token.js index b166bac..f443654 100644 --- a/token.js +++ b/token.js @@ -119,14 +119,14 @@ export default class TorchToken { // Private internal methods async _turnOffSource() { + let source = this._library.getLightSource(this.currentLightSource); if (TorchSocket.requestSupported("delete", this.currentLightSource)) { // separate token lighting - TorchSocket.sendRequest(this._token.id, "delete", this.currentLightSource); + TorchSocket.sendRequest(this._token.id, "delete", this.currentLightSource, source.light); } else { // self lighting - to turn off, use light settings from prototype token let protoToken = game.actors.get(this._token.actorId).prototypeToken; await this._token.update(getLightUpdates(protoToken.light)); - let source = this._library.getLightSource(this.currentLightSource); if (source.consumable) { await this._consumeSource(source); } @@ -134,12 +134,12 @@ export default class TorchToken { } async _turnOnSource() { + let source = this._library.getLightSource(this.currentLightSource); if (TorchSocket.requestSupported("create", this.currentLightSource)) { // separate token lighting - TorchSocket.sendRequest(this._token.id, "create", this.currentLightSource); + TorchSocket.sendRequest(this._token.id, "create", this.currentLightSource, source.light[0]); } else { // self lighting - let source = this._library.getLightSource(this.currentLightSource); await this._token.update(getLightUpdates(source.light[0])); } } From 42ddf2ee8a63848f88f27293346c908017a14211 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sat, 23 Jul 2022 18:26:57 -0400 Subject: [PATCH 18/26] Adding German translation, adjusted for implementation changes in new version. --- CHANGELOG.md | 3 ++- README.md | 1 + lang/de.json | 23 +++++++++++++++++++++++ module.json | 5 +++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 lang/de.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e705713..d74fe02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,10 @@ ## Middle Kingdom - v10 branch -### 2.0.1 - July 24, 2022 +### 2.1.0 - July 24, 2022 - [BUGFIX] Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. - [BUGFIX] Making Dancing Lights respond to configured settings. + - [FEATURE] Added (partial) German translation. ### 2.0.0 - July 23, 2022 - [BREAKING] (Lupestro) This release supports v10 of Foundry, and not v0.7, v0.8, or v9. diff --git a/README.md b/README.md index 8b4c970..48d0be0 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ The following is the current status of translation. Some features have arrived, | fr | `[##########--------]` 10/18 (56%) | Aymeeric | | pt-br | `[############------]` 12/18 (67%) | rinnocenti | | zh-tw | `[############------]` 12/18 (67%) | zeteticl | +| de | `[############------]` 12/18 (67%) | ToGreedy | PRs for further translations will be dealt with promptly. While German, Japanese, and Korean are most especially desired - our translation story seems deeply incomplete without them - all others are welcome. diff --git a/lang/de.json b/lang/de.json new file mode 100644 index 0000000..46cbb84 --- /dev/null +++ b/lang/de.json @@ -0,0 +1,23 @@ +{ + "I18N.LANGUAGE": "Deutsch", + "I18N.MAINTAINERS": ["ToGreedy"], + + "torch.playerTorches.name": "Spieler Fackeln", + "torch.playerTorches.hint": "Erlaubt es Spielern, ihre eigenen Fackeln umzuschalten.", + "torch.brightRadius.hint": "Wie viele Rastereinheiten helles Licht sollen ausgestrahlt werden.", + "torch.dimRadius.hint": "Wie viele Rastereinheiten gedämpftes Licht sollen ausgestrahlt werden.", + "torch.gmUsesInventory.name": "GM verwendet Inventar", + "torch.gmUsesInventory.hint": "Wenn diese Option gesetzt ist, verwendet der GM beim Umschalten einer Fackel das Inventar des Spielers.", + "torch.gmInventoryItemName.name": "Name des Inventargegenstandes", + "torch.gmInventoryItemName.hint": "Name des zu verwendenden Inventargegenstandes, wenn \"GM verwendet Inventar\" eingestellt ist.", + "torch.turnOffAllLights": "Erzwingen Sie Hell- und Dimmwerte für die konfigurierten Werte \"Aus\".", + "torch.holdCtrlOnClick": "Halte Ctrl beim Klick", + "torch.dancingLightVision.name": "Geben Sie \"Dancing Lights\" sicht.", + "torch.dancingLightVision.hint": "Wenn diese Option gesetzt ist, hat jedes \"Dancing Light\" sicht und kann dazu verwendet werden, Bereiche der Karte aufzudecken, die Spieler nicht sehen können.", + "torch.playerUsesInventory.name": "Player Uses Inventory", + "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", + "torch.gameLightSources.name": "Additional Light Sources", + "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", + "torch.brightRadius.name": "Bright Light Radius", + "torch.dimRadius.name": "Dim Light Radius" +} \ No newline at end of file diff --git a/module.json b/module.json index 5f4c96d..bdc96f1 100644 --- a/module.json +++ b/module.json @@ -16,6 +16,11 @@ "name": "中文", "path": "./lang/cn.json" }, + { + "lang": "de", + "name": "Deutsch", + "path": "./lang/de.json" + }, { "lang": "fr", "name": "Français", From 4cfa166f325377632ec05c91918b6948d25baec0 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sun, 24 Jul 2022 10:13:34 -0400 Subject: [PATCH 19/26] German translation now complete for v10 --- CHANGELOG.md | 6 +++--- README.md | 2 +- lang/de.json | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d74fe02..eea0f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,9 @@ ## Middle Kingdom - v10 branch ### 2.1.0 - July 24, 2022 - - [BUGFIX] Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. - - [BUGFIX] Making Dancing Lights respond to configured settings. - - [FEATURE] Added (partial) German translation. + - [BUGFIX] (Lupestro) Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. + - [BUGFIX] (Lupestro) Making Dancing Lights respond to configured settings. + - [FEATURE] (ToGreedy) German translation. ### 2.0.0 - July 23, 2022 - [BREAKING] (Lupestro) This release supports v10 of Foundry, and not v0.7, v0.8, or v9. diff --git a/README.md b/README.md index 48d0be0..ed9295f 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ The following is the current status of translation. Some features have arrived, | fr | `[##########--------]` 10/18 (56%) | Aymeeric | | pt-br | `[############------]` 12/18 (67%) | rinnocenti | | zh-tw | `[############------]` 12/18 (67%) | zeteticl | -| de | `[############------]` 12/18 (67%) | ToGreedy | +| de | `[##################]` 18/18 (100%) | ToGreedy | PRs for further translations will be dealt with promptly. While German, Japanese, and Korean are most especially desired - our translation story seems deeply incomplete without them - all others are welcome. diff --git a/lang/de.json b/lang/de.json index 46cbb84..fa56607 100644 --- a/lang/de.json +++ b/lang/de.json @@ -14,10 +14,10 @@ "torch.holdCtrlOnClick": "Halte Ctrl beim Klick", "torch.dancingLightVision.name": "Geben Sie \"Dancing Lights\" sicht.", "torch.dancingLightVision.hint": "Wenn diese Option gesetzt ist, hat jedes \"Dancing Light\" sicht und kann dazu verwendet werden, Bereiche der Karte aufzudecken, die Spieler nicht sehen können.", - "torch.playerUsesInventory.name": "Player Uses Inventory", - "torch.playerUsesInventory.hint": "If set, when the player toggles a torch, it will use the actors inventory.", - "torch.gameLightSources.name": "Additional Light Sources", - "torch.gameLightSources.hint": "JSON file containing additional light sources to use beyond those supplied", - "torch.brightRadius.name": "Bright Light Radius", - "torch.dimRadius.name": "Dim Light Radius" + "torch.playerUsesInventory.name": "Spieler verwendet Inventar", + "torch.playerUsesInventory.hint": "Wenn diese Option gesetzt ist, verwendet der Spieler beim Umschalten einer Fackel sein Inventar.", + "torch.gameLightSources.name": "Zusätzliche Lichtquellen", + "torch.gameLightSources.hint": "JSON Datei mit zusätzlichen Lichtquellen, die über die mitgelieferten hinaus verwendet werden sollen", + "torch.brightRadius.name": "Radius des hellen Licht", + "torch.dimRadius.name": "Radius des gedämpften Licht" } \ No newline at end of file From a9ce7f2cbef8796a1b5b0baf931b791dae14f6a4 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Sun, 24 Jul 2022 10:57:41 -0400 Subject: [PATCH 20/26] Tweak to fix for context menu on disabled sources --- hud.js | 2 +- module.json | 2 +- torch.zip | Bin 22738 -> 24150 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hud.js b/hud.js index 1043bf3..97f6e36 100644 --- a/hud.js +++ b/hud.js @@ -35,7 +35,7 @@ export default class TokenHUD { tbutton.prepend(disabledIcon); } hudHtml.find(".col.left").prepend(tbutton); - tbutton.find("i").contextmenu(async (event) => { + tbutton.find("i.fa-fire").contextmenu(async (event) => { event.preventDefault(); event.stopPropagation(); if (token.lightSourceState === token.STATE_OFF) { diff --git a/module.json b/module.json index bdc96f1..757e2fa 100644 --- a/module.json +++ b/module.json @@ -3,7 +3,7 @@ "name": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "2.0.0", + "version": "2.1.0", "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], "languages": [ { diff --git a/torch.zip b/torch.zip index 5ec69e4eaf84079c8f45d99a8a9247e706c1702c..37d6730f23ff4e828036de2a5bfabf0cd108fcae 100644 GIT binary patch delta 12297 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 I|5_mb2mcx%sQ>@~ From 4ae7158f8e5396d5dcd7efb47fd24d4d98f90fb8 Mon Sep 17 00:00:00 2001 From: Lupestro Date: Tue, 6 Sep 2022 18:42:38 -0400 Subject: [PATCH 21/26] Fixes for unlinked tokens and GURPS consumability --- CHANGELOG.md | 5 +++ library.js | 26 ++++++------- module.json | 9 +++-- sources.json | 26 ++++++------- test/common-library-tests.js | 48 ++++++++++++------------ test/common-token-tests.js | 2 +- token.js | 8 ++-- topology.js | 70 ++++++++++++++++++----------------- torch.zip | Bin 24150 -> 23860 bytes 9 files changed, 102 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea0f64..1c7814d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Middle Kingdom - v10 branch +### 2.1.1 - September 6, 2022 + - [BUGFIX] (Lupestro) Fixed issue where unlinked tokens were adjusting inventory on source token. + - [BUGFIX] (Lupestro) Fixed designation of which stock light sources are consumable in GURPS + - [BUGFIX] (Lupestro) Adjusted manifest to use strings for minimumCoreVersion and compatibility values. + ### 2.1.0 - July 24, 2022 - [BUGFIX] (Lupestro) Context menu wasn't opening in HUD after exhausting a source - had to reopen HUD. - [BUGFIX] (Lupestro) Making Dancing Lights respond to configured settings. diff --git a/library.js b/library.js index 23f0958..c5bc950 100644 --- a/library.js +++ b/library.js @@ -65,32 +65,32 @@ export default class SourceLibrary { } return; } - getInventory(actorId, lightSourceName) { + getInventory(actor, lightSourceName) { let source = this.getLightSource(lightSourceName); - return this.library.topology.getInventory(actorId, source); + return this.library.topology.getInventory(actor, source); } - async _presetInventory(actorId, lightSourceName, quantity) { // For testing + async _presetInventory(actor, lightSourceName, quantity) { // For testing let source = this.getLightSource(lightSourceName); - return this.library.topology.setInventory(actorId, source, quantity); + return this.library.topology.setInventory(actor, source, quantity); } - async decrementInventory(actorId, lightSourceName) { + async decrementInventory(actor, lightSourceName) { let source = this.getLightSource(lightSourceName); - return this.library.topology.decrementInventory(actorId, source); + return this.library.topology.decrementInventory(actor, source); } - getImage(actorId, lightSourceName) { + getImage(actor, lightSourceName) { let source = this.getLightSource(lightSourceName); - return this.library.topology.getImage(actorId, source); + return this.library.topology.getImage(actor, source); } - actorHasLightSource(actorId, lightSourceName) { + actorHasLightSource(actor, lightSourceName) { let source = this.getLightSource(lightSourceName); - return this.library.topology.actorHasLightSource(actorId, source); + return this.library.topology.actorHasLightSource(actor, source); } - actorLightSources(actorId) { + actorLightSources(actor) { let result = [] for (let source in this.library.sources) { - if (this.library.topology.actorHasLightSource(actorId, this.library.sources[source])) { + if (this.library.topology.actorHasLightSource(actor, this.library.sources[source])) { let actorSource = Object.assign({ - image: this.library.topology.getImage(actorId, this.library.sources[source]) + image: this.library.topology.getImage(actor, this.library.sources[source]) }, this.library.sources[source]); result.push(actorSource); } diff --git a/module.json b/module.json index 757e2fa..a6aa251 100644 --- a/module.json +++ b/module.json @@ -3,7 +3,7 @@ "name": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "2.1.0", + "version": "2.1.1", "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], "languages": [ { @@ -49,9 +49,10 @@ "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", "url": "https://github.com/League-of-Foundry-Developers/torch", - "minimumCoreVersion": 10, + "minimumCoreVersion": "10", "compatibility": { - "minimum": 10, - "verified": 10 + "minimum": "10", + "verified": "10.284", + "maximum": "10" } } diff --git a/sources.json b/sources.json index 0fbb940..fff8dac 100644 --- a/sources.json +++ b/sources.json @@ -279,7 +279,7 @@ "Candle, Tallow": { "name": "Candle, Tallow", "type": "equipment", - "consumable": "true", + "consumable": true, "states": 2, "light": [ {"bright": 0, "dim": 2, "angle": 360, "color": "#ff9329", "alpha": 0.6 } @@ -288,7 +288,7 @@ "Flashlight, Heavy": { "name": "Flashlight, Heavy", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 30, "dim": 30, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -297,7 +297,7 @@ "Mini Flashlight": { "name": "Mini Flashlight", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 4, "dim": 5, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -306,7 +306,7 @@ "Micro Flashlight": { "name": "Micro Flashlight", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 1, "dim": 1, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -315,7 +315,7 @@ "Survival Flashlight": { "name": "Survival Flashlight", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 1, "dim": 1, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -324,7 +324,7 @@ "Lantern": { "name": "Lantern", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 4, "dim": 5, "angle": 360, "color": "#ff9329", "alpha": 1 } @@ -333,7 +333,7 @@ "Torch": { "name": "Torch", "type": "equipment", - "consumable": "false", + "consumable": true, "states": 2, "light": [ {"bright": 9, "dim": 10, "angle": 360, "color": "#ff9329", "alpha": 1 } @@ -342,7 +342,7 @@ "Bull's-Eye Lantern": { "name": "Bull's-Eye Lantern", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 9, "dim": 10, "angle": 53, "color": "#ff9329", "alpha": 1 } @@ -351,7 +351,7 @@ "Electric Lantern, Small": { "name": "Electric Lantern, Small", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 3, "dim": 3, "angle": 360, "color": "#ff9329", "alpha": 1 } @@ -360,7 +360,7 @@ "Electric Lantern, Large": { "name": "Electric Lantern, Large", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 4, "dim": 5, "angle": 360, "color": "#ff9329", "alpha": 1 } @@ -369,7 +369,7 @@ "Small Tactical Light": { "name": "Small Tactical Light", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 22, "dim": 25, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -378,7 +378,7 @@ "Large Tactical Light": { "name": "Large Tactical Light", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 95, "dim": 100, "angle": 3, "color": "#ffd6aa", "alpha": 1 } @@ -387,7 +387,7 @@ "Floodlight": { "name": "Floodlight", "type": "equipment", - "consumable": "false", + "consumable": false, "states": 2, "light": [ {"bright": 190, "dim": 200, "angle": 3, "color": "#ffd6aa", "alpha": 1 } diff --git a/test/common-library-tests.js b/test/common-library-tests.js index 5da75c7..0c509cd 100644 --- a/test/common-library-tests.js +++ b/test/common-library-tests.js @@ -68,22 +68,22 @@ export let torchCommonLibraryTests = (context) => { let versatile = game.actors.getName("Versatile"); assert.ok(versatile, "Actor Versatile found"); let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); - let torches = library.getInventory(versatile.id, "Torch"); + let torches = library.getInventory(versatile, "Torch"); assert.ok(typeof torches === "number", "Count of torches has a numeric value"); - let candles = library.getInventory(versatile.id, "Candle"); + let candles = library.getInventory(versatile, "Candle"); assert.ok(typeof candles === "number", "Count of candles has a numeric value"); - let lights = library.getInventory(versatile.id, "Light"); + let lights = library.getInventory(versatile, "Light"); assert.ok(typeof lights === "undefined", "Light cantrip doesn't have inventory"); - let lamps = library.getInventory(versatile.id, "Lamp"); + let lamps = library.getInventory(versatile, "Lamp"); assert.ok(typeof lamps === "undefined", "Lamp doesn't have inventory"); - await library._presetInventory(versatile.id, "Torch", 2); - let before = library.getInventory(versatile.id, "Torch"); - await library.decrementInventory(versatile.id, "Torch"); - let afterFirst = library.getInventory(versatile.id, "Torch"); - await library.decrementInventory(versatile.id, "Torch"); - let afterSecond = library.getInventory(versatile.id, "Torch"); - await library.decrementInventory(versatile.id, "Torch"); - let afterThird = library.getInventory(versatile.id, "Torch"); + await library._presetInventory(versatile, "Torch", 2); + let before = library.getInventory(versatile, "Torch"); + await library.decrementInventory(versatile, "Torch"); + let afterFirst = library.getInventory(versatile, "Torch"); + await library.decrementInventory(versatile, "Torch"); + let afterSecond = library.getInventory(versatile, "Torch"); + await library.decrementInventory(versatile, "Torch"); + let afterThird = library.getInventory(versatile, "Torch"); assert.equal(before, 2, "Started with set value"); assert.equal(afterFirst, 1, "Decremented to one"); assert.equal(afterSecond, 0, "Decremented to zero"); @@ -93,16 +93,16 @@ export let torchCommonLibraryTests = (context) => { let versatile = game.actors.getName("Versatile"); assert.ok(versatile, "Actor Versatile found"); let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); - let torchImage = library.getImage(versatile.id, "Torch"); + let torchImage = library.getImage(versatile, "Torch"); assert.ok(torchImage, "Torch should have a defined image"); assert.notEqual(torchImage,"", "Torch image has a reasonable value"); - let candleImage = library.getImage(versatile.id, "Candle"); + let candleImage = library.getImage(versatile, "Candle"); assert.ok(candleImage, "Candle should have a defined image"); assert.notEqual(candleImage,"", "Candle image has a reasonable value"); - let lampImage = library.getImage(versatile.id, "Lamp"); + let lampImage = library.getImage(versatile, "Lamp"); assert.ok(lampImage, "Lamp should have a defined image"); assert.notEqual(lampImage,"", "Lamp image has a reasonable value"); - let lightImage = library.getImage(versatile.id, "Light"); + let lightImage = library.getImage(versatile, "Light"); assert.ok(lightImage, "Light cantrip should have a defined image"); assert.notEqual(lightImage,"", "Light cantrip image has a reasonable value"); }); @@ -117,25 +117,25 @@ export let torchCommonLibraryTests = (context) => { assert.ok(everythingBut, "Actor Bic found"); let library = await SourceLibrary.load('dnd5e', 50, 10, 'Torch', './userLights.json'); - let breakerSources = library.actorLightSources(breaker.id); + let breakerSources = library.actorLightSources(breaker); assert.equal (breakerSources.length, 2, "Breaker has two known light sources"); assert.notEqual(breakerSources[0].name, breakerSources[1].name, "Breaker's sources are different"); assert.ok (["Torch", "Dancing Lights"].includes(breakerSources[0].name), "Breaker's first source is expected"); assert.ok (["Torch", "Dancing Lights"].includes(breakerSources[1].name), "Breaker's second source is expected"); - assert.equal(library.actorHasLightSource(breaker.id, "Dancing Lights"), true, "Breaker is reported as having Dancing Lights"); + assert.equal(library.actorHasLightSource(breaker, "Dancing Lights"), true, "Breaker is reported as having Dancing Lights"); - let bearerSources = library.actorLightSources(bearer.id); + let bearerSources = library.actorLightSources(bearer); assert.equal(bearerSources.length, 1, "Torchbearer has precisely one light source"); assert.equal(bearerSources[0].name, "Torch", "Torchbearer's light source is Torch, as eqpected"); - assert.equal(library.actorHasLightSource(bearer.id, "Torch"), true, "Bearer is reported as having the Torch light source"); + assert.equal(library.actorHasLightSource(bearer, "Torch"), true, "Bearer is reported as having the Torch light source"); - let emptySources = library.actorLightSources(empty.id); + let emptySources = library.actorLightSources(empty); assert.equal (emptySources.length, 0, "Empty truly has no known light sources"); - assert.equal(library.actorHasLightSource(empty.id, "Candle"), false, "Empty is reported as not having the candle light source"); + assert.equal(library.actorHasLightSource(empty, "Candle"), false, "Empty is reported as not having the candle light source"); - let everythingButSources = library.actorLightSources(everythingBut.id); + let everythingButSources = library.actorLightSources(everythingBut); assert.equal(everythingButSources.length, 0, "Bic has no known light sources, even though it has ways of casting light"); - assert.equal(library.actorHasLightSource(everythingBut.id, "Candle"), false, "Empty is reported as not having the candle light source"); + assert.equal(library.actorHasLightSource(everythingBut, "Candle"), false, "Empty is reported as not having the candle light source"); }); }); } diff --git a/test/common-token-tests.js b/test/common-token-tests.js index 67e38fc..ded6945 100644 --- a/test/common-token-tests.js +++ b/test/common-token-tests.js @@ -6,7 +6,7 @@ let initiateWith = async function(name, item, count, assert) { assert.ok(foundryToken, "Token for "+ name + " found in scene"); let library = await SourceLibrary.load("dnd5e", 10, 20); assert.ok(library, "Library successfully created"); - library._presetInventory(foundryToken.actor.id, item, count); + await library._presetInventory(foundryToken.actor, item, count); return new TorchToken(foundryToken, library); } export let torchCommonTokenTests = (context) => { diff --git a/token.js b/token.js index f443654..2449855 100644 --- a/token.js +++ b/token.js @@ -27,7 +27,7 @@ export default class TorchToken { constructor(token, library) { this._token = token; this._library = library; - this._ownedSources = library.actorLightSources(this._token.actorId); + this._ownedSources = library.actorLightSources(this._token.actor); } get ownedLightSources() { @@ -72,7 +72,7 @@ export default class TorchToken { lightSourceIsExhausted(source) { if (this._library.getLightSource(source).consumable) { - let inventory = this._library.getInventory(this._token.actorId, source); + let inventory = this._library.getInventory(this._token.actor, source); return inventory === 0; } return false; @@ -154,9 +154,9 @@ export default class TorchToken { async _consumeSource(source) { if ((game.user.isGM && Settings.gmUsesInventory) || (!game.user.isGM && Settings.userUsesInventory)) { - let count = this._library.getInventory(this._token.actorId, source.name); + let count = this._library.getInventory(this._token.actor, source.name); if (count) { - await this._library.decrementInventory(this._token.actorId, source.name); + await this._library.decrementInventory(this._token.actor, source.name); } } } diff --git a/topology.js b/topology.js index a7294d3..4092942 100644 --- a/topology.js +++ b/topology.js @@ -19,30 +19,30 @@ class StandardLightTopology { constructor(quantityField) { this.quantityField = quantityField ?? "quantity"; } - _findMatchingItem(actorId, lightSourceName) { - return Array.from(game.actors.get(actorId).items).find( + _findMatchingItem(actor, lightSourceName) { + return Array.from(actor.items).find( (item) => item.name.toLowerCase() === lightSourceName.toLowerCase() ); } - actorHasLightSource(actorId, lightSource) { - return !!this._findMatchingItem(actorId, lightSource.name); + actorHasLightSource(actor, lightSource) { + return !!this._findMatchingItem(actor, lightSource.name); } - getImage (actorId, lightSource) { - let item = this._findMatchingItem(actorId, lightSource.name); + getImage (actor, lightSource) { + let item = this._findMatchingItem(actor, lightSource.name); return item ? item.img : DEFAULT_IMAGE_URL; } - getInventory (actorId, lightSource) { + getInventory (actor, lightSource) { if (!lightSource.consumable) return; - let item = this._findMatchingItem(actorId, lightSource.name); + let item = this._findMatchingItem(actor, lightSource.name); return item ? item.system[this.quantityField] : undefined; } - async decrementInventory (actorId, lightSource) { - if (!lightSource.consumable) return; - let item = this._findMatchingItem(actorId, lightSource.name); + async decrementInventory (actor, lightSource) { + if (!lightSource.consumable) return Promise.resolve(); + let item = this._findMatchingItem(actor, lightSource.name); if (item && item.system[this.quantityField] > 0) { let fieldsToUpdate = {}; fieldsToUpdate["system." + this.quantityField] = item.system[this.quantityField] - 1; @@ -51,9 +51,9 @@ class StandardLightTopology { return Promise.resolve(); } } - async setInventory (actorId, lightSource, count) { - if (!lightSource.consumable) return; - let item = this._findMatchingItem(actorId, lightSource.name); + async setInventory (actor, lightSource, count) { + if (!lightSource.consumable) return Promise.resolve(); + let item = this._findMatchingItem(actor, lightSource.name); let fieldsToUpdate = {}; fieldsToUpdate["system." + this.quantityField] = count; return item.update(fieldsToUpdate); @@ -67,49 +67,53 @@ class StandardLightTopology { this.quantityField = quantityField ?? "quantity"; } - _findMatchingItem(actorId, lightSourceName) { - let actor = game.actors.get(actorId); - return actor.findEquipmentByName(lightSourceName); - } - - actorHasLightSource(actorId, lightSource) { - let [item] = this._findMatchingItem (actorId, lightSource.name); + actorHasLightSource(actor, lightSource) { + let [item] = actor.findEquipmentByName(lightSource.name); return !!item; } - getImage (/*actorId, lightSource*/) { + getImage (/*actor, lightSource*/) { // We always use the same image because the system doesn't supply them return DEFAULT_IMAGE_URL; } - getInventory (actorId, lightSource) { - let [item] = this._findMatchingItem (actorId, lightSource.name); + getInventory (actor, lightSource) { + let [item] = actor.findEquipmentByName(lightSource.name); return item ? item.count : undefined; } - async decrementInventory (actorId, lightSource) { - let [item, key] = this._findMatchingItem (actorId, lightSource.name); + async decrementInventory (actor, lightSource) { + if (!lightSource.consumable) return Promise.resolve(); + let [item, key] = actor.findEquipmentByName(lightSource.name); if (item && item.count > 0) { - game.actors.get(actorId).updateEqtCount(key, item.count - 1 ); - } else { - return Promise.resolve(); + actor.updateEqtCount(key, item.count - 1); } + return Promise.resolve(); + } + + async setInventory (actor, lightSource, count) { + if (!lightSource.consumable) return Promise.resolve(); + let [item, key] = actor.findEquipmentByName(lightSource.name); + if (item) { + actor.updateEqtCount(key, count); + } + return Promise.resolve(); } } class DefaultLightTopology { quantityField = "quantity"; constructor(/*quantityField*/) {} - actorHasLightSource(actorId, lightSource) { + actorHasLightSource(actor, lightSource) { return lightSource.name === "Self"; } - getImage (actorId, lightSource) { + getImage (actor, lightSource) { return DEFAULT_IMAGE_URL; } - getInventory (actorId, lightSource) { + getInventory (actor, lightSource) { return 1; } - async decrementInventory (actorId, lightSource) { + async decrementInventory (actor, lightSource) { return Promise.resolve(); } } diff --git a/torch.zip b/torch.zip index 37d6730f23ff4e828036de2a5bfabf0cd108fcae..4af0c77ab3f31c8996e82d96983d0d57e9153817 100644 GIT binary patch delta 7983 zcmZX3Wl$a6()BsGyF+kycL?ql+z#$efM9_M79{wA;BLX)2`&ll?(PyS!SeCkTVK_C z@13fynIF@8_w1?eUcJ_Q2e_dPjG>_n3x@*w=R!%Q*Ga%2MbaK6wEV|vxun48KjNRX zK|hhoBVa)wd*~-HIieoYj)gwb4zvhN4ag|PwXc+gpO${FyxR^Dx>&jv-PzrP0rk;jheWen_OdTpXAU5vEPCy6Dtq9_QDH0?oJZg) zh$?)qyxy@HN{^m=9YqWhBC7^^6{Rk{}4qTx{Mv59uSR!;0@(|$q0^iz3e1SDv& zvs@Imc|TzeoZ+NX_Y8H~Xo@c4L@%1U;@CVIWq^FZgRqsSu|GdO3cVbK283EcR+qEY zyB)=LO>d99{4ICH3(w_ZzImb}{79dTuz_H)nPZiL@v{pqD=1?((Cmgw?DjoDH#`HU zN5nA7O%m924sQPMc3_Ve4Ktg~fkP_wIL)Xd!h$t$!I<8`M^~?O^pM}F?AKita^HA<11A6W)I;n#{4Gs8%8hp{P-DN zd7z?Exx!^{)r`p1%|jgN#rx}9$>;R>=)jrjh;@mmajG$&?B<;E1Ra8*JmBX>A#1Fz zmJY_j{zQ6@+M^Cv)v<~}{UVgzZ7{ProqBJFFq3TGSi{({PRlK}ZC&(P<>d-F-_%k} zPRP!a@^(gWIY3T}oaOTLjQeYyHtT7QS5AWCCBZOSXEM_hgk?(fI7?*s1F=i6iT5VT zd-hG%vuYmC2r{l;1*K2E!vJhB?~>1D+Gr9pRa<)$JX&vq+>3cpuefQEofD@K)4)5D zw#wUOBw1pCR23w9>qeI@c@~_u*7YIS&=gvtlO#FOFYu*|Z%`5z-DgpzOej^{Fu5>t zyrkX@p>YsRGbN?HMGfcuOK+-M9_GvzEeaz~ni}`WwLtFvD})hul^Xb+RBEt@Nh*Ey zeFdbf;^ToNOJ7WldD{z3$D$w;ZiR-(xy|q0j0P+B5%4O!>w6R7wsn*o-kv1>M;(iz zPzvAYgH!b5(>E$YjItf`)q*uQ!kEXi46YQ&?4DOf2p^M64gRD=I8F7_xs$QwF8!=` zGm)wrVU3}Ls-o7VTRUK_Rp1U|@-@DSbh)y4lSez+qSv7HkiK(-AAT%ud-%uJ1NN7c z)Ms710qtfB8DgQ#;s`+JxOujtr$T7qMcnu?oQ00SB77M&uyo;i5~Vm*i}Ho9>OIqg z7!FRu!g-^pSf{klTzqt^d{5(L!_1^;xj|E`b0&ug#|!Lh;*!AOz2+?vXg-Nvr%0XC z91j@;O2-F*a6n{Gus%M341&EGA+*GQO=d42R1g^M1`GnhS-twi=r6rr5hMt>*E3_D zOKwrtRDPe?f6b}2zSD5l(xKu#H!CVZ%Sep?CLkEOS(s=ADYDgC-b)+O2l->3zdr|W z)b=Hww3;2*bT;0eziE&6t%7^hI`(y8=C{Ixa%C7))hDiZ0hTjUR=hper0@LSjfGn0 zAUFwUt2Gm4zua%0-|U`V{H?`2$RijxsvlF^E%=e<$&S>-7>3VkM|Uem?&rbLV2SGY zz!_qVm?TcooX=hUguG&7zA-B^>W1XPBDbpObJ;!iwj$)mo|H6FTyBG(w9!7UD+ z<*=v;H^b7UBVg@zZtTncJq^Flx0bK)}1YTYgH7DFhO26T-TQeJiyzvF6RCET+5 z)OBE|RhW-R@49ucVlk%WJis{-g(WiQ>gG&pcuUC68+R z)bJ9b!`fk_VZ69>K)eD{8UMncDk7)Q*{G-;$RL|VcD+I0N8}n}x7wnTi#C7VD}+2h zEWr}$Wh}=#yM+D`lFBV^$#&5?$w;Eg)OKFTgXP<1FI9V^#^elH2dB{ds-0kXSKYHG zctCxARDuoNh{$2Om2!}ilJVWn=plt3suf9)orUz4TVPO?6ij7BB(^`}ck{9pBi4=w zzxdcvEaM?@o{+Pq`j+@knN>$xqX&XkMwHn)Bff@e?VIIuoH*j&qBzmBK`bBgeUi&| zA_RUaMY-P0k%VT2ZRUo8M|=kk6bdehvw;U3u#1XMPDnBDKpPATb+DHryd)nyJD;yc z|0&WJDZf)Y{7utY8p%DoW*BnhljH#mD@t=m%YjKsvPEq!&tH-0l)-1~nu02qDlnBC zYDSLsr+6%O?ejjpYiPW-2XJ0ho)#EXX-L|}Kf1c7gYG~Lfe_KphmS)n7l~JD1 zLhc}oz+u#|rM`Cvnmx~tv?7xWdeA%8iEH!|ygN>gt*w=@du3e(I^k~Hl;)v@l*a?d zh&&c`b#32o;tn**$wX~(dAzH>3k1es7=|?* z8VEw7tnihZf(My`^P>a5e-|9WmL*3fR#WN>m_F}lVVEW!#qP!8O~VU66pF@iW)x}>N(qDKn>&w+8Gxf}a#4~&vZ0Bo zjLXhRGg#VzQZtQ*R0oA%T7;Ba+qqoNqmFQZBIr8T=iTkpb*)D})=Ij3CKayxBJL$Vh1P-({ zdq?dj9#OU=4`%oU{^3vokyRV-DXa19;XKm{6UM~Nk2oe0rogwuy1Tx#XJ7REJXN_} zNj72PIN(b?``WIYnOfBp@tA*f7;r;0BsRFF$CZ6Fs*kpcgr`^e4bi1F6tCBdH0dOf zOt9t&Sv#miFLm6U{BQup_t=hXYRXkc1#X{5l9vKJNdFsHlM_KDl8cWabXno| z+b`&uf_xW{8(=6W+K8}EfHty>Sg(4jL`F1s*~dKT4I(~Ufed-Cs`E{%&8W9R!5(2m z%+hqz1l)}$XUK|tPyI=1f1tc-&i&0z|($6%*w(SfpTyx zQ4vHuzxl&`mf7iRtF2!(mHqZZ27~MJM`AvQr_rFt@(cbr)5WK;TzkVUNFJuzm!t6ToH}isrDj+icv4ALSNt++BXI-QvwaNX7mZ?xpKK_;zvGj!vz>*s~Hz+n6 z(n*vf{37LalBqoUF}V;m#aN|_k;7)3Od>iv3A>FBGZhQt#FP^mU(ZX2|BD<$dgUgs z5%B=-fJDIzrCA@e`>n@Qab0l6{T zwACS2-Kb$b_&RIQis=UF^~9JDRtD3XBaHA|kgLW0-s8D9%HB~#9-&e+Xq8_}((~CM zGHH_@$H8!#;O@w$cjjlP;d9?8F-UZ%;z8SV%LGE24N&myT33RsL;HhWnhG)B2b^&1 z7VkZNk~l3V{uP4P{!Q|ZOYGn#_3$kpi%xMY`eu5ja~RW~fQTD2PHj`Jq;~>sv-*2Y zx&vEsyC>=e)nJjC9Bkn%=fSo^bBBe?83&nR-M@URO~s&d6*1%KWrYvT2DMH~bO!Rw zdmjPh>cGGy)@*)s%Qe8eULj^sY*T1`;iW@;&7UX0VcQ=Mkw@K1@mTSEHP9&S^G!fU zp42Jx<=GAF_U96!MORZ73sgQ5r`o_W58%0RSQ106JJXo?8urcw^aR8)WG%XaA-L9UVqY3+MFX*?zu~dI%(XS z!IsQ1m~STA{K_RSGi<6aeEbJT{q02ZyD)x>il3j5rMH`BGzzpS?rLs*xy^x}I!FP; zLRxS?*Y7C~plbg~*u)f>QVbf`?74eF#1182qYF0)7@-y0`|JI_|BE=_Y8S)JGAp#( zN)EYfT~YCi8rumQ?C4X!^(ZWM3T7@8;}TW{E?>@Z)wQFcx=kE3hSl6zt$PACA7(?E z^2MkZ=f%HRe*9)lf*ro{a#KMsjGqCNL(?P~+>onlYX`U@|FCNbB>OSy+*2PCU!gnf zhcxX$`l{G*lg$eh5rc(pR5Sv120{X^2`y<=$Rv)rh#VT#hLAdg_Q{)=xAWC$3 zpkcT`J&Etwxc?Q)s0=sD9$(zMo|etkh8RK4BtR)IZ8vptO=N|~|9%!^N97jq3$iG6 z_`uuNjUHkPauUx$$^~1lRr7Ov1k*&HX<)*|?&&n04TpN3GuVMxo!1UOzB~!ZJeQI= zoeKv$Q;$E2R2Ir+3=8HTpw0shnZ%GQ46Wie%S`s4rOhAgrRxQBhNSg|S;I8_ma9=e zq25r)EwJjghn@35EY!&yv>WLT{(-O(0WlmUmQnXt@7QTj8XbI=7DE8quO+9 z3FN-IF5pggy5HzAU-X%?fIM4k|50IP`g8WWeCAi-yj>yRwj1QVa3mRhCOn6mgow#+ z;qrMeLQv0AlC&>QZwk6feYN#)X}7xmVXpo?{|YA1>)^acr8tIrWwCHTAPf*0$kX+` zr5%T-tE06GyMu?0_UkOnIzVX2_G(ajk~hG)Xi?v~Md83az3iucVw(ddMijYxD>m+< z1nueETVP{Lr;wGw@1GN^;sdzpLpGWm)1_)2w_d4LTNVI^D^ zvzV!1Fr(zj{h>!8hD779&)vxwgJ%@2D>Ca|G)g4O`vu*Uir`z`5$;hIF1eD6$z}js z$}3W{l6U?swkC50l|dgfhiF*VnB5MTg_`5Ufys3q{;LegWog=YKDs!@&^wz%Wsi^{ z-45h?eadv`e+;+R?9~yZ1!*;U*0}U3o#6S1xbW~U zC|c>K&B)gK+ne>DE_3%$eB+oFOpwyU%UW{wNC=!M9jEh*)LZbwf;(SMh^0*_W{e3@16DS80 z(H$mG22m(%eaC!SW*ACe8m(Gg zqZ)aEFrdm-SbZf?h@O{hobP>jrW@2$&2ZXL72wMr;)kA)Sr%WXEdsFaB$opSHLBK1UHqLf&OvgAe1V^MLJ)dhvF zlX)A2N7nLsLB3MOj3NP}P%F2~O$4!djTe|1<19bn$ZbA&E>FQD_e^ww-r(GB3VE0B zYCQw6E!_PR*sabpRsv_((0j4W^92MF<-_HtRleZHO~m${N5O|r5*EL5;6)fjW^LOW zwl?EtEi;-zu!5?NL^@`sI{n)<{wiA9tu#@<@j^OfN)4`E_Pc=;u|5XzSwg=bAI}AQ z?q?uw^|q6T*=8DH=DaQO-$B+F{{Eo^*is+uKe@leEMS&Z@g;|95gp+tDC8ZrWpCTvb#0CZ7*f?-Qq5jy9!k?;osPHuCRd;AC)g87a$wJkj~c zE$JJtyr%}9q@EYFs({jLzEr*JIT#cC%fTzxa0f4%h|7g$u7{U5tyBt_Iq$T~YaKp9 zMh%qrSgNBQirdh!ZkYJV^>-QM4aA2G4*6PEA zKOn)5+ppEXNFl<)+Pyv?$^C*s$9QEBiKXvp_g@LZAt<8a8zlc>Ld$;)%Bn~KxKHN& zNNte}N0UuxlAa1G9>*b#!t;@DkIif4(71sY##2bhGxF|I!p?Pl7xmsr&npz;4ew>w zYZptDuk58H@coE0P>9a4lVHT6+mnNWoM_Uvsw6}ykWZgC$B!ij@I{U{P#4hoEs&wP zkPRIPzVS~mU~ZIT=!D3wIN;U*WZO0k43I^V$)4?fNCrb1Gb>BC?PtlF_ogLHHX3ZA zi;aI(@HNd^-#N-zmlqpk#0M#D6gZ9!q)(Z$uG>C8x%=P$*)hkXklCA*ZVkZsg}6Ly z9>K>WHs5OQ@SKb+s*;gLMPl~1yT<0^!cg4JLJ+Q;DebP^KM#GWx5||h@bh#aLizKX zbD1uulPupf1)0t7rhSE@t22P4jVQ(I&ptV+>^oe9z*rDVmeJB!0?(t6cjuz~yK5Fo zLTbw%)P_@kA(v6U9X;_~J$x;Tefp-eJle7=+7UFP!m!+FHMZcbbN5gANhi$hAYFZh z8&;tO-94K$6*COYh4V2_1FyP2zZp;b&YHF;vP~FE4PH#Yc&@Y&G;)~7td@B10{y-c zbV=Wwyo%8~)}O(H^5Tbr=lT8G3X;kt&3XYjwe^31@Kodecgvb44iyI?;_EffFQWD=0$%oy}Z3CH@x2MNwJ$A6;rj_bGH4{9)Z zc7Y%kXIv=@Qo*JrmpK(`YECLOvO8h6g$z9QPrYE+iQo-3AU5< zp)Kq&cZXaw{>LU9nmJ>^l*>`eX-qUyE zLTk-QmXGiTcsUgbf~nbqPB}JaVp!?Jd0?Sgq;>2lRDj$~IJ9+*BZG#o9(oTe+p!0# z%n}S%*g>_FtLyztf8UCfgG#pFjw0VyCAblWK1jcgI+~?zxqo5|$Xb7k7;YNjx%Jc9)J_cXe?(#pva%)d8%D{>Xy z6X=&eN#hICi*jjh)Ln_6Z!~l)y$M;B?4$d|)se>9Ej5!2S!98jR3Qi{7tttFsFju1 z6-zusmxijZvumEUeW(RBPHxlrp;>99cijv8xeOgysc8FfK{uAFbWM%tPkMu;{*5JO zEap-ZNE9X0X-P&+b90hUt=}ZhnBV$QenYZ#_$}dq>0=X$OL^kN98yk={UKFQGI6z^ zuY|9gP2pq{n8ML`%S6qEc@vkcdJ5IK-ZHC)m0?uKQyse$(1<%lB_>fIDudqH~ogz3D>4=;O&vCi=YP2U8p` z%~aLc85fplO*%(RZ{ax<;qf+vp&$w(jmu!1Ds|7=?rv)WN|H_Nc1ECb1(m!nGZ3qL zhZ7Blua1?_QxNuTyp*i^ymHNW;Ks~Hz9IIf{-4=0EH4?;t2N7bf@_W;4DNXrDrpTy znwd0f?tMM!nN@{rUCk{ak7)W3lKfvV682vNO_7VE3r`YN^g>$*DsrL;=uvBJJ8t{qNoT z-gl~Yozqpl&e^@YdiUOIHMYYncEF&kD}i8<0e=UU?!8tDItgNt@w&xdtLK!2K~soG z)zT(b0AzsxfDe#R7;*&0Nn;Co_ccf<3>7#gjoi0I8{@lXpy6pR9K8frB(Yt@r{_D~ zH@%6Lq=h9jJW&i2@s#J^uSItE_JM$Y+A~rj-K_}Yx^adCi@k>9Idl9U0*D||r@0BN z!Q|HDcHvD+pu)*uB}~nSxH%#?ByV*s(a`LW`fWlad>iVSX%^vvQBil|ALKV#eo%0d zJd03XsB{d*o2%799*OXz;7I{+9W7yf%lDy)UoBiP7r=M~gOvk1(3$mpInFPvRFx3N zKu(z2g=Fx4`O25@E? zwxPTnv3eOOlUC77CY^wBxVE^{fd&Q=v<0MTD0dT{;uAV|2@xPv%~ul*3^!)KXbT!N z6?tZc!$o09cV0AJuklKOrFQ(RKv%ABDpM3Vr6fK!YG_k*dIaKUTJjg!9y0M)_gxb4 zvEVf&NP6pG^6_1_4pk=4=nk^)0I74%7K z%0H1#O%(9HKbX`VZ2iP-J!zq~(nI9A;SigaWa{RTg(BhpdDNs=@MpmF4fn$^b#zF= z4dH&_Y}hxoUAsFX760lU&$@BFF%8ZZ@M5rRO)m)p_q|TZh3{wS>k8j9hFt4bq=5A> zrIGB=hQ@BySx%3Ek)*P#14b|g;+)@cpDmZ>rw@lN%=j}HsmKpe>Bvt(=|#=GP9?Z` zJYKrTyjZbRE*#mIb7Kx`AxSzkUY^0x2#D+o9DGP<{`e>w?*wqE>JKV#i`vAqw#Y3= zMUFod#U2gRreHbU&=JiEre%wwA@BswWZnkY8!A!9^=#wY?1r#3MtubTW*CEQL3~^4 zK#tzu{w}3_%CHU7A^8uT-cFyfID-c#UUmh3!MA9?t>vRUQXUE%Zg1-a5C%FLF(#!9 z+Q>i}ih8Z`8tu(!VE8=)(Wf5ny>G0U$^*c z-hL$1;%G9<@j3>lKz8z(d+HDQ=IH8(WAhlFY&B9T@+O_rkZ;@g|%2 zHVlT#Nc{87tEilj8L}VkUxGiIR-fOqOlC{0;6~ zcs516jH-7suGSUch2u*l*FbTouUmv`{v%Kcdd8Z&t=Pc_x`&J37Euiq_>eeI@3uE~ zawjAscZz+gT-Ff=Gc3bc+Gw)PLjL4uI30kp%*nF`GTM_=eG6RnafGMZ@=<*3e;67T zysc->E>o-%E6p!2GYkI3L;NLsUug8`515{hJRV+3nb{@0sJDaHk9#CKD5W?R;;LJd9;+oi7mOE}h|+F0moFqmQ>W(WA@vG!ayr7W6Fp z5)(T5v3!EW2JRHP@ZML{_)=~q2v~CSQu^^=!@Qh10bLfmm|L*6T0R>mU!-klPqo2R#weoGFp-^;!Yb4%kI1+i zfh6+QojIa*G>Pghu?1N_u@c5(Oe%QPGG5gN%hlB>wLyPhG~>oxGKH2M1?CyqZP60v zSH&b%p~Z=>j~3&;bOP!QV$(u&HgyeLE|N(yy(>9ZN4%y{yEDO1NwoCW(cA|kII}-m z_`wkCM()DnX43amC3INc1ZI74RiVm1>7ikyEQ)z!9|C*v1%vn$Xy>MeN?E`q7~5Vi ziPMKiP)KDoY_u&=gBx{<4b_1fHjKn#nb0~;Cz|4C27wLQweah-NrxYvp(>$qtfGHr zP8-6P3V$kWeH@|Qaa?GVKY3v3k4joui_TRFlAwTo{D6s>#<2e@3|(wO5uJppcO zRU`?)^qqGGlpxXBB202&J-B;<1^H5qG(-5L54S582vnH$yhfb3hzNY?w|PNODnzr@ z7U`%;5s3J)r&4`faxV8xg$Ney6|VX;0EhAP3o-Q1Ik9#7FTp|KYR9ob8Tf2Xc=9g! zyb7jL8L?uI+%^%vlQ}Ce{!jGMyrFA5P_I^k^El|pt%@I#a-5TT-e05>D{aH|y@GGd z2l-vBXcgCi!=B%EfU|}_H1DGV?Ip1}cXW2CfvYe;fkkZ|?N~aHcY1sK=`@LPyabzd zYxL&q#wNh8E%II`Z7mWNW*F&aVlb7FY2P{~-rub^8s(#hPf#G3q|3GGR!#;;nEC;U zW<#w{a;z{pBH2XGU-nOGqcyf8W?1+xMFk;i=y!KR452_)3Y!FpzCG=gIIgv0)h?*% zO&8DD1Z^<7g?Xx+guW(j4;rov+@uT+1Xuq(Cd0yQm>ax8Q~V*pZ>)f#)UBse-hM=s7F+pc!fCe>H$;#5~K1@fh$43^6aFw)mu`2(tcurqN!U6$SzDn2i zPAWen5KIRasq!L?UgV3gpMWBujDe2&(*zsL6?-KEY^z}PS0|Xywf``Vxkn~u^s5%q zP!pK9_T<+#46kIF_jtK&lr{_x(x!Pl zw61mUbI~QDEQwOlOP6(`&I%48o#~-yn-^`{SFK?1YPaH4tBiYjuPcXazSKrh*iaeC zfjqKWF_*RG@p+2>;2SeLvOsx3E%+sE2DfpXIY~0_FU;Y2$qt*$i2e6l1pbSb>}&Wp zF|zvBCaqi;C0n#h(KJ^khY?pr}v zCT8F+^JwB;E(%F9A*GPJ1f2$KWn)&X(BHd*U$1+)0i3+ALggm)n^?nOA3 zvu+@DqozI)w_#dYN+V3lHnv2NTt>Gz)5Q((dRva}jRjmy;I6XLg>%o4(zEPw-$jNM znI;f`?-5Ez2r8A%&B2j$runTV2_vVXA`B*2zm)4Al6e716gdqoI9~7~W0de25n5MC z=od3cM7e2_hzyfogNkRbraEQ~RiWvV6!?dumE#f6k7tT1u2N>48tK>us-_&^d04!~ zUEH;RRH91ba3W5sdr7qOAdg`?$-z_Gy3VFs;i!n;J|70O7V*G!c6yr`+nj65Bm3PNSWN^tfrkFbx7>n{Q6LBsQn!RyO8V(wBTa*iQJm8e_3@mbY(9vSpLnI}k-t z^3XhmNs_=}3}e`3^FI9PBe*ml>Ro@QHz%Q+xFVoxoWVtf&YnxEa~N%LsIj@DF0q_; zy2gAlza5U8^xAwxXvjnV3d@-hl&S?Mf3h_R2LR0D0|1!+QyDthTX>jz__H~9{;M>c z=rpzJ9iBR*(xqn83-s=oZOd=Wbz^{RI}sizm$r~=90B+w+3DQCj}T5=QgBk8 zUQZO4n551|99SUVG$x=R;Jt9!ev-OddMxW+xN|WPF=*HBBob;uyIy{dP@-Xoy|G-b9&qH2A=jl&oFNHkaOPu1L|I>$%X;+QX!8-J&Hmvn1?H1O%`P%E9Sp}FESCY2~5>Vht+^|u5zUY)kj+U~7b50CSQjy+R1 zwjWe&bL-gxvQ~J1Hxj3OSpcK8d}a(4*2Ktc+#>!67HufUjOt5n7OC_L zQxxMvvUKs%fL?3YiN2UyAz_?j%hYyTrsiK$lak#QPalvNw0xxT#>}ooAP$1%hvZ}5 zcs+2|Ubj36X*lvpc+YhaelBypce~4`XBt9cS*d>N8M*Gld&G8ypuC(>sB`2}UDdd> z^>X;!J9Bi=2JOwQOFPqkxZjohYq%`#jh@#R(7yUIjYo#Yk<*g(Y{X-1dJg+`^K#l7 zukmgx4FwgZm3=ja(`p{Y8`bdO&L6T*Clt-h+gI7hkBTI6pMn|7CglW$2 zIyI|%6=mBga?nr}| zi}SmXEZF(FqJ~bs?*;Jh{!}>mPHT9XBlQsWwKv5E0MP#@cYC=yTD$yr=ALt1v-tLE zkX#}%u!|^d|As}$;3Az&=m3$!0V4ylY>^c!*HKE^eBmv4b6dNFie4$wIYi#+>f`yE z7}-Jtw=`}h8m19vL*qK+sd<)hj@kjjybh^NupHt7tzy%h-ix$+WnH8dT2;pdu1jYW&zXR?E-%S zz`UTF53^Q3liuc=erhN(fs4|$wKb??x!u;Sbh~LO>)Ou}J3K$hPqmmb$j5;p;vtHB z+)KUGs!~o)_cb54X5|8vI~bT0|Y|iI;qT58-ouJzoT7>o` z@}T{iQ$pq|r)Y0tbeGa~>6u!4z$ofMj&A968rrnkg(Vm%@#QHX497mjC~shs z9pkqa_$cg~4Kv?TA-?dGB>!OFx2K- zR29oqqlB8Iz6yV`z|fni)BSI8%$vvsWaUns{x3ll?|W@hjq3vY#LS=$C6c})m+4XN zn~5psu}5~(R-1BOZgt&n?yJ5uw;Xului4SVW7B^(pig?2*IkA`x8z4L7dfNIHY@CP zQz4#I>YCVvnw`gqS*p!})Ky;Z2QF-*8pTK=34EWMAy9QszQpy@=+f#yHTm6LM)0-QQexCvasQ*$AopO*De@*?iBjB|$dYx)1hCN4S8%`@$Ev>EJp&|d z2QB7k(kU89IMmy@Kx+>V!Yp(edxa+5)bQcw(2*1~7L@hCTS!5K+Q`VA9Up05}2?H|uC9bwHqU+Ioka(Z-$I z;}_W(J;|jXnKgLTaAGTDyTh{xxY+YAEUkhY(6_6lXc#Z@qRsZgh**a?UH!sjr(au8 zZVPxKo1LE|#OL>w(sH=Z!8Yje-?@WMq=L($CJr)ig80|r_yd6nU6`n!IO5@zzH_Vl z90W?(VNnh^g5s~wJ2ooX7qzfe%}aEp+4L#fD}NTBeXKIRuCqK0hokay91(bEfQ!s& zactTJqS3>_80M41hqlv&hQjJws<_%5e4E_M=U@mD3!5q)w6$cl0Iw4x-su-7LeLcA z5$NRt=hHS2ZXi^u#t3?i^c8pmYi+tPPj4H4D04Mo<`9P4MtA8iTeoXGg@;HZreF24 z3S8^Ox!jTy_v>`OlyojZ8@cU3x51S^Z8SM<2I8Cvk+5+}q*fmJ)_;9Xe0EjI?4S8_ zpmO(dw)M!Y^f55_&UUWoE&)~=TZe(73JeuljME8qi!PFycs3W&U1lnsh5YgvhgJKZZ8rlSX5J7jyYYkkI2q2Kc>q124 z3W&D!?p5bV%4}JAHiV4SUjYZMvlRw~hoKWWZaD~*Vk z>K+n%*85x$#9`=*f!8*IAKgd?<=zRRs=E|?_?^=1L1>9*(<8*M?A*-}Hf$e3R1AZT zflAW+r)Hm;KGL!-GIg--YvxKa>*+6MSv9aKniORLx!l*id0oq!Tqfv3Xo&CA!%|C< zCBg0&w#~EpceqXFTL*}Bi*4zDNCg`TJP+c6ae2GpJt(>GJH(HnnY&YK93iJtA==!u z$o%!j7)`X+&JOO2Mk!`0167RBuu8K&oYb4Ah0ueb8gFO8~X zG}tfRt+@7!iF9gcH2m(yKWg!&CN}LOen{ScykH%`G^rOg-)8e&{J22KQu&%(-0Fyp zLJ9XdZ^ig#qnrQXC6WPl&jhKj5N-8>zd>8E85X^%azP8BOQquNtB7@p1|d=-1rIuX zu^4%^|3iIG>KLl6a^QxX*9uI6l-k*dYPH}v8LBmiqMKKwv?(F2l}!Uk^_uu9f6g27 zcv|3*BVzWU?z^5{p0~d7;3$cK-d+^kKvI0pCP5^I)-1Jfjte0FtUvY+o5(=4M;`leU2I;O!Fhdo*9iQQ=6U{dL6_P4C#1wep?k^N_wKvaH-kLCK6t;Hmr z%2iD+O>_6t2n6NxvwJRn-T{KGxf_Yn#5{2$Dsi8|M&l4bnVx;8&iGI9NEr?gxj0L_ zFH6`yS*O4??=pt4fmV zl*3(#C6&;5IJ|9vXljxd@wXdHBlft^0VCJ+A*k(nk^Xtj8&UhBzQRqvraN|E>*Z)ECgew z#UoII=)$PC9-Jo}hvqfm_;4%pR%eajf(n}QK?I4-hRwYS?R{E=@;-u07}j4;!cY2_ zx8Vfh!%^KD# z-ZHwC#e_Fsj^{w>z*|I#z-BG=wh|_3X1?bv``wS+R!m%56A@dstuB)5n9U^-w!=>b z=3l-3wQD=M(JyPGr+_4^mXK}G_sy2q6CQ&Ti54qJiRP0@%hk{&WpM83)kMJw^hdKZ zO}CGsq_z>$Q0#(z-;rJfcLtmoC&f7yt@66qJvIo(CzK&N(5d)$!%rzU;KU(4It#iK zdx~(#Lh0Q{hV$I&s}l97GJA#&dz!sxL!WH|)r}uF_?zWyOBH`c4YdO95iou$e>&>R zb0Lz&g`58Zzvg#9Nmv*>WJ8At$f5o(H_FkmAq={x zKsyWwpDv%!KlzaVgc7}~K>u~_f3hR52fwl-f8FN$XG#8_a1P@ChX1=je+AD$pnBvG zd_8iY*&7I-o(Av(4J1}i6Zn}PviUmE@<4d>)qoB>kO+MppaVZ-{dH0hg76yXz$`-I z4Rk>pVgLxc7$M}@KpfaD2@yBchnaxn7z$GUuRq&smw*NMPj@cl0T2m!T)0;k7Bv4A z3??ITAf6IL(TMBK|4xzr7upNSH6jAN=FGoO4T;jgM*q)^6*VE(M%?HG00KZe0ss)H I^Y_L5KU#Ymv;Y7A From f17be56dd4ef11d6e388389bb2978f733d0a332e Mon Sep 17 00:00:00 2001 From: Ralph Mack Date: Fri, 16 Sep 2022 20:12:42 -0400 Subject: [PATCH 22/26] Patch with bugfix for dancing lights and user source now overrides config value --- CHANGELOG.md | 4 ++++ library.js | 62 ++++++++++++++++++++++++++++++++++++++++----------- module.json | 4 ++-- request.js | 2 +- torch.zip | Bin 23860 -> 24317 bytes 5 files changed, 56 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7814d..a2b629b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Middle Kingdom - v10 branch +### 2.1.2 - September 14, 2022 + - [BUGFIX] Fixed image used for dancing lights + - [BUGFIX] Reset precedence order to user-supplied light sources first, then the source in config settings, then module-provided defaults based on game rules. + ### 2.1.1 - September 6, 2022 - [BUGFIX] (Lupestro) Fixed issue where unlinked tokens were adjusting inventory on source token. - [BUGFIX] (Lupestro) Fixed designation of which stock light sources are consumable in GURPS diff --git a/library.js b/library.js index c5bc950..d38b386 100644 --- a/library.js +++ b/library.js @@ -15,13 +15,20 @@ export default class SourceLibrary { SourceLibrary.commonLibrary = await fetch('/modules/torch/sources.json') .then( response => { return response.json(); }); } + let configuredLight = { + system: systemId, + name: selfItem, + states: 2, + light: [ {bright: selfBright, dim:selfDim, angle:360} ] + }; // The user library reloads every time you open the HUD to permit cut and try. let mergedLibrary = userLibrary ? await fetch(userLibrary) .then( response => { return response.json(); }) - .then( userData => { return mergeLibraries (userData, SourceLibrary.commonLibrary); }) + .then( userData => { return mergeLibraries (userData, SourceLibrary.commonLibrary, configuredLight); }) .catch(reason => { console.warn("Failed loading user library: ", reason); - }) : mergeLibraries ({}, SourceLibrary.commonLibrary); + }) : mergeLibraries ( + {}, SourceLibrary.commonLibrary, configuredLight); // All local changes here take place against the merged data, which is a copy, // not against the common or user libraries. if (mergedLibrary[systemId]) { @@ -29,15 +36,6 @@ export default class SourceLibrary { mergedLibrary[systemId].topology, mergedLibrary[systemId].quantity); let library = new SourceLibrary(mergedLibrary[systemId]); - let targetLightSource = library.getLightSource(selfItem); - if (targetLightSource) { - targetLightSource.states = 2; - targetLightSource.light = [{ - bright: selfBright, - dim: selfDim, - angle: 360 - }]; - } return library; } else { mergedLibrary["default"].topology = getTopology( @@ -102,7 +100,7 @@ export default class SourceLibrary { /* * Create a merged copy of two libraries. */ -let mergeLibraries = function (userLibrary, commonLibrary) { +let mergeLibraries = function (userLibrary, commonLibrary, configuredLight) { let mergedLibrary = {} // Merge systems - system properties come from common library unless the system only exists in user library @@ -141,9 +139,47 @@ let mergeLibraries = function (userLibrary, commonLibrary) { }; } } + // Source properties for configured source override common library but not user library + let configuredName = ""; + if (configuredLight.name) { + let inUserLibrary = false; + let template = null; + if (system === configuredLight.system) { + for (let source in mergedLibrary[system].sources) { + if (source.toLowerCase() === configuredLight.name.toLowerCase()) { + inUserLibrary = true; + break; + } + } + if (!inUserLibrary) { + for (let source in commonLibrary[system].sources) { + if (source.toLowerCase() === configuredLight.name.toLowerCase()) { + configuredName = source; + template = commonLibrary[system].sources[source]; + break; + } + } + if (!configuredName) { + configuredName = configuredLight.name; //But might be blank + } + // We finally have the best name to use and perhaps a template + // We can build one + mergedLibrary[system].sources[configuredName] = { + "name": configuredName, + "type": template ? template["type"] :"equipment", + "consumable": template ? template["consumable"] : true, + "states": configuredLight.states, + "light": Object.assign({}, configuredLight.light ) + }; + } + } + } + // Finally, we will deal with the common library for whatever is left if (system in commonLibrary) { for (let source in commonLibrary[system].sources) { - if (!userLibrary || !(system in userLibrary) || !(source in userLibrary[system].sources)) { + if ((!userLibrary || !(system in userLibrary) || + !(source in userLibrary[system].sources)) && + (!configuredName || source !== configuredName)) { let commonSource = commonLibrary[system].sources[source]; mergedLibrary[system].sources[source] = { "name": commonSource["name"], diff --git a/module.json b/module.json index a6aa251..35d6fdc 100644 --- a/module.json +++ b/module.json @@ -3,7 +3,7 @@ "name": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "2.1.1", + "version": "2.1.2", "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], "languages": [ { @@ -52,7 +52,7 @@ "minimumCoreVersion": "10", "compatibility": { "minimum": "10", - "verified": "10.284", + "verified": "10.285", "maximum": "10" } } diff --git a/request.js b/request.js index 3970c77..26290b5 100644 --- a/request.js +++ b/request.js @@ -58,7 +58,7 @@ export default class TorchRequest { angle: 360 }, texture: { - src: "systems/dnd5e/icons/spells/light-air-fire-1.jpg", + src: "icons/magic/light/explosion-star-glow-orange.webp", scaleX: 0.25, scaleY: 0.25, rotation: 0 diff --git a/torch.zip b/torch.zip index 4af0c77ab3f31c8996e82d96983d0d57e9153817..50730075c4ddb3aafb75604276096bcda3651997 100644 GIT binary patch delta 8283 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

CsbpVRj7=+e;y2CGSQ6M9u%}Qk-=%8p% z>2)!&Ak=ME5fQ)SgVH8wpCEFQLNUHu;m&@#^~xJXX|yl`Pkt;jC>^BOtD7oGA1j^g zW*xa7@P&uqg)X3%#iAwW7J!OZW!UlM&RuE!5E8&!tex;&;vCgab%-W!m6h0J0A)d* zmk+gXVs=p1a%q z$t|F-jt>o)*p;qG%*gQZOp1hT9L`ysSe#07*RNfC(z=0cVljdDuW!kYA%ZtRA^>Yg z48Huxp;3`Vzh06&>0@b^{pTvf4^j%T((b9YvvApsQb|6^P9|t(!b``;-8bjPAU5#T z6L>5Sc~-v4XF}df)GJvqNo&y!?YQfGQ}jA4bG$b+jd{BPFmtrQZDk#wEY$B%mPNAW zVfL=zZkPtL-4}AvuaBMa=|_e(Te>jnYTvLG8Rpt9Ru|j&3(|A((G1C1h*{3h#qx^1 zPj~B>@sr#XYqQrQS6iuK-$|V^YQpm{?ef8WAEtu9Y>7I=dql0C@Og{((jA5jrPv5aa;o6PUiEk$-e81D?9j9jY8%lL z=2l;b=M}uqwOO?F<)P%qSLC0ch9*LCL}S`Ifn~k#h<*d6>kB(|{FQrt+*t~RbPe*$ zYs>=3Z!T_suglX%;tgRq3^;$L6rWu&<8MEfT5|0`)lj~s7L=xrP@{p2O?c^hm66u$ z`bhQiPBB>{^wOD4>2{tQj zY6EMJ&vQ^FcGASd8;PQ7wv>9YbgvRu@kA$-LPxbbFK*BXBB+;`qRoPb7{#}~`+4;| zmH^xx;_k3b32!!0n;thUs0PN4Y()08c59q_7Z$mMu@;K+h^PU_-wug1be}-=>$w<= zEAM1AZ3)`lp9-(f7pI?nFY(y=!An~Tf@omj+gTa22yx~sXoeKT1EaFKx|iqC6{n71 zN+65gCH*e>369ftc>R`XcLgU=ie-TkTA1*es%G#;Z+P%2$#L|uM@>Z!!l>GtXw2v| z<-LM&`i4^<`@>f%&ua?z0-SNhQ}qUD8vT;`xq{tniAr|0CRC%8IA&DPI3-L|Uo^JURs$$d8tM^3-)Rn4l z9I04jzQ$ei4rW%Gug!rx7ncP+nLcjUdB1-V#a%$1t+V|_aq?YM_OinBRFS+*VZMOo zGmNchblKZrr?bSE@t4sGdEbPgJ|z?xk6msRw3h_v8W1tAwg1uwp+sh=5G5m+y_eLQ z17^Rc{~8a(dr${{($r| z9UvkPt9{mmHnK2+xz(1e=A4)QgdDSoVIwOi!C1KYX_?Br7p>s-JIfz0OO@edzWfld zsOpkctj=F#Rs5ZZ$ZmQtGG8GG7!PC73KjEDvYINPTts9>I7C+z0**jmw5oUbizPu+ z9T8at4p|e2fJb2vxc%-@4VA=OL}VQ}WKWV6ybQbi_V0lpQaOmomT<_R3@i8?&f@sj zK*Qb+i^Fa^#~VY^ZF?+M)sN;G1vlY+FF$}Kn%RZ*?m)j~{XDaJe3w@ou48LaGV z9rjUDRbJ-B^>&&ZVzYJMRf)ANnzWqdZC@1`Wj+m6UMX-M?tM04&c1B_^~N*k@@m5p zOe4EBF4GiD_z7)(&@zUfR~&fXWa;!Z1w%|VGlP!8;(JGx-TslWgol+7@~hh!7cD_~ zICEVU?%aVsPGp!@pSkCma@wf!&C@VA0?%3&xZ2x-DVoXBe6O~tDdn_?P($KDY*{9A zBZ<6Ug#FrP6|_#-Xi4djSZy=cALKEXo-hJK*d~RMx$o3D z!qyHwALb|Tv$lZr4HeJWg=h4)>@rj>aJ6R+M|^x=UwvlT4?L(}qsca7DKYwH{@7=s znWT=(GH$WhZxiSVkYq}J>Ec_2)4Kc>p-(|#SLiUmM^{lwrMST$IH$Vi3W!d}7!c(U z54kR!WV^5&qd--gjcCV1__c%CbYYu;U6l@e$8gDt7iPc5M2)4YH!{*Hlo>js2>~;} zB8Ks=sHq{;l0ZVHgU6q)E;Bd;7Jm<6_B%;Iye<+FtdRjtG+<@;pIr| z`A-@jcV}C-A9Xm41fGchK?2Xl>~_BOi?CM?-q;`z?jN&DriOkuA_m}@`X3zdbPciE z*;+*ef#%RaAiO^~8X122hX|}0Q|K7+{=DDC(#@VrPZxIY{}hE^F$I@}W1;6L0`YP^yD_6o{u%Pz+Nl04`YnfWuYm9?MwIENsZa z|5|X6+#f4w>-}p5;VSnJ-k(xOwhh~~{jf+U_+u`NiqKY5vL71w4-$A@TrYl}YX`f3 z1`h64=)kwq=|2;Q#&))4JZZ From 60a46bb6f96b2b0c4767eae75b8938e783c0118e Mon Sep 17 00:00:00 2001 From: Ralph Mack Date: Mon, 19 Sep 2022 18:20:43 -0400 Subject: [PATCH 23/26] Fixed a deprecation warning on the quench check --- torch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torch.js b/torch.js index 3beef5b..554521c 100644 --- a/torch.js +++ b/torch.js @@ -101,7 +101,7 @@ Hooks.on("ready", () => { Hooks.once("init", () => { // Only load and initialize test suite if we're in a test environment - if (game.world.data.name.startsWith("torch-test-")) { + if (game.world.id.startsWith("torch-test-")) { Torch.setupQuenchTesting(); } Settings.register(); From 2dd522870f949a2ba229bbe35cabd8f4c09872cd Mon Sep 17 00:00:00 2001 From: Ralph Mack Date: Sat, 8 Oct 2022 20:08:49 -0400 Subject: [PATCH 24/26] Three bug fixes --- CHANGELOG.md | 5 + lang/cn.json | 1 + lang/pt-BR.json | 1 + lang/zh-TW.json | 1 + library.js | 11 +- module.json | 131 +++++++++++++----------- test/tickets/issue-26/user-sources.json | 32 ++++++ torch.js | 1 + torch.zip | Bin 24317 -> 24523 bytes 9 files changed, 122 insertions(+), 61 deletions(-) create mode 100644 test/tickets/issue-26/user-sources.json diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b629b..f549c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Middle Kingdom - v10 branch +### 2.1.3 - October 8, 2022 + - [BUGFIX] Corrected issue (found by vkdolea) where user-supplied sources for new systems weren't processing properly. + - [BUGFIX] Now pulling non-dim/bright light properties for the light source configured in settings from the prototype token. + - [BUGFIX] Fixed the translation files for several languages. + ### 2.1.2 - September 14, 2022 - [BUGFIX] Fixed image used for dancing lights - [BUGFIX] Reset precedence order to user-supplied light sources first, then the source in config settings, then module-provided defaults based on game rules. diff --git a/lang/cn.json b/lang/cn.json index 5c5c0da..1506f61 100644 --- a/lang/cn.json +++ b/lang/cn.json @@ -1,4 +1,5 @@ { + "I18N.LANGUAGE": "中文", "I18N.MAINTAINERS": ["Chivell"], "torch.playerTorches.name": "玩家火炬", diff --git a/lang/pt-BR.json b/lang/pt-BR.json index 8901593..e4fb12d 100644 --- a/lang/pt-BR.json +++ b/lang/pt-BR.json @@ -1,4 +1,5 @@ { + "I18N.LANGUAGE": "Português (Brasil)", "I18N.MAINTAINERS": ["Innocenti"], "torch.playerTorches.name": "Tocha dos jogadores", diff --git a/lang/zh-TW.json b/lang/zh-TW.json index 9a66d3c..8181469 100644 --- a/lang/zh-TW.json +++ b/lang/zh-TW.json @@ -1,4 +1,5 @@ { + "I18N.LANGUAGE": "正體中文", "I18N.MAINTAINERS": ["Chivell"], "torch.playerTorches.name": "玩家火炬", diff --git a/library.js b/library.js index d38b386..fc3232e 100644 --- a/library.js +++ b/library.js @@ -9,17 +9,20 @@ export default class SourceLibrary { this.library = library; } - static async load(systemId, selfBright, selfDim, selfItem, userLibrary) { + static async load(systemId, selfBright, selfDim, selfItem, userLibrary, protoLight) { // The common library is cached - to update it, you must reload the game. if (!SourceLibrary.commonLibrary) { SourceLibrary.commonLibrary = await fetch('/modules/torch/sources.json') .then( response => { return response.json(); }); } + let defaultLight = Object.assign({}, protoLight); + defaultLight.bright = selfBright; + defaultLight.dim = selfDim; let configuredLight = { system: systemId, name: selfItem, states: 2, - light: [ {bright: selfBright, dim:selfDim, angle:360} ] + light: [ defaultLight ] }; // The user library reloads every time you open the HUD to permit cut and try. let mergedLibrary = userLibrary ? await fetch(userLibrary) @@ -151,7 +154,7 @@ let mergeLibraries = function (userLibrary, commonLibrary, configuredLight) { break; } } - if (!inUserLibrary) { + if (!inUserLibrary && commonLibrary[system]) { for (let source in commonLibrary[system].sources) { if (source.toLowerCase() === configuredLight.name.toLowerCase()) { configuredName = source; @@ -169,7 +172,7 @@ let mergeLibraries = function (userLibrary, commonLibrary, configuredLight) { "type": template ? template["type"] :"equipment", "consumable": template ? template["consumable"] : true, "states": configuredLight.states, - "light": Object.assign({}, configuredLight.light ) + "light": configuredLight.light }; } } diff --git a/module.json b/module.json index 35d6fdc..a3a1031 100644 --- a/module.json +++ b/module.json @@ -1,58 +1,75 @@ { - "id": "torch", - "name": "torch", - "title": "Torch", - "description": "Torch HUD Controls", - "version": "2.1.2", - "authors": [{ "name": "Deuce"},{ "name": "Lupestro"}], - "languages": [ - { - "lang": "en", - "name": "English", - "path": "./lang/en.json" - }, - { - "lang": "cn", - "name": "中文", - "path": "./lang/cn.json" - }, - { - "lang": "de", - "name": "Deutsch", - "path": "./lang/de.json" - }, - { - "lang": "fr", - "name": "Français", - "path": "./lang/fr.json" - }, - { - "lang": "es", - "name": "Español", - "path": "./lang/es.json" - }, - { - "lang": "pt-br", - "name": "Português (Brasil)", - "path": "./lang/pt-BR.json" - }, - { - "lang": "zh-tw", - "name": "正體中文", - "path": "./lang/zh-TW.json" - } - ], - "esmodules": ["./torch.js"], - "socket": true, - "styles": ["./torch.css"], - "packs": [], - "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", - "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", - "url": "https://github.com/League-of-Foundry-Developers/torch", - "minimumCoreVersion": "10", - "compatibility": { - "minimum": "10", - "verified": "10.285", - "maximum": "10" - } -} + "id": "torch", + "title": "Torch", + "description": "Torch HUD Controls", + "version": "2.1.3", + "authors": [ + { + "name": "Deuce", + "flags": {} + }, + { + "name": "Lupestro", + "flags": {} + } + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json", + "flags": {} + }, + { + "lang": "cn", + "name": "中文", + "path": "lang/cn.json", + "flags": {} + }, + { + "lang": "de", + "name": "Deutsch", + "path": "lang/de.json", + "flags": {} + }, + { + "lang": "fr", + "name": "Français", + "path": "lang/fr.json", + "flags": {} + }, + { + "lang": "es", + "name": "Español", + "path": "lang/es.json", + "flags": {} + }, + { + "lang": "pt-BR", + "name": "Português (Brasil)", + "path": "lang/pt-BR.json", + "flags": {} + }, + { + "lang": "zh-TW", + "name": "正體中文", + "path": "lang/zh-TW.json", + "flags": {} + } + ], + "esmodules": [ + "torch.js" + ], + "socket": true, + "styles": [ + "torch.css" + ], + "manifest": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/module.json", + "download": "https://raw.githubusercontent.com/League-of-Foundry-Developers/torch/v10/torch.zip", + "url": "https://github.com/League-of-Foundry-Developers/torch", + "compatibility": { + "minimum": "10", + "verified": "10.287", + "maximum": "10" + } +} \ No newline at end of file diff --git a/test/tickets/issue-26/user-sources.json b/test/tickets/issue-26/user-sources.json new file mode 100644 index 0000000..d17e0a5 --- /dev/null +++ b/test/tickets/issue-26/user-sources.json @@ -0,0 +1,32 @@ +{ + "age-system": { + "system": "age-system", + "topology": "standard", + "quantity" : "quantity", + "sources": { + "Lanterna": { + "name": "Lanterna", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [ + { + "bright": 10, "dim": 15, "angle": 30, "color": "#ff9329", "alpha": 0.5, + "animation": { "type": "torch", "speed": 5, "intensity": 5, "reverse": false } + } + ] + }, + "Visão Noturna": { + "name": "Visão Noturna", + "type": "equipment", + "consumable": false, + "states": 2, + "light": [{ + "bright": 1000, "dim": 1000, "angle": 360, "color": "#184601", "alpha": 0.4, + "animation": { "type": "none", "speed": 5, "intensity": 5, "reverse": false }, + "light": {"luminosity": 0.3} + }] + } + } + } +} \ No newline at end of file diff --git a/torch.js b/torch.js index 554521c..d18d3f8 100644 --- a/torch.js +++ b/torch.js @@ -31,6 +31,7 @@ class Torch { Settings.lightRadii.dim, Settings.inventoryItemName, Settings.gameLightSources, + hud.object.actor.prototypeToken.light, ); let token = new TorchToken(hud.object.document, library); let lightSources = token.ownedLightSources; diff --git a/torch.zip b/torch.zip index 50730075c4ddb3aafb75604276096bcda3651997..c33db5de342b591ee4c780a6e9d077b5e8914dbb 100644 GIT binary patch delta 9268 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 ZE@LeiX-xo_LkkGbF{XtN)%m+g{{w)&Y&ie` From 8671acb3d7f0beea6f19f7b277b9f45ca07af6a4 Mon Sep 17 00:00:00 2001 From: Ralph Mack Date: Sun, 4 Dec 2022 10:31:29 -0500 Subject: [PATCH 25/26] Fixed the bullseye lanterns --- sources.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sources.json b/sources.json index fff8dac..f1f6e28 100644 --- a/sources.json +++ b/sources.json @@ -37,7 +37,7 @@ "consumable": false, "states": 2, "light": [ - { "bright": 15, "dim": 45, "angle": 53 } + { "bright": 60, "dim": 120, "angle": 57 } ] }, "Hooded Lantern": { @@ -151,7 +151,7 @@ "consumable": false, "states": 2, "light": [ - {"bright": 60, "dim": 120, "angle": 53} + {"bright": 60, "dim": 120, "angle": 57} ] }, "Hooded Lantern": { @@ -169,7 +169,7 @@ "consumable": false, "states": 2, "light": [ - {"bright": 30, "dim": 60, "angle": 53} + {"bright": 30, "dim": 60, "angle": 57} ] }, "Torch": { @@ -213,7 +213,7 @@ "consumable": false, "states": 2, "light": [ - {"bright": 60, "dim": 120, "angle": 53} + {"bright": 60, "dim": 120, "angle": 57} ] }, "Torch": { @@ -345,7 +345,7 @@ "consumable": false, "states": 2, "light": [ - {"bright": 9, "dim": 10, "angle": 53, "color": "#ff9329", "alpha": 1 } + {"bright": 9, "dim": 10, "angle": 57, "color": "#ff9329", "alpha": 1 } ] }, "Electric Lantern, Small": { From e5cfa3afb3eae5bb00ca62ddf57ae87316144f1b Mon Sep 17 00:00:00 2001 From: Ralph Mack Date: Sun, 4 Dec 2022 10:57:12 -0500 Subject: [PATCH 26/26] Preparing patch release --- CHANGELOG.md | 10 ++++++++++ module.json | 4 ++-- torch.zip | Bin 24523 -> 24874 bytes 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f549c0b..c063a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Middle Kingdom - v10 branch +### 2.1.4 - December 4, 2022 + - [BUGFIX] DnD5e Bullseye Lantern had wrong dim/bright ranges due to copy/paste error. + - [BUGFIX] Bullseye lanterns in all systems that had 53 degree radius, now have 57 degree radius. + * 53 degrees is technically correct for a cone (n units wide at center distance n, per rules). + * 57 degrees (1 radian) would be correct for a (spherical) sector of radius n and arc length n. + * Foundry projects light radially, so you get a spherical sector, regardless of the game rules. + * Using 57 degrees is the fairest solution available within Foundry's constraints. + * The extra beam width compensates for what you lose in the corners that radial light won't reach. + * You can always reduce it back to 53 degrees with a custom JSON, of course, if you feel strongly about it. + ### 2.1.3 - October 8, 2022 - [BUGFIX] Corrected issue (found by vkdolea) where user-supplied sources for new systems weren't processing properly. - [BUGFIX] Now pulling non-dim/bright light properties for the light source configured in settings from the prototype token. diff --git a/module.json b/module.json index a3a1031..21589aa 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "torch", "title": "Torch", "description": "Torch HUD Controls", - "version": "2.1.3", + "version": "2.1.4", "authors": [ { "name": "Deuce", @@ -69,7 +69,7 @@ "url": "https://github.com/League-of-Foundry-Developers/torch", "compatibility": { "minimum": "10", - "verified": "10.287", + "verified": "10.291", "maximum": "10" } } \ No newline at end of file diff --git a/torch.zip b/torch.zip index c33db5de342b591ee4c780a6e9d077b5e8914dbb..ae5ec54e76d0f056a3b00f6b8f15f086b8c43c25 100644 GIT binary patch delta 4896 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 myA3OFAAwc2c?irhz^2+DK`