var MarchingOrder = (() => { 'use strict'; /** * Makes a token's followers move to the token's previous position. * @param {Graphic} leader */ function _doFollowMovement(leader) { let follower = leader.follower; follower.prevLeft = follower.get("left"); follower.prevTop = follower.get("top"); follower.prevRotation = follower.get("rotation"); follower.set("left",leader.prevLeft); follower.set("top",leader.prevTop); follower.set("rotation", leader.prevRotation); if(typeof CustomStatusMarkers !== 'undefined') CustomStatusMarkers.repositionStatusMarkers(follower); } /** * Makes a single token follow another token. * @param {Graphic} leader * @param {Graphic} follower */ function follow(leader, follower) { if(!leader || !follower) return; // unbind all of follower's following links. unfollow(follower); let prevFollower = leader.follower; follower.leader = leader; follower.follower = prevFollower; leader.follower = follower; if(prevFollower) prevFollower.leader = follower; } /** * Makes tokens follow each other in order of some compass direction. * @param {Graphic[]} tokens * @param {String} direction */ function followAllDirection(tokens, direction) { tokens.sort((a,b) => { let aX = parseFloat(a.get("left")); let bX = parseFloat(b.get("left")); let aY = parseFloat(a.get("top")); let bY = parseFloat(b.get("top")); if(direction === "north") return (aY - bY); else if(direction === "south") return (bY - aY); else if(direction === "west") return (aX - bX); else // east return (bX - aX); }); setMarchingOrder(tokens); } /** * Makes a chain of tokens follow some other token. * @param {Graphic} follower * @param {Graphic} leader */ function followAllToken(follower, leader) { // Can't follow self. if(follower === leader) return; // Include the follower's previous followers in the marching order. let tokens = [follower]; let next = follower.follower; while(next) { if(next === leader) throw new Error('Cyclical marching orders are not allowed!'); tokens.push(next); next = next.follower; } tokens.unshift(leader); setMarchingOrder(tokens); } /** * Sets a marching order for an array of tokens, with the token at index 0 * being the leader. * @param {Graphic[]} */ function setMarchingOrder(tokens) { _.each(_.range(tokens.length-1), i => { let leader = tokens[i]; let follower = tokens[i+1]; MarchingOrder.utils.Chat.broadcast(follower.get("name") + " is following " + leader.get("name")); follow(leader, follower); }); } /** * Makes a token stop following other tokens. * @param {Graphic} token */ function unfollow(token) { if(token.leader) token.leader.follower = token.follower; if(token.follower) token.follower.leader = token.leader; token.leader = null; token.follower = null; } /** * Makes all tokens stop following each other. */ function unfollowAll() { let allObjs = findObjs({ _type: 'graphic', layer: 'objects' }); _.each(allObjs, obj => { unfollow(obj); }); MarchingOrder.utils.Chat.broadcast("Tokens are no longer following each other."); } /** * Applies the default marching order to the page that currently has the * player ribbon. */ function useDefaultMarchingOrder() { let playerpageid = Campaign().get('playerpageid'); let tokens = []; let defaultOrder = MarchingOrder.Config.getDefaultMarchingOrder(); _.each(defaultOrder, item => { let token = findObjs({ _type: 'graphic', _pageid: playerpageid, represents: item.represents })[0]; if(token) tokens.push(token); }); setMarchingOrder(tokens); } // When the API is loaded, install the Custom Status Marker menu macro // if it isn't already installed. on('ready', () => { MarchingOrder.Macros.installMacros(); log('--- Initialized Marching Order v2.4 ---'); }); /** * Set up an event handler to do the marching order effect when the * leader tokens move! */ on("change:graphic", (obj, prev) => { try { let leader = obj; leader.prevLeft = prev["left"]; leader.prevTop = prev["top"]; leader.prevRotation = prev["rotation"]; // Only move the followers if there was a change in either the leader's // left or top attributes. if(leader.get("left") !== leader.prevLeft || leader.get("top") !== leader.prevTop) { // We stepped out of line. Stop following the guy in front of us. if(leader.leader) unfollow(leader); // move everyone to the previous position of the token in front of them. while(leader.follower) { _doFollowMovement(leader); leader = leader.follower; // avoid cycles. if(leader === obj) return; } } } catch(err) { MarchingOrder.utils.Chat.error(err); } }); return { follow: follow, followAllDirection, followAllToken, set: setMarchingOrder, unfollow, unfollowAll, useDefaultMarchingOrder }; })(); /** * This module defines and implements the chat commands used by this script. */ (() => { 'use strict'; const MENU_CMD = '!showMarchingOrderMenu'; const FOLLOW_CMD = '!marchingOrderFollow'; const STOP_ALL_CMD = '!marchingOrderStopAll'; const DEFAULT_USE_CMD = '!marchingOrderUseDefault'; const DEFAULT_SET_CMD = '!marchingOrderSetDefault'; /** * Extracts the selected graphics from a chat message. * @param {ChatMessage} msg * @return {Graphic[]} */ function _getGraphicsFromMsg(msg) { var result = []; var selected = msg.selected; if(selected) { _.each(selected, s => { let graphic = getObj('graphic', s._id); if(graphic) result.push(graphic); }); } return result; } /** * Process an API command to have tokens follow each other. */ function _cmdFollow(msg) { let argv = msg.content.split(' '); let dirMatch = argv[1].match(/(north|south|east|west)/); if(dirMatch) { let selected = _getGraphicsFromMsg(msg); MarchingOrder.followAllDirection(selected, dirMatch[0]); } else { let follower = getObj('graphic', argv[1]); let leader = getObj('graphic', argv[2]); if(follower && leader) MarchingOrder.followAllToken(follower, leader); else throw new Error(`Invalid arguments given for FOLLOW_CMD: ${argv.slice(1)}`); } } /** * Process an API command to set the default marching order. */ function _cmdSetDefaultMarchingOrder(msg) { let argv = msg.content.split(' '); let leader = getObj('graphic', argv[1]); if (leader) { MarchingOrder.Config.setDefaultMarchingOrder(leader); menu(msg); } else throw new Error(`Leader token not found for DEFAULT_SET_CMD: ${argv.slice(1)}`); } function _cmdUnfollowAll(msg) { _.noop(msg); MarchingOrder.unfollowAll(); } /** * Process an aPI command to use the default marching order. */ function _cmdUseDefaultMarchingOrder(msg) { _.noop(msg); MarchingOrder.useDefaultMarchingOrder(); } /** * Processes an API command to display the script's main menu. */ function menu(msg) { let player = getObj('player', msg.playerid); MarchingOrder.Wizard.show(player); } // Event handler for the script's API chat commands. on('chat:message', msg => { let argv = msg.content.split(' '); try { if(argv[0] === MENU_CMD) menu(msg); else if (argv[0] === DEFAULT_SET_CMD) _cmdSetDefaultMarchingOrder(msg); else if (argv[0] === DEFAULT_USE_CMD) _cmdUseDefaultMarchingOrder(msg); else if (argv[0] === FOLLOW_CMD) _cmdFollow(msg); else if (argv[0] === STOP_ALL_CMD) _cmdUnfollowAll(msg); } catch(err) { MarchingOrder.utils.Chat.error(err); } }); /** * Expose the command constants for use in other modules. */ MarchingOrder.Commands = { DEFAULT_SET_CMD, DEFAULT_USE_CMD, FOLLOW_CMD, MENU_CMD, STOP_ALL_CMD }; })(); (() => { 'use strict'; /** * Module for global script configurations. */ MarchingOrder.Config = class { /** * Get the configured default marching order. */ static getDefaultMarchingOrder() { return MarchingOrder.State.getState().defaultOrder; } /** * Set the configured default marching order. * @param {Graphic} leader */ static setDefaultMarchingOrder(leader) { let items = []; let next = leader; while(next) { let represents = next.get('represents'); if(!represents) throw new Error('All tokens in the default marching order must represent a character.'); items.push({ represents, imgsrc: next.get('imgsrc'), name: next.get('name') }); next = next.follower; } MarchingOrder.State.getState().defaultOrder = items; } }; })(); (() => { 'use strict'; /** * Installs/updates a macro for the script. * @param {string} name * @param {string} action */ function _installMacro(player, name, action) { let macro = findObjs({ _type: 'macro', _playerid: player.get('_id'), name })[0]; if(macro) macro.set('action', action); else { createObj('macro', { _playerid: player.get('_id'), name, action }); } } /** * This module is responsible for installing and updating the macros * used by this script. */ MarchingOrder.Macros = class { /** * Installs/updates the macros for this script. */ static installMacros() { let players = findObjs({ _type: 'player' }); const Commands = MarchingOrder.Commands; // Create the macro, or update the players' old macro if they already have it. _.each(players, player => { _installMacro(player, 'MarchingOrderMenu', Commands.MENU_CMD); }); } }; })(); (() => { 'use strict'; /** * This module provides an interface to the script's state. */ MarchingOrder.State = class { /** * Displays the JSONified state for this script to the chat. * @param {Player} player * @return {string} */ static exportState(player) { let json = MarchingOrder.State.jsonifyState(); let content = `
${json}`;
let menu = new MarchingOrder.utils.Menu('Export Marching Order', content);
menu.show(player);
return json;
}
/**
* Gets the script's configured options.
* @return {Object}
*/
static getOptions() {
let scriptState = MarchingOrder.State.getState();
if(!scriptState.options)
scriptState.options = {};
return scriptState.options;
}
/**
* Returns this module's object for the Roll20 API state.
* @return {Object}
*/
static getState() {
if(!state.marchingOrder)
state.marchingOrder = {};
_.defaults(state.marchingOrder, {
defaultOrder: []
});
return state.marchingOrder;
}
/**
* Imports the state for this script from JSON.
* @param {Player} player
* @param {string} json
*/
static importState(player, json) {
let scriptState = MarchingOrder.State.getState();
_.extend(scriptState, JSON.parse(json));
MarchingOrder.Wizard.show(player);
}
/**
* Gets the JSON string for this script's state.
* @return {string}
*/
static jsonifyState() {
let scriptState = MarchingOrder.State.getState();
return JSON.stringify(scriptState);
}
};
})();
(() => {
'use strict';
MarchingOrder.Wizard = class {
/**
* Create an instance of the main menu.
* @param {Player} player
* @return {MarchingOrder.utils.Menu}
*/
static getMainMenu(player) {
let playerId = player.get('_id');
let moState = MarchingOrder.State.getState();
const Commands = MarchingOrder.Commands;
const Menu = MarchingOrder.utils.Menu;
// Menu options
let actionsHtml = '| [North](' + Commands.FOLLOW_CMD + ' north) | ||
| [West](' + Commands.FOLLOW_CMD + ' west) | [East](' + Commands.FOLLOW_CMD + ' east) | |
| [South](' + Commands.FOLLOW_CMD + ' south) |