[Script] Syntax error : Unexpected token )

Hi ! I try to add this script ("new script" button) to my campaign, but I get this error (in API Output Console) : "SyntaxError: Unexpected token )\n at eval (<anonymous>)\n at messageHandler (evalmachine.<anonymous>:284:6)\n at process.<anonymous> (/home/node/d20-api-server/node_modules/tiny-worker/lib/worker.js:60:55)\n at emitTwo (events.js:106:13)\n at process.emit (events.js:194:7)\n at process.nextTick (internal/child_process.js:766:12)\n at _combinedTickCallback (internal/process/next_tick.js:73:7)\n at process._tickCallback (internal/process/next_tick.js:104:9)" The script : on('chat:message',function(msg) { //example roll: !openroll 90 mastery inhumanity // will roll 1d100+90 including open roll values, limited to values below zen, with a reduced chance to fumble from mastery // Adding 'initiative' as a flag will dictate the result with appropriate flags to the initiative tracker. if(!(msg.selected && msg.selected.length > 0)) return; // Make sure there's a selected object var token = getObj('graphic', msg.selected[0]._id); if(token.get('subtype') != 'token') return; // Don't try to perform this command on a drawing or card //code supplied by Brian for determining if a player is speaking in character or not. var name = msg.who; var character = findObjs({ _type: 'character', name: name })[0]; var who; if(character) // Player is speaking as a character { who = 'character|'+character.id; } else // Player is speaking as a player { who = 'player|'+msg.playerid; } //parse the message type if(msg.type != 'api') return; var parts = msg.content.toLowerCase().split(' '); log( 'parts: '+parts); var command = parts.shift().substring(1); //remove the ! if(command == 'openroll') { //Variable Declarations var openFloor = 90; // The minimum result needed to roll again. set to the default described in the core rules. var fumbleCeil = 3; // Rolls below or equal to this number are considered fumbles. set to core rules default. var open; // Flag for the player having rolled a number above openFloor. var dice; // Numberical value of latest dice rolled (1d100). Good for debugging/transparency. var mod = 0; // Variable to keep track of mod throughout number calculations. var rollCount=1; // Number of times dice have been rolled. Good for debugging and fumble validation. var fumbleLevel; // Number used when fumble is achieved. Fumbles depend on context, so little automated use. var total = 0; // The final result of the roll. var inhumanity = false; // Limit rolls below 320 unless this is true. var zen = false; // Limit rolls below 440 unless this is true. var rollTracking=''; // Track rolls in string form for logs / output. var initiative = false; // Flag to set the result after flag calculation to the initiative table. var reset = false; // Flag to reset the initiative window in case pages were changed. var capped = false; // Flag to display to the player when their roll is limited. //loop through the remaining parts for flags and other mods _.each(parts,function(curPart){ if(!isNaN(Number(curPart))) { //add numbers to mod mod+= Number(curPart); }else{ if(curPart == 'inhumanity' || curPart == 'inhuman') inhumanity = true; if(curPart == 'zen') zen = true; if(curPart == 'reset') reset = true; if(curPart == 'initiative') initiative = true; if(curPart == 'complex') { if(!initiative) { //complex doesn't apply to initiative rolls. fumbleCeil+=2; } } if(_.contains(parts, 'mastery')) { if(!initiative) { //mastery doesn't apply to initiative rolls. fumbleCeil--; } } } }); total += mod; log('Openroll Floor: '+openFloor); log('Fumble Ceiling: '+fumbleCeil); //Roll the actual dice already! do{ open=false; // Reset open roll flag. dice = randomInteger(100); // Roll 1d100. log('Dice roll ' +rollCount + ': '+dice); //check if dice result was > openFloor, if so, will roll again. if(dice >= openFloor) { if(openFloor <100)openFloor++; // Increment openFloor, can never exceed 100. sendChat( who,rollCount+' Open Roll! <a style="color:DeepSkyBlue"> <b>('+dice+')</b></a>'); // Bragging / transparency. rollTracking += '<a style="color:DeepSkyBlue"><b>('+dice+')</b></a>+'; // Add dice roll to string for output later. open=true; rollCount++; } else if(dice <= fumbleCeil && rollCount==1) { // if dice result was < fumbleCeil, a fumble has occured. // NOTE: fumbles cannot happen after an open roll occurs, hence the check on rollCount. mod = 0; modArray = [15, 0, -15, -15, -15]; modArrayMastery = [0, -15]; if (fumbleCeil == 2) mod = modArrayMastery[dice-1]; else mod = modArray[dice-1]; fumbleLevel=randomInteger(100) + mod; //roll 1d100 to determine fumble level + modifier (if first dice is a 3, and second dice 80 : fumbleLevel = 80 - 15 log('Fumble Level: '+fumbleLevel); rollTracking += '('+dice+')+'; sendChat( who, 'Fumble! level: ('+fumbleLevel+')'); }else{ rollTracking += '('+dice+')+'; } total += dice; //record the total so far. } while(open); //roll again if openroll occured. //take off the last + in the rollTracking string so the output doesn't look stupid. rollTracking = rollTracking.substring(0,(rollTracking.length)-1); //apply any and all limitations. if(inhumanity && !zen)//only one can apply, as zen overwrites inhumanity. { if(total>439) { capped = true; // the player was limited. dice = total; //reuse variable, save original (uncapped) roll. total = 439; //zen starts at 440. } }else if(!zen) { if(total > 319) { capped = true; // the player was limited. dice = total; // reuse variable, save original (uncapped) roll. total = 319; // standard roll, limit below inhumanity (320). } } //no else case for if(zen), as all rolls above 440 are allowed if the roll has zen. if(capped) { sendChat(who,'/direct <p style="color:GoldenRod"><b>CAPPED! ('+dice+')</b></p>'); } //finally, make sure total result is above 0, as rolls in A:BF cannot be negative, and output. if(total<0) total=0; sendChat(who, '/direct Roll Result: ['+rollTracking+']+('+mod+') = ('+total+')'); //Separate logic for applying the above roll to the current token's Initiative. if(initiative) { var turnorder; //turn object that keeps all initiative information. if(Campaign().get("turnorder") == "" || reset) turnorder = []; //check for reset flag or empty init else turnorder = JSON.parse(Campaign().get("turnorder")); //reject any tokens from turnorder that match this ID . turnorder = _.reject(turnorder,function(curChar){ if(curChar.id == msg.selected[0]._id) //same character return true; }); //push new token onto the turn order. turnorder.push({ id: msg.selected[0]._id, pr: total, }); //If the initiative window is not displayed, open it. Campaign().initiativepage=true; //Add a new custom entry to the end of the turn order. Campaign().set("turnorder", JSON.stringify(turnorder)); } } }); Someone can help me ? Thx ! Max

Edited 1531237409
The Aaron
API Scripter
Try putting a ; (semi-colon) as the very first character of the script: ; on('chat:message',function(msg) Basically what's happening is that all the API scripts get concatenated together in together when the API Sandbox starts up.  Because Javascript was initially designed by neophytes, they thought it would be a good idea to make the ; on the end of traditional C/C++ syntax optional.  However, they didn't foresee all the implications that would have. Whatever your previous script is, it likely doesn't end in a ; and the concatenation is joining the 'on' at the beginning of this script with the end of the prior and making the ( an unexpected character.
Is that because there might be another script loading previously that does not end with a semicolon?
The Aaron
API Scripter
keithcurtis said: Is that because there might be another script loading previously that does not end with a semicolon? yup. =D
Thx for the reply ! Unfortunately this doesn't resolve my problem :( (I don't have another script)
The Aaron
API Scripter
Ok. My next guess will be place your cursor after the on and delete backwards about a dozen times, then retype the on .  Copying from the forum, can often copy invisible unicode characters that cause errors. Those unicode characters are most often at the beginning of the copied test.
I've gotten this error whenever I try to copy/paste the Shaped Companion script from one game to another. Copying/pasting from the raw code on github works though. Dunno why I can't copy/paste from one game to another, but whatever.

Edited 1531240483
Use this and see if you have a basic syntax error.  Copy your code to it and run the code.  It will tell you exactly what line the error is on https://js.do/ Forget: I checked, code is fine.  
The Aaron
API Scripter
I actually ran it against the Google Closure Compiler and had no errors.
This is just a suggestion, but I'd start by commenting out everything starting with if command = openroll and see if it still bombs.  If it does the problem is at the start.  Then start commenting back in a section at a time until you've narrowed down the issue 
Wow ! Your reactivity impresses me ! Effectively the script has no errors (I had checked before post here). No, the error is between the chair and the keyboard... I did not see it was necessary to select the token before typing the command... I'm sorry Max
This also occurs if you try to modify a script from within the API editor (don't do that, by the way!).  Why are Roll20's editors such crap, all of a sudden (API, text, etc)?  I don't remember them being this bad.   Anyway, I get around it, as others have already mentioned, by modifying in an external editor.  I then just copy and paste into the script window.  As long as there weren't already syntax errors within the script, all is well.  Sometimes, read as rarely, restarting the sandbox can clear this.
I did not have be clear : it's me the error ! The script work verry well, but I did not know how to use it. Now I've take the time to read the doc, and everything is fine.