diff --git a/CarryTokens/2.0/carry.js b/CarryTokens/2.0/carry.js new file mode 100644 index 0000000000..464bc54908 --- /dev/null +++ b/CarryTokens/2.0/carry.js @@ -0,0 +1,367 @@ +var CarryTokens = (() => { + 'use strict'; + + const CARRY_MENU_CMD = '!CARRY_TOKENS_MENU'; + const CARRY_ABOVE_CMD = '!CARRY_TOKENS_CARRY_ABOVE'; + const CARRY_BELOW_CMD = '!CARRY_TOKENS_CARRY_BELOW'; + const DROP_ONE_CMD = '!CARRY_TOKENS_DROP_ONE'; + const DROP_ALL_CMD = '!CARRY_TOKENS_DROP_ALL'; + + let MENU_CSS = { + 'centeredBtn': { + 'text-align': 'center' + }, + 'menu': { + 'background': '#fff', + 'border': 'solid 1px #000', + 'border-radius': '5px', + 'font-weight': 'bold', + 'margin-bottom': '1em', + 'overflow': 'hidden' + }, + 'menuBody': { + 'padding': '5px', + 'text-align': 'center' + }, + 'menuHeader': { + 'background': '#000', + 'color': '#fff', + 'text-align': 'center' + } + }; + + /** + * Handlers for chat commands + */ + class Commands { + /** + * Command for carryAbove() + */ + static carryAbove(msg) { + Commands._enforcePermission(msg.playerid); + + let argv = msg.content.split(' '); + let carrierId = argv[1]; + let targetId = argv[2]; + + let carrier = getObj('graphic', carrierId); + let target = getObj('graphic', targetId); + + carryAbove(carrier, target); + } + + /** + * Command for carryBelow() + */ + static carryBelow(msg) { + Commands._enforcePermission(msg.playerid); + + let argv = msg.content.split(' '); + let carrierId = argv[1]; + let targetId = argv[2]; + + let carrier = getObj('graphic', carrierId); + let target = getObj('graphic', targetId); + + carryBelow(carrier, target); + } + + /** + * Command for drop() + */ + static dropOne(msg) { + Commands._enforcePermission(msg.playerid); + + let argv = msg.content.split(' '); + let carrierId = argv[1]; + let name = argv.slice(2).join(' '); + + let carrier = getObj('graphic', carrierId); + let target = findObjs({ + _type: 'graphic', + _pageid: carrier.get('_pageid'), + name + })[0]; + if(!target) + throw new Error(`Token ${name} not found.`); + + drop(carrier, target); + } + + /** + * Command for dropAll() + */ + static dropAll(msg) { + Commands._enforcePermission(msg.playerid); + + let argv = msg.content.split(' '); + let carrierId = argv[1]; + let carrier = getObj('graphic', carrierId); + + dropAll(carrier); + } + + /** + * If a player does not have permission to use this script's functions, + * throw an Error for the incident. + * @param {string} playerId + */ + static _enforcePermission(playerId) { + let allowPlayerUse = getOption('allowPlayerUse'); + let hasPermission = allowPlayerUse || _.isUndefined(allowPlayerUse) || playerIsGM(playerId); + + if(!hasPermission) + throw new Error(`Player ${playerId} tried to use a restricted part of this script without permission.`); + } + + /** + * Shows the menu. + */ + static showMenu(msg) { + Commands._enforcePermission(msg.playerid); + _showMenu(msg.who, msg.playerid); + } + } + + /** + * Makes a token carry another token, without regard to their positions + * on the Z axis. + * @private + * @param {Graphic} carrier + * @param {Graphic} token + */ + function _carry(carrier, token) { + // A token can't carry itself. + if(carrier === token) + return; + + // Initialize the carry list if it doesn't exist. + if(!carrier.carryList) + carrier.carryList = []; + + // Add the carried token. + if(!carrier.carryList.includes(token)) { + carrier.carryList.push(token); + token.carriedBy = carrier; + } + } + + /** + * Makes a token carry another token such that the carried token is + * above the carrier token on the Z axis. + * @param {Graphic} carrier + * @param {Graphic} token + */ + function carryAbove(carrier, token) { + _carry(carrier, token); + toFront(token); + } + + /** + * Makes a token carry another token such that the carried token is + * below the carrier token on the Z axis. + * @param {Graphic} carrier + * @param {Graphic} token + */ + function carryBelow(carrier, token) { + _carry(carrier, token); + toBack(token); + } + + /** + * Makes a token drop a token it's carrying. + * @param {Graphic} carrier + * @param {Graphic} token + */ + function drop(carrier, token) { + if(!carrier.carryList) + return; + + let index = carrier.carryList.indexOf(token); + if(index !== -1) { + carrier.carryList.splice(index, 1); + delete token.carriedBy; + } + } + + /** + * Makes a token drop all tokens it is carrying. + * @param {Graphic} carrier + */ + function dropAll(carrier) { + if(carrier.carryList) + delete carrier.carryList; + } + + /** + * Fixes msg.who. + * @param {string} who + * @return {string} + */ + function _fixWho(who) { + return who.replace(/\(GM\)/, '').trim(); + } + + /** + * Returns a copy of a token's list of carried tokens. + * @param {Graphic} carrier + * @return {Graphic[]} + */ + function getCarriedTokens(carrier) { + return _.clone(carrier.carryList || []); + } + + /** + * Gets the value of a One-Click user option for this script. + * @param {string} name + * @return {any} + */ + function getOption(name) { + let options = globalconfig && globalconfig.carrytokens; + if(!options) + options = (state.carrytokens && state.carrytokens.useroptions) || {}; + + return options[name]; + } + + /** + * If a token is carrying other tokens, move the carried tokens to the + * carrier token. + * @param {Graphic} carrier + */ + function moveCarriedTokens(carrier) { + // Move each of the tokens carried by the token. + if(carrier.carryList) + _.each(carrier.carryList, carried => { + carried.set('left', carrier.get('left')); + carried.set('top', carrier.get('top')); + carried.set('rotation', carrier.get('rotation')); + + // If the carried token is carrying anything, move its carried + // tokens too. + moveCarriedTokens(carried); + }); + } + + /** + * Shows the list of effects which can be applied to a selected path. + * @param {string} who + * @param {string} playerid + */ + function _showMenu(who, playerid) { + let content = new HtmlBuilder('div'); + content.append('.centeredBtn').append('a', 'Carry Above', { + href: `${CARRY_ABOVE_CMD} @{selected|token_id} @{target|token_id}`, + title: 'Make selected token carry target token on top.' + }); + content.append('.centeredBtn').append('a', 'Carry Below', { + href: `${CARRY_BELOW_CMD} @{selected|token_id} @{target|token_id}`, + title: 'Make selected token carry target token underneath.' + }); + content.append('.centeredBtn').append('a', 'Drop by Name', { + href: `${DROP_ONE_CMD} @{selected|token_id} ?{Drop token name:}`, + title: 'Make selected token drop a carried token.' + }); + content.append('.centeredBtn').append('a', 'Drop All', { + href: `${DROP_ALL_CMD} @{selected|token_id}`, + title: 'Make selected token drop all carried tokens.' + }); + + let menu = _showMenuPanel('Carry Tokens', content); + _whisper(who, menu.toString(MENU_CSS)); + } + + /** + * Displays one of the script's menus. + * @param {string} header + * @param {(string|HtmlBuilder)} content + * @return {HtmlBuilder} + */ + function _showMenuPanel(header, content) { + let menu = new HtmlBuilder('.menu'); + menu.append('.menuHeader', header); + menu.append('.menuBody', content) + return menu; + } + + /** + * Whispers a Marching Order message to someone. + * @private + */ + function _whisper(who, msg) { + sendChat('Carry Tokens', '/w "' + _fixWho(who) + '" ' + msg); + } + + on('chat:message', msg => { + try { + if(msg.content.startsWith(CARRY_MENU_CMD)) + Commands.showMenu(msg); + if(msg.content.startsWith(CARRY_ABOVE_CMD)) + Commands.carryAbove(msg); + if(msg.content.startsWith(CARRY_BELOW_CMD)) + Commands.carryBelow(msg); + if(msg.content.startsWith(DROP_ONE_CMD)) + Commands.dropOne(msg); + if(msg.content.startsWith(DROP_ALL_CMD)) + Commands.dropAll(msg); + } + catch(err) { + log('Carry Tokens ERROR: ' + err.message); + sendChat('Carry Tokens ERROR:', '/w ' + _fixWho(msg.who) + ' ' + err.message); + log(err.stack); + } + }); + + // Do the carrying logic when the carrier tokens move. + on("change:graphic", obj => { + try { + // If the token was moved by a player while it was being carried, + // stop carrying it. + if(obj.carriedBy) + drop(obj.carriedBy, obj); + + moveCarriedTokens(obj); + } + catch(err) { + log('Carry Tokens ERROR: ' + err.message); + log(err.stack); + } + }); + + // Create macros + on('ready', () => { + let players = findObjs({ + _type: 'player' + }); + + // Create the macro, or update the players' old macro if they already have it. + _.each(players, player => { + let macro = findObjs({ + _type: 'macro', + _playerid: player.get('_id'), + name: 'CarryTokensMenu' + })[0]; + + if(macro) + macro.set('action', CARRY_MENU_CMD); + else + createObj('macro', { + _playerid: player.get('_id'), + name: 'CarryTokensMenu', + action: CARRY_MENU_CMD + }); + }); + + log('--- Initialized Carry Tokens ---'); + }); + + // Exposed API + return { + carryAbove, + carryBelow, + drop, + dropAll, + getCarriedTokens, + getOption + }; +})(); diff --git a/CarryTokens/README.md b/CarryTokens/README.md new file mode 100644 index 0000000000..414ac88837 --- /dev/null +++ b/CarryTokens/README.md @@ -0,0 +1,58 @@ +# Carry Tokens + +This script allows you to set tokens to carry each other, either as a +character holding an item, or as a mount carrying a rider. The carried +token will always set its position to that of the carrying token. When you +move the carrier around, the carried token will follow. + +## To Use + +When this script is installed, it creates a CarryTokensMenu macro which +displays a menu for this script in the VTT's chat. This menu has the following +commands: + +### Carry Above +This command allows you to have a token carry another above it on the Z-axis. +This is most helpful when you need a large token such as a mount to carry +a smaller token such as a rider. + +To use this, first select the carrier token and then click the Carry Above +command. When prompted, click on the token you want to carry. + +### Carry Below +This command allows you to have a token carry another below it on the Z-axis. +This is most helpful when you need a token to carry a token of the same size or +smaller, for example when you want a character to carry an item such as a +torch. + +To use this, first select the carrier token and then click the Carry Below +command. When prompted, click on the token you want to carry. + +### Drop by Name +This command makes a token drop another token it is carrying. + +To use this, select the carrier token and then click the Drop by Name command. +When prompted, enter the name of the token you want to drop. + +You can also cause a token to stop being carried by moving it away from +the token carrying it. + +### Drop All +This command makes a token drop all other tokens it is carrying. + +To use this, select the carrier token and then click the Drop All command. + +## Help + +If you experience any issues while using this script or the trap themes, +need help using it, or if you have a neat suggestion for a new feature, +please shoot me a PM: +https://app.roll20.net/users/46544/stephen-l +or create a help thread on the Roll20 API forum + +## Show Support + +If you would like to show your appreciation and support for the work I do in writing, +updating, and maintaining my API scripts, consider buying one of my art packs from the Roll20 marketplace (https://marketplace.roll20.net/browse/search/?keywords=&sortby=newest&type=all&genre=all&author=Stephen%20Lindberg) +or, simply leave a thank you note in the script's thread on the Roll20 forums. +Either is greatly appreciated! Happy gaming! diff --git a/CarryTokens/demo.gif b/CarryTokens/demo.gif new file mode 100644 index 0000000000..e999d39b30 Binary files /dev/null and b/CarryTokens/demo.gif differ diff --git a/CarryTokens/script.json b/CarryTokens/script.json new file mode 100644 index 0000000000..11e8a55b5e --- /dev/null +++ b/CarryTokens/script.json @@ -0,0 +1,21 @@ +{ + "name": "Carry Tokens", + "script": "carry.js", + "version": "2.0", + "previousversions": [], + "description": "# Carry Tokens\r\rThis script allows you to set tokens to carry each other, either as a\rcharacter holding an item, or as a mount carrying a rider. The carried\rtoken will always set its position to that of the carrying token. When you\rmove the carrier around, the carried token will follow.\r\r## To Use\r\rWhen this script is installed, it creates a CarryTokensMenu macro which\rdisplays a menu for this script in the VTT's chat. This menu has the following\rcommands:\r\r### Carry Above\rThis command allows you to have a token carry another above it on the Z-axis.\rThis is most helpful when you need a large token such as a mount to carry\ra smaller token such as a rider.\r\rTo use this, first select the carrier token and then click the Carry Above\rcommand. When prompted, click on the token you want to carry.\r\r### Carry Below\rThis command allows you to have a token carry another below it on the Z-axis.\rThis is most helpful when you need a token to carry a token of the same size or\rsmaller, for example when you want a character to carry an item such as a\rtorch.\r\rTo use this, first select the carrier token and then click the Carry Below\rcommand. When prompted, click on the token you want to carry.\r\r### Drop by Name\rThis command makes a token drop another token it is carrying.\r\rTo use this, select the carrier token and then click the Drop by Name command.\rWhen prompted, enter the name of the token you want to drop.\r\rYou can also cause a token to stop being carried by moving it away from\rthe token carrying it.\r\r### Drop All\rThis command makes a token drop all other tokens it is carrying.\r\rTo use this, select the carrier token and then click the Drop All command.\r\r## Help\r\rIf you experience any issues while using this script or the trap themes,\rneed help using it, or if you have a neat suggestion for a new feature,\rplease shoot me a PM:\rhttps://app.roll20.net/users/46544/stephen-l\ror create a help thread on the Roll20 API forum\r\r## Show Support\r\rIf you would like to show your appreciation and support for the work I do in writing,\rupdating, and maintaining my API scripts, consider buying one of my art packs from the Roll20 marketplace (https://marketplace.roll20.net/browse/search/?keywords=&sortby=newest&type=all&genre=all&author=Stephen%20Lindberg)\ror, simply leave a thank you note in the script's thread on the Roll20 forums.\rEither is greatly appreciated! Happy gaming!\r", + "authors": "Stephen Lindberg", + "roll20userid": 46544, + "useroptions": [ + { + "name": "allowPlayerUse", + "type": "checkbox", + "value": "1", + "checked": "checked", + "description": "Check this if you want to allow your players to access the functionality of this script. Otherwise, only the GM will be allowed to use it." + } + ], + "dependencies": ["HTML Builder"], + "modifies": {}, + "conflicts": [] +}