Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Script] SimpleDoorControls

Edit: The Aaron rewrote my original script in a post below. I recommend using his version over mine. It is cleaner and less prone to potential breakage. Greetings! After manually opening doors and moving Dynamic Lighting objects for a few game sessions, I decided I needed to bump to Mentor to automate it. Unfortunately I wasn't completely happy with Matt's implementation that can be found in the link below. (I didn't like the whole "gear thing" and I don't need the traps feature). <a href="https://app.roll20.net/forum/post/636123/script-do" rel="nofollow">https://app.roll20.net/forum/post/636123/script-do</a>... So I had to write my own. This is my first time using JavaScript so I know there is probably a better way to do it. But my programming experience is limited to some VBA coding for Excel and when I used to write in COBOL and FORTRAN77 many years ago. So while not a complete noob, my knowledge of modern language structures, coding concepts, naming standards, etc is minimal. Thus I warn you the code is VERY ugly. I'm not even sure I should share it. I just figured it it helps one other person, it's worth posting. If the excellent coders on site could want to clean it up, make it more robust, add some error checking, etc, that's fine. So w/o further ado... Script Code /* SimpleDoorControls Use Creating a linked door system ----------------------------- Verify you're on the Token/Object Layer. Place a graphic of a door or other barrier in the closed position. Name that token DoorClosed (all one word, and yes the D and C need to be capitalized). Place a graphic of a door in the desired open position. Name that token DoorOpen (all one word, and yes the D and O need to be capitalized). Draw a line to act as the Dynamic Lighting barrier for the door when closed (while still on the Token layer). Select all three objects. Type !DoorsLink (or set it as a macro and use that macro) The Open Door and Dynamic Lighting barrier will be moved to the GM layer and Dynamic Lighting layer, respectively. The open and closed doors will be renamed. This is how they are linked. DO NOT RENAME THE LINKED OBJECTS. Do this for each set of doors needed. Opening and Closing doors ------------------------- Once the objects are linked Select the door on the token layer Type !DoorOpenClose The door will open if it is closed, or it will close if it is opened. */ on("chat:message", function (msg) { var DoorOpenID; var DoorClosedID; var PathID; var cmdNames = ["!DoorsLink","!DoorOpenClose"]; switch(msg.content.split(" ")[0]) { case "!DoorsLink": //make sure three items are selected try { if (msg.selected.length != 3) throw "invalid selection"; } catch (err) { sendChat("Doors", "/w GM Select and place a graphic named 'DoorClosed', a graphic named 'DoorOpen' and a path to use as a wall."); break; } //identify and get the ID for each each type of selected item _.each(msg.selected, function(obj) { try { var o = getObj(obj["_type"], obj["_id"]); if (o.get("_type") == "graphic" && o.get("name") == "DoorOpen") { DoorOpenID = o.get("_id"); } else if (o.get("_type") == "graphic" && o.get("name") == "DoorClosed") { DoorClosedID = o.get("_id"); } else if (o.get("_type") == "path") { PathID = o.get("_id"); } } catch (err) { } }); //move each item to the appropriate layer && //rename each door with the ID of the Path followed by the ID of the other door _.each(msg.selected, function(obj) { try { var o = getObj(obj["_type"], obj["_id"]); if (o.get("_type") == "graphic" && o.get("name") == "DoorOpen") { o.set("name",PathID + " " + DoorClosedID); o.set("layer","gmlayer"); } else if (o.get("_type") == "graphic" && o.get("name") == "DoorClosed") { o.set("name",PathID + " " + DoorOpenID); } else if (o.get("_type") == "path") { o.set("layer","walls"); } } catch (err) { } }); break; case "!DoorOpenClose": _.each(msg.selected, function(obj) { try { var o = getObj(obj["_type"], obj["_id"]); var currDoorID = o.get("_id"); o.set("layer","gmlayer"); //Determine ID of other two objects var str = o.get("name"); var pathID = str.slice(0,20); var otherDoorID = str.slice(21,41); //Move other objects to appropriate layers var otherDoor = getObj("graphic",otherDoorID); otherDoor.set("layer","objects"); var Path = getObj("path",pathID); var currPathLayer = Path.get("layer"); if (currPathLayer == "walls") { Path.set("layer","gmlayer"); } else { Path.set("layer","walls"); } } catch (err) { } }); break; } });
1419744752

Edited 1419744795
Lithl
Pro
Sheet Author
API Scripter
Your code isn't that ugly. I cringe at empty catch blocks in any language (and you could probably get rid of those catches entirely with some null guards), and it would be better to separate your different commands into different functions instead of one large switch, but overall the style is pretty good! =)
Thanks for the kind words. Normally I would have used functions, but I wanted to keep it simple for my first foray into the language. Then after debugging it for longer than I care to mention because I tried to send an object to a "layber" rather than a "layer" and received no error message (so I assumed I was using one of the prior functions incorrectly rather than the error being a typo), I declared it 'good enough!'
1419792949
The Aaron
Pro
API Scripter
=D If you're planing to do more scripting and want to up your JS ante, I highly recommend Javascript: The Good Parts by Douglas Crockford . It's a pretty quick read, and you obviously already know how to program, so you should get quite a bit out of it regarding the finer points of the Javascript language. =D Personally, I have mixed feelings about exception handling in Javascript. You no doubt noticed that nothing gets passed to the catch portion, making it pretty difficult to actually handle the exception, or even give a reasonable error message. As Brian mentioned, it would be better off using some "null guards" as he calls them. Here's one portion rewritten with a guard: _.each(msg.selected, function(obj) { var o = getObj(obj._type, obj._id); if(o) { if (o.get("type") === "graphic" && o.get("name") === "DoorOpen") { DoorOpenID = o.id; } else if (o.get("type") === "graphic" && o.get("name") === "DoorClosed") { DoorClosedID = o.id; } else if (o.get("type") === "path") { PathID = o.id; } } }); Here's a few other notes: In Javascript, it's more idiomatic to use the dot notation for referencing properties (though you can still use the array notation, and must for property names stored in variables or which contain spaces or other problem characters. For Roll20 objects, the leading underscore denotes a read only property, but it's preferable to omit it when using the .get() function. That will allow your code to still work later if they make a property read/write. Roll20 objects have an id property that is shorthand for obj.get('id'). Anyway, nice script contribution, keep them coming!!
1419793040
The Aaron
Pro
API Scripter
Oh, and I want to reiterate what Brian said about your code not being so ugly. It's better written than a lot of scripts I see an I imagine your scripts will only get better. =D
1419794595

Edited 1419794623
vÍnce
Pro
Sheet Author
I want to say thanks for contributing and I like the use of descriptive text within the code. This helps us noobs(me) better understand how the script functions. Thank you.
1419794739
The Aaron
Pro
API Scripter
Ouch Vince! Guess I'll have to make annotated versions of some of my scripts.... :D Maybe I'll add a section to the wiki with annotated code snippets....
Thanks for the response and information, Aaron. A question about something you said: In Javascript, it's more idiomatic to use the dot notation for referencing properties (though you can still use the array notation, and must for property names stored in variables or which contain spaces or other problem characters. Are you referring to this line of code: switch(msg.content.split(" ")[0]) { If so, that was copied from elsewhere and to be honest, I'm not sure what it's actually doing. I just used it because it worked. :) In either case, if you could clarify what you mean, I'd appreciate it! Edit: I think I just logically figured out what it does. It places each word into an array (assuming words are split by spaces). But I still don't know what you mean by using dot notation. :) Vince, we noobs have to stick together! Being a noob myself, I realized I should explicitly state how it's used since it's supposed to be 'simple'. I'm glad you liked it.
1419805377

Edited 1419805505
Lithl
Pro
Sheet Author
API Scripter
Three of Swords said: Thanks for the response and information, Aaron. A question about something you said: In Javascript, it's more idiomatic to use the dot notation for referencing properties (though you can still use the array notation, and must for property names stored in variables or which contain spaces or other problem characters. Are you referring to this line of code: switch(msg.content.split(" ")[0]) { If so, that was copied from elsewhere and to be honest, I'm not sure what it's actually doing. I just used it because it worked. :) In either case, if you could clarify what you mean, I'd appreciate it! Edit: I think I just logically figured out what it does. It places each word into an array (assuming words are split by spaces). But I still don't know what you mean by using dot notation. :) No, here Aaron is talking about something like this: var o = getObj(obj["_type"], obj["_id"]); Which can be equivalently written as: var o = getObj(obj._type, obj._id); If the property name contained problematic characters such as spaces, you wouldn't be able to do that ( obj["my property name"] would be required, as obj.my property name obviously doesn't work). The former syntax is also necessary if you have the name of the property you're trying to access already stored in a variable. Note that the above equivalency only applies to plain old JavaScript objects, not Roll20 objects such as you might get as a return value from the getObj function. Roll20 object properties need to be get and set using the get and set functions (with the exception of getting the object's id, for which there is a property you can use). As for what the switch code is doing: msg.content contains the text of the message that was sent split(" ") separates the string into an array (splitting on spaces, since that's the parameter given). The string "Foo bar baz" becomes the array ["Foo", "bar", "baz"]. [0] accesses the first element of the array So, switch(msg.content.split(" ")[0]) is going to switch on the part of the input that comes before the first space. Personally, I am a fan of something such as: var parts = msg.content.split(" "); var command = parts.shift().substring(1); The shift() function removes the first element from the array and returns it, and then substring(1) takes the first character off the string (which, for an API command, will always be an exclamation mark). At this point, command will be the name of the command the user is trying to use, such as "DoorsLink" or "DoorOpenClose", while parts will be an array of all of the parameters passed to the command.
1419805897
vÍnce
Pro
Sheet Author
The Aaron said: Ouch Vince! Guess I'll have to make annotated versions of some of my scripts.... :D Maybe I'll add a section to the wiki with annotated code snippets.... How did you know that was directed at you Aaron? :-) There are " some " scripts that are void of much, if any, description. Most of those are rather simple one-trick-ponies that you probably wrote while DMing a game on roll20, reading the latest Dresden Files book, while practicing violin... Actually, I really appreciate your well-formated "help" screens.
1419806179
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Vince said: The Aaron said: Ouch Vince! Guess I'll have to make annotated versions of some of my scripts.... :D Maybe I'll add a section to the wiki with annotated code snippets.... How did you know that was directed at you Aaron? :-) There are " some " scripts that are void of much, if any, description. Most of those are rather simple one-trick-ponies that you probably wrote while DMing a game on roll20, reading the latest Dresden Files book, while practicing violin... Actually, I really appreciate your well-formated "help" screens. Don't let him walk it back Aaron. It was a clear, direct and personal attack. I, for one, was mortified by it.
1419806516
vÍnce
Pro
Sheet Author
Why is everyone so mean to me?
1419845060
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Because its a well organized plot. We meet on Tuesdays, everyone is there, its usually a pretty good time.
1419845924
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Place a graphic of a door or other barrier in the closed position. Name that token DoorClosed (all one word, and yes the D and C need to be capitalized). Place a graphic of a door in the desired open position. Name that token DoorOpen (all one word, and yes the D and O need to be capitalized). Draw a line to act as the Dynamic Lighting barrier for the door when closed (while still on the Token layer). You know.... reading through this. Is the same old chestnut.... we need tags or something. And name field for path data, I can't name a path.
1419870862
The Aaron
Pro
API Scripter
Vince said: The Aaron said: Ouch Vince! Guess I'll have to make annotated versions of some of my scripts.... :D Maybe I'll add a section to the wiki with annotated code snippets.... How did you know that was directed at you Aaron? :-) There are " some " scripts that are void of much, if any, description. Most of those are rather simple one-trick-ponies that you probably wrote while DMing a game on roll20, reading the latest Dresden Files book, while practicing violin... Actually, I really appreciate your well-formated "help" screens. :) I'm personally not a huge fan of comments in source code, for a couple reasons. They get stale and they tend to be a crutch against writing descriptive code. That said, I could be a bit more descriptive with some of my variable names. My my preference would be to take snippets of code and fully describe them for people, rather than cluttering up my scripts with a bunch of text (Not that I'm offended if others comment theirs! Just my preference. ). I've done that for quite a few people via PM or in the course of other discussions.
1419881493
Lithl
Pro
Sheet Author
API Scripter
The Aaron said: I'm personally not a huge fan of comments in source code, for a couple reasons. They get stale and they tend to be a crutch against writing descriptive code. I've been working on the Best Practices page on the wiki, so I'm gonna quote what I've written there: Commenting can be something of a holy war among programmers. Some say you need more, some say your code should be enough, and so on. However, understand that for a Roll20 API script, the person reading your code may not be a programmer at all, and is completely bewildered by your black magic... but they still need to make modifications in order to suit their game. At the very least, comments describing your configuration variables are helpful to everyone who installs your script, and comments describing generally what's going on in each section of code can help the layperson trying to struggle his or her way through making the tabletop experience better. A common mantra about commenting code is that your names should describe what the code does, while your comments describe why the code does that. Personally, I'm very much in the camp of your code should describe what while comments should describe why .
1419882290
The Aaron
Pro
API Scripter
Yeah, I read what you wrote and we can agree on the self documenting code. =D I'm not a huge fan of the name "Best Practices" either, but this is not the right thread for that discussion. =D
Stephen S. said: And name field for path data, I can't name a path. I was very surprised I couldn't name a path. It's not a big deal and wouldn't have made it easier, but there are probably some situations where it might.
Is it possible to get a troubleshoot for this? I'm getting an TypeError: Cannot call method 'get' of undefined at evalmachine.&lt;anonymous&gt;:474:15 at eval ( and not really sure what to do, the script works just fine on the campaign I had it running on, but trying to move it got this when trying to use the !DoorsLink command
Askren said: Is it possible to get a troubleshoot for this? I'm getting an TypeError: Cannot call method 'get' of undefined at evalmachine.&lt;anonymous&gt;:474:15 at eval ( and not really sure what to do, the script works just fine on the campaign I had it running on, but trying to move it got this when trying to use the !DoorsLink command I'm far from an expert on JavaScript, so any help I could provide is minimal, but I'm trying to duplicate your error and I cannot. I thought the Try portion of the code would prevent errors like that, but apparently not. Any additional information you can provide? Did you modify the script at all?
1423079774

Edited 1423114721
The Aaron
Pro
API Scripter
Probably the error is here: var pathID = str.slice(0,20); var otherDoorID = str.slice(21,41); This assumes the length of an ID will be 20 characters, but I don't think that's a good idea. If the path ID was a character shorter or longer, it would make one of both of the IDs read back incorrect. That would lead to getObj() returning undefined and then an error when you try to get() or set on it. Try this version: /* SimpleDoorControls Use Creating a linked door system ----------------------------- Verify you're on the Token/Object Layer. Place a graphic of a door or other barrier in the closed position. Name that token DoorClosed (all one word, and yes the D and C need to be capitalized). Place a graphic of a door in the desired open position. Name that token DoorOpen (all one word, and yes the D and O need to be capitalized). Draw a line to act as the Dynamic Lighting barrier for the door when closed (while still on the Token layer). Select all three objects. Type !DoorsLink (or set it as a macro and use that macro) The Open Door and Dynamic Lighting barrier will be moved to the GM layer and Dynamic Lighting layer, respectively. The open and closed doors will be renamed. This is how they are linked. DO NOT RENAME THE LINKED OBJECTS. Do this for each set of doors needed. Opening and Closing doors ------------------------- Once the objects are linked Select the door on the token layer Type !DoorOpenClose The door will open if it is closed, or it will close if it is opened. */ on("chat:message", function (msg) { "use strict"; var Parts = {}; if (msg.type !== "api") { return; } switch(msg.content.split(/\s+/).shift()) { case "!DoorsLink": //make sure three items are selected if (msg.selected.length !== 3) { sendChat("Doors", "/w GM Select and place a graphic named 'DoorClosed', a graphic named 'DoorOpen' and a path to use as a wall."); break; } //identify and get the ID for each each type of selected item _.each(msg.selected, function(obj) { var o = getObj(obj._type, obj._id); if(o) { if (o.get("_type") === "graphic" && o.get("name") === "DoorOpen") { Parts.DoorOpen=o; } else if (o.get("type") === "graphic" && o.get("name") === "DoorClosed") { Parts.DoorClosed = o; } else if (o.get("type") === "path") { Parts.Path = o; } } }); if( Parts.DoorOpen && Parts.DoorClosed && Parts.Path) { Parts.DoorOpen.set({ name: Parts.Path.id +" "+Parts.DoorClosed.id, layer: "gmlayer" }); Parts.DoorClosed.set({ name: Parts.Path.id +" "+Parts.DoorOpen.id }); Parts.Path.set({ layer: "walls" }); } break; case "!DoorOpenClose": _.chain(msg.selected) .map(function(o){ return getObj('graphic', o._id); }) .reject(_.isUndefined) .filter(function(o){ return 'objects' === o.get('layer'); }) .each(function(o){ var params=o.get('name').split(/\s+/), oDoor = getObj('graphic',params[1]), oPath = getObj('path',params[0]); if(oDoor && oPath) { o.set({ layer: 'gmlayer' }); oDoor.set({ layer: 'objects' }); oPath.set({ layer: ( 'walls' === oPath.get('layer') ? 'gmlayer' : 'walls') }); } else { sendChat('Doors','/w gm Missing components: &lt;ul&gt;' +(oDoor ? '' : '&lt;li&gt;GM Layer Door&lt;/li&gt;') +(oPath ? '' : '&lt;li&gt;Dynamic Lighting Path&lt;/li&gt;') +'&lt;/ul&gt;'); } }); break; } });
I disabled the old and put Aaron's version in a new script, and now the !DoorsLink gives a return of Unexpected identifier Again, I am not sure why this seems to work on the original campaign I installed it on, but not this one. I didn't modify the script at all.
Aaron, your code is much more elegant than mine. I like it, and thank you for cleaning it up for me. I'll edit the original post to recommend others use it instead. However, when using the Link command, you move both the Closed and Open doors to the GM layer. You'll want to edit that to only move the Open door to the GM layer.
Askren, it does seem odd that it works fine in one campaign and not another. A couple of suggestions, though you've probably already tried them: Set up a new test campaign to try it on (rather than a copied one). Make sure this is the only script installed and see if you get the same result. If that works, disable all scripts but the SimpleDoorsControl in the campaign where it fails, and test it. Create a new page, doors, etc and see what happens. Maybe we can isolate the issue and solve it.
1423115253
The Aaron
Pro
API Scripter
Three of Swords said: Aaron, your code is much more elegant than mine. I like it, and thank you for cleaning it up for me. I'll edit the original post to recommend others use it instead. However, when using the Link command, you move both the Closed and Open doors to the GM layer. You'll want to edit that to only move the Open door to the GM layer. Whoops! Fixed. Glad you like it, now if only it fixed his problem. I didn't get any issues when JSLint'd it. I think unexpected identifier is a code structure error, not a data error. That makes me think there is something weird going on with interaction with other scripts. I think disabling other scripts is the right thing to try. You might also try executing a fictitious api command like !foo and see it you get an error. Some api scripts are not careful about what they process.
1423116113

Edited 1423116949
EDIT: Alright, I think I found out what's wrong, and it's entirely my fault. Apparently Firefox has started highlighting bits of the thread and posting them with the script. This has never happened before, so I kind of didn't even think to check for it, but I saw it happen when I pasted the code to a new campaign, and decided to check my other, and it had happened there too. However, after fixing it, the script still throws the same error as before, so apparently it's not playing nice on the actual live campaign I want to use it on. It does seem that it sorta works if I disable every other script, but I say sorta because !DoorsLink just throws the chat message even when I've got the properly-named objects selected.
1423142081
The Aaron
Pro
API Scripter
What is "the other script"? If you PM me a join link and GM me, I can track down the issue for you.
Sounds like you figured it out, Askren. Mind sharing what it was in case someone else makes the same mistake? If nothing else, your mistake got Aaron to clean up my kiddie-script into an adult version. Thanks again, Aaron! I had meant to go back and clean it up now that I know a little more, but never got around to it.
1423153779
The Aaron
Pro
API Scripter
No worries. =D I left it in the same general style so you could compare and contrast. I'm always happy to help with this sort of thing. =D I'd still like to take a peek at Askren's script setup, just to be certain. 3oS, have you tried my version of your script? (I'll be honest.. I haven't actually tested it.. =D )
I did test your script, Aaron. That's how I noticed that it moved both door tokens to the GM layer. Once I changed that, it worked to both Link and open/close doors. My campaign is on a break right now. Once we get going again, your script will get more testing since I've converted to it, but I'm pretty sure nothing will break.
1423165234
The Aaron
Pro
API Scripter
Cool deal! =D
1423168910
The Aaron
Pro
API Scripter
Ok. Found the issue: He had more than 3 things selected when running the command. There was an extra copy of one of the doors. I modified my version of your script to account for that and give reasonable feedback: /* SimpleDoorControls Use Creating a linked door system ----------------------------- Verify you're on the Token/Object Layer. Place a graphic of a door or other barrier in the closed position. Name that token DoorClosed (all one word, and yes the D and C need to be capitalized). Place a graphic of a door in the desired open position. Name that token DoorOpen (all one word, and yes the D and O need to be capitalized). Draw a line to act as the Dynamic Lighting barrier for the door when closed (while still on the Token layer). Select all three objects. Type !DoorsLink (or set it as a macro and use that macro) The Open Door and Dynamic Lighting barrier will be moved to the GM layer and Dynamic Lighting layer, respectively. The open and closed doors will be renamed. This is how they are linked. DO NOT RENAME THE LINKED OBJECTS. Do this for each set of doors needed. Opening and Closing doors ------------------------- Once the objects are linked Select the door on the token layer Type !DoorOpenClose The door will open if it is closed, or it will close if it is opened. */ on("chat:message", function (msg) { "use strict"; var Parts = {}; if (msg.type !== "api") { return; } switch(msg.content.split(/\s+/).shift()) { case "!DoorsLink": //make sure three items are selected if (msg.selected.length &gt; 3) { sendChat("Doors", "/w gm You have selected too many things, looking among them for what I need."); } else if (msg.selected.length &lt; 3) { sendChat("Doors", "/w gm You have not selected enough things."); break; } //identify and get the ID for each each type of selected item _.each(msg.selected, function(obj) { var o = getObj(obj._type, obj._id); if(o) { if (o.get("_type") === "graphic" && o.get("name") === "DoorOpen" && !Parts.DoorOpen) { Parts.DoorOpen=o; } else if (o.get("type") === "graphic" && o.get("name") === "DoorClosed" && !Parts.DoorClosed) { Parts.DoorClosed = o; } else if (o.get("type") === "path" && !Parts.Path) { Parts.Path = o; } } }); if( Parts.DoorOpen && Parts.DoorClosed && Parts.Path) { Parts.DoorOpen.set({ name: Parts.Path.id +" "+Parts.DoorClosed.id, layer: "gmlayer" }); Parts.DoorClosed.set({ name: Parts.Path.id +" "+Parts.DoorOpen.id }); Parts.Path.set({ layer: "walls" }); } else { sendChat("Doors", "/w GM Couldn't fine required piece:&lt;ul&gt;" +(Parts.DoorOpen ? '' : '&lt;li&gt;Token named "DoorOpen".&lt;/li&gt;') +(Parts.DoorClosed ? '' : '&lt;li&gt;Token named "DoorClosed".&lt;/li&gt;') +(Parts.Path ? '' : '&lt;li&gt;Path for Dynamic Light Layer.&lt;/li&gt;') +'&lt;/ul&gt;' ); } break; case "!DoorOpenClose": _.chain(msg.selected) .map(function(o){ return getObj('graphic', o._id); }) .reject(_.isUndefined) .filter(function(o){ return 'objects' === o.get('layer'); }) .each(function(o){ var params=o.get('name').split(/\s+/), oDoor = getObj('graphic',params[1]), oPath = getObj('path',params[0]); if(oDoor && oPath) { o.set({ layer: 'gmlayer' }); oDoor.set({ layer: 'objects' }); oPath.set({ layer: ( 'walls' === oPath.get('layer') ? 'gmlayer' : 'walls') }); } else { sendChat('Doors','/w gm Missing components: &lt;ul&gt;' +(oDoor ? '' : '&lt;li&gt;GM Layer Door&lt;/li&gt;') +(oPath ? '' : '&lt;li&gt;Dynamic Lighting Path&lt;/li&gt;') +'&lt;/ul&gt;'); } }); break; } });
The Aaron said: Ok. Found the issue: He had more than 3 things selected when running the command. Now I'm a little confused. I thought my code accounted for that. I guess not. I any case, thank you once again for your effort! I think I speak on behalf of the other Mentors when i say that I hope you never tire of gaming and using roll20. Your contributions here are immeasurable.
1423170798
The Aaron
Pro
API Scripter
Your code: if (msg.selected.length != 3) throw "invalid selection"; My code: if (msg.selected.length !== 3) { sendChat("Doors", "/w GM Select and place a graphic named 'DoorClosed', a graphic named 'DoorOpen' and a path to use as a wall."); break; } My new code: //make sure three items are selected if (msg.selected.length &gt; 3) { sendChat("Doors", "/w gm You have selected too many things, looking among them for what I need."); } else if (msg.selected.length &lt; 3) { sendChat("Doors", "/w gm You have not selected enough things."); break; } // [...] if( Parts.DoorOpen && Parts.DoorClosed && Parts.Path) { // [...] } else { sendChat("Doors", "/w GM Couldn't fine required piece:&lt;ul&gt;" +(Parts.DoorOpen ? '' : '&lt;li&gt;Token named "DoorOpen".&lt;/li&gt;') +(Parts.DoorClosed ? '' : '&lt;li&gt;Token named "DoorClosed".&lt;/li&gt;') +(Parts.Path ? '' : '&lt;li&gt;Path for Dynamic Light Layer.&lt;/li&gt;') +'&lt;/ul&gt;' ); } So, it will break out with an error if there aren't enough things selected, but only warns in the case that there are more than 3 selected. Later on, it verifies that each of the 3 parts are available, and if something is missing, it will list out what it was. You're welcome! I've told people that I actually enjoy writing the scripts more than playing the games. =D I don't think I'll be going anywhere anytime soon. =D