'
rowCount = 1
}
listContents += getDefaultIcon(condition.iconType,condition.icon,'display:inline-block;margin-right:3px')
listContents += ''+condition.name+''
if (state[combatState].config.status.userChanges && who != 'gm') {
listContents += ''+addButton+removeButton+''
} else {
listContents += ''+addButton+removeButton+favoriteButton+conditionButton+''
}
listContents += '
'
if (state[combatState].config.status.showConditions == 'favorites') {
if (condition.favorite) {
listItems.push(listContents);
}
} else {
listItems.push(listContents);
}
}
//send menu
state[combatState].config.previousPage = 'main'
if (state[combatState].config.status.access) {
let playerIDs = state[combatState].config.status.access.split(',');
playerIDs.forEach((player) => {
makeAndSendMenu(contents+makeList(listItems),titleText,player);
})
}
if (who == 'gm') {
makeAndSendMenu(contents+makeList(listItems),titleText,who);
} else {
makeAndSendMenu(makeList(listItems),titleText,who);
}
},
sortObject = function (obj) {
return Object.keys(obj).sort().reduce(function (result, key) {
result[key] = obj[key];
return result;
}, {});
},
sendConfigMenu = function() {
let configIntiativeButton = makeBigButton('Initiative', '!cmaster --show,initiative')
let configTurnorderButton = makeBigButton('Turnorder', '!cmaster --show,turnorder')
let configTimerButton = makeBigButton('Timer', '!cmaster --show,timer')
let configAnnouncementsButton = makeBigButton('Announce', '!cmaster --show,announce')
let configMacroButton = makeBigButton('Macro & API', '!cmaster --show,macro')
let configStatusButton = makeBigButton('Status', '!cmaster --show,status')
let configConditionButton = makeBigButton('Conditions', '!cmaster --show,conditions')
let exportButton = makeBigButton('Export', '!cmaster --show,export')
let importButton = makeBigButton('Import', '!cmaster --import,config=?{Config}')
let resetButton = makeBigButton('Reset', '!cmaster --reset')
let helpButton = makeImageButton('!cmaster --help,setup',helpImage,'Help','transparent',18,'white')
let backToTrackerButton = makeBigButton('Back', '!cmaster --back,tracker')
let titleText = 'Setup'+'
'
let contents
contents = combatHeaderText
contents += configIntiativeButton
contents += configTurnorderButton
contents += configTimerButton
contents += configAnnouncementsButton
contents += configMacroButton
contents += statusHeadersText
contents += configStatusButton
contents += configConditionButton
contents += exportButton
contents += importButton
contents += resetHeaderText
contents += resetButton
contents += backToTrackerText
contents += backToTrackerButton
makeAndSendMenu(contents, titleText, 'gm');
},
sendInitiativeMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let listItems = []
let helpButton = makeImageButton('!cmaster --help,initiative',helpImage,'Help','transparent',18,'white')
let titleText = 'Initiative Setup'+'
'
let initiative = state[combatState].config.initiative;
listItems.push(makeTextButton('Roll Initiative', initiative.rollInitiative, '!cmaster --config,initiative,key=rollInitiative,value=?{Initiative|None,None|CombatMaster,CombatMaster|Group-Init,Group-Init} --show,initiative'))
listItems.push(makeTextButton('Roll Each Round', initiative.rollEachRound, '!cmaster --config,initiative,key=rollEachRound,value='+!initiative.rollEachRound + ' --show,initiative'))
if (initiative.rollInitiative == 'CombatMaster') {
listItems.push(makeTextButton('Initiative Attr', initiative.initiativeAttributes, '!cmaster --config,initiative,key=initiativeAttributes,value=?{Attribute|'+initiative.initiativeAttributes+'} --show,initiative'))
listItems.push(makeTextButton('Initiative Die', 'd' + initiative.initiativeDie, '!cmaster --config,initiative,key=initiativeDie,value=?{Die (without the d)'+initiative.initiativeDie+'} --show,initiative'))
listItems.push(makeTextButton('Show Initiative in Chat', initiative.showInitiative, '!cmaster --config,initiative,key=showInitiative,value='+!initiative.showInitiative + ' --show,initiative'))
}
if (initiative.rollInitiative == 'Group-Init') {
listItems.push(makeTextButton('Target Tokens', initiative.apiTargetTokens, '!cmaster --config,initiative,key=apiTargetTokens,value=?{Target Tokens|} --show,initiative'))
if (!initiative.apiTargetTokens > '') {
listItems.push('
')
}
}
makeAndSendMenu(makeList(listItems,backButton),titleText,'gm');
},
sendTurnorderMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let listItems = []
let helpButton = makeImageButton('!cmaster --help,turnorder',helpImage,'Help','transparent',18,'white')
let titleText = 'Turnorder Setup'+'
'
let turnorder = state[combatState].config.turnorder
let installed
installed = verifyInstalls(turnorder.nextMarkerType)
if (!installed) {
return
}
installed = verifyInstalls(turnorder.markerType)
if (!installed) {
return
}
listItems.push(makeTextButton('Sort Turnorder',turnorder.sortTurnOrder, '!cmaster --config,turnorder,key=sortTurnOrder,value='+!turnorder.sortTurnOrder + ' --show,turnorder'))
listItems.push(makeTextButton('Center Map on Token', turnorder.centerToken, '!cmaster --config,turnorder,key=centerToken,value='+!turnorder.centerToken + ' --show,turnorder'))
listItems.push(makeTextButton('Use Marker',turnorder.useMarker, '!cmaster --config,turnorder,key=useMarker,value='+!turnorder.useMarker + ' --show,turnorder'))
listItems.push(makeTextButton('Marker Type',turnorder.markerType, '!cmaster --config,turnorder,key=markerType,value=?{Marker Type|External URL,External URL|Token Marker,Token Marker|Token Condition,Token Condition} --show,turnorder'))
if (turnorder.markerType == 'External URL') {
listItems.push(makeTextButton('Marker', '
', '!cmaster --config,turnorder,key=externalMarkerURL,value=?{Image Url} --show,turnorder'))
} else if (turnorder.markerType == 'Token Marker') {
listItems.push(makeTextButton('Marker Name',turnorder.tokenMarkerName, '!cmaster --config,turnorder,key=tokenMarkerName,value=?{Marker Name|} --show,turnorder'))
listItems.push(getDefaultIcon('Token Marker',turnorder.tokenMarkerName))
}
listItems.push(makeTextButton('Use Next Marker',turnorder.nextMarkerType, '!cmaster --config,turnorder,key=nextMarkerType,value=?{Next Marker Type|None,None|External URL,External URL|Token Marker,Token Marker|Token Condition,Token Condition} --show,turnorder'))
if (turnorder.nextMarkerType == 'External URL') {
listItems.push(makeTextButton('Next Marker', '
', '!cmaster --config,turnorder,key=nextExternalMarkerURL,value=?{Image Url} --show,turnorder'))
} else if (turnorder.nextMarkerType == 'Token Marker') {
listItems.push(makeTextButton('Next Marker Name',turnorder.nextTokenMarkerName, '!cmaster --config,turnorder,key=nextTokenMarkerName,value=?{Next Marker Name|} --show,turnorder'))
listItems.push(getDefaultIcon('Token Marker', turnorder.nextTokenMarkerName))
}
listItems.push('
' )
listItems.push(makeTextButton('API',turnorder.roundAPI, '!cmaster --config,turnorder,key=roundAPI,value=?{API Command|} --show,turnorder'))
listItems.push(makeTextButton('Roll20AM',turnorder.roundRoll20AM, '!cmaster --config,turnorder,key=roundRoll20AM,value=?{Roll20AM Command|} --show,turnorder'))
listItems.push(makeTextButton('FX',turnorder.roundFX, '!cmaster --config,turnorder,key=roundFX,value=?{FX Command|} --show,turnorder'))
listItems.push(makeTextButton('Characters Macro',turnorder.characterRoundMacro, '!cmaster --config,turnorder,key=characterRoundMacro,value=?{Macro Name|} --show,turnorder'))
listItems.push(makeTextButton('All Tokens Macro',turnorder.allRoundMacro, '!cmaster --config,turnorder,key=allRoundMacro,value=?{Macro Name|} --show,turnorder'))
listItems.push('
' )
listItems.push(makeTextButton('API',turnorder.turnAPI, '!cmaster --config,turnorder,key=turnAPI,value=?{API Command|} --show,turnorder'))
listItems.push(makeTextButton('Roll20AM',turnorder.turnRoll20AM, '!cmaster --config,turnorder,key=turnRoll20AM,value=?{Roll20AM Command|} --show,turnorder'))
listItems.push(makeTextButton('FX',turnorder.turnFX, '!cmaster --config,turnorder,key=turnFX,value=?{FX Command|} --show,turnorder'))
listItems.push(makeTextButton('Macro',turnorder.turnMacro, '!cmaster --config,turnorder,key=turnMacro,value=?{Macro Name|} --show,turnorder'))
makeAndSendMenu(makeList(listItems,backButton),titleText,'gm');
},
sendTimerMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let listItems = []
let helpButton = makeImageButton('!cmaster --help,timer',helpImage,'Help','transparent',18,'white')
let titleText = 'Timer Setup'+'
'
let timer = state[combatState].config.timer
listItems.push(makeTextButton('Turn Timer', timer.useTimer, '!cmaster --config,timer,key=useTimer,value='+!timer.useTimer + ' --show,timer'))
if (timer.useTimer) {
listItems.push(makeTextButton('Time', timer.time, '!cmaster --config,timer,key=time,value=?{Time|'+timer.time+'} --show,timer'))
listItems.push(makeTextButton('Skip Turn', timer.skipTurn, '!cmaster --config,timer,key=skipTurn,value='+!timer.skipTurn + ' --show,timer'))
listItems.push(makeTextButton('Send to Chat', timer.sendTimerToChat, '!cmaster --config,timer,key=sendTimerToChat,value='+!timer.sendTimerToChat + ' --show,timer'))
listItems.push(makeTextButton('Show on Token', timer.showTokenTimer, '!cmaster --config,timer,key=showTokenTimer,value='+!timer.showTokenTimer + ' --show,timer'))
listItems.push(makeTextButton('Token Font', timer.timerFont, '!cmaster --config,timer,key=timerFont,value=?{Font|Arial|Patrick Hand|Contrail|Light|Candal} --show,timer'))
listItems.push(makeTextButton('Token Font Size',timer.timerFontSize, '!cmaster --config,timer,key=timerFontSize,value=?{Font Size|'+timer.timerFontSize+'} --show,timer'))
}
let contents = makeList(listItems, backButton);
makeAndSendMenu(contents,titleText,'gm');
},
sendAnnounceMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let helpButton = makeImageButton('!cmaster --help,announce',helpImage,'Help','transparent',18,'white')
let titleText = 'Announcement Setup'+'
'
let announcements = state[combatState].config.announcements
let listItems = [
makeTextButton('Announce Rounds', announcements.announceRound, '!cmaster --config,announcements,key=announceRound,value='+!announcements.announceRound + ' --show,announce'),
makeTextButton('Announce Turns', announcements.announceTurn, '!cmaster --config,announcements,key=announceTurn,value='+!announcements.announceTurn + ' --show,announce'),
makeTextButton('Whisper GM Only', announcements.whisperToGM, '!cmaster --config,announcements,key=whisperToGM,value='+!announcements.whisperToGM + ' --show,announce'),
makeTextButton('Shorten Long Names', announcements.handleLongName, '!cmaster --config,announcements,key=handleLongName,value='+!announcements.handleLongName + ' --show,announce'),
makeTextButton('Show NPC Conditions', announcements.showNPCTurns, '!cmaster --config,announcements,key=showNPCTurns,value='+!announcements.showNPCTurns + ' --show,announce'),
]
let contents = makeList(listItems, backButton);
makeAndSendMenu(contents,titleText,'gm');
},
sendMacroMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let addButton = makeBigButton('Add Substiution', '!cmaster --new,macro,type=?{Type|CharID,CharID|CharName,CharName|TokenID,TokenID|PlayerID,PlayerID},action=?{Action|}')
let helpButton = makeImageButton('!cmaster --help,macro',helpImage,'Help','transparent',18,'white')
let titleText = 'Macro & API Setup'+'
'
let substitutions = state[combatState].config.macro.substitutions
let listItems=[],contents,deleteButton,listContents
substitutions.forEach((substitution) => {
deleteButton = makeImageButton('!cmaster --delete,macro,action='+substitution.action,deleteImage,'Delete Substitution','transparent',12)
listContents ='
'
listItems.push(listContents)
})
contents = makeList(listItems, backButton, addButton);
makeAndSendMenu(contents,titleText,'gm');
},
sendStatusMenu = function() {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let helpButton = makeImageButton('!cmaster --help,status',helpImage,'Help','transparent',18,'white')
let titleText = 'Status Setup'+'
'
let listItems = [
makeTextButton('Whisper GM Only', state[combatState].config.status.sendOnlyToGM, '!cmaster --config,status,key=sendOnlyToGM,value='+!state[combatState].config.status.sendOnlyToGM+' --show,status'),
makeTextButton('Player Allowed Changes', state[combatState].config.status.userChanges, '!cmaster --config,status,key=userChanges,value='+!state[combatState].config.status.userChanges+' --show,status'),
makeTextButton('Send Changes to Chat', state[combatState].config.status.sendConditions, '!cmaster --config,status,key=sendConditions,value='+!state[combatState].config.status.sendConditions+' --show,status'),
makeTextButton('Clear Conditions on Close', state[combatState].config.status.clearConditions, '!cmaster --config,status,key=clearConditions,value='+!state[combatState].config.status.clearConditions + ' --show,status'),
makeTextButton('Use Messages', state[combatState].config.status.useMessage, '!cmaster --config,status,key=useMessage,value='+!state[combatState].config.status.useMessage + ' --show,status'),
makeTextButton('Allow Access To', state[combatState].config.status.access, '!cmaster --config,status,key=access,?{Font Size|'+state[combatState].config.status.access+'} --show,status'),
]
let contents = makeList(listItems, backButton);
makeAndSendMenu(contents,titleText,'gm');
},
sendConditionsMenu = function(message) {
let key, condition, conditionButton, favorite, icon, output, rowCount=1
let backButton = makeBigButton('Back', '!cmaster --back,setup')
let addButton = makeBigButton('Add Condition', '!cmaster --new,condition=?{Name}')
let helpButton = makeImageButton('!cmaster --help,conditions',helpImage,'Help','transparent',18,'white')
let titleText = 'Conditions Setup'+'
'
let listItems = []
let listContents
let icons = []
let check = true
for (key in state[combatState].config.conditions) {
condition = getConditionByKey(key)
let installed = verifyInstalls(condition.iconType)
if (!installed) {
return
}
conditionButton = makeImageButton('!cmaster --show,condition=' + key,backImage,'Edit Condition','transparent',12)
if (rowCount == 1) {
listContents = '
'
rowCount = 2
} else {
listContents = '
'
rowCount = 1
}
listContents += getDefaultIcon(condition.iconType,condition.icon,'display:inline-block;margin-right:3px')
listContents += ''+condition.name+''
listContents += ''+conditionButton+''
listContents += '
'
listItems.push(listContents);
if(check && icons.includes(condition.icon)){
message = message || '' + '
Multiple conditions use the same icon';
check = false;
}
}
message = (message) ? '
'+message+'
' : '';
let contents = message + makeList(listItems, backButton, addButton);
state[combatState].config.previousPage = 'conditions'
makeAndSendMenu(contents,titleText,'gm');
},
sendConditionMenu = function(key) {
let condition = state[combatState].config.conditions[key], listItems = [], markerDropdown = ''
let helpButton = makeImageButton('!cmaster --help,condition',helpImage,'Help','transparent',18,'white')
let titleText = 'Condition Setup'+'
'+helpButton+''
if (typeof condition.description == 'undefined') {
condition.description = ' '
}
let removeButton = makeBigButton('Delete Condition', '!cmaster --delete,condition='+key+',confirm=?{Are you sure?|Yes,yes|No,no}')
let descriptionButton = makeBigButton('Edit Description', '!cmaster --config,condition='+key+',key=description,value=?{Description|'+condition.description+'} --show,condition='+key)
let backButton = makeBigButton('Back', '!cmaster --back')
listItems.push(makeTextButton('Name', condition.name, '!cmaster --config,condition='+key+',key=name,value=?{Name}'))
listItems.push(makeTextButton('Icon Type', condition.iconType, '!cmaster --config,condition='+key+',key=iconType,value=?{Icon Type|Combat Master,Combat Master|Token Marker,Token Marker|Token Condition,Token Condition} --show,condition='+key))
let installed = verifyInstalls(condition.iconType)
if (!installed) {
return
}
markerDropdown = '?{Marker';
if (condition.iconType == 'Combat Master') {
ctMarkers.forEach((marker) => {
markerDropdown += '|'+ucFirst(marker).replace(/-/g, ' ')+','+marker
})
} else if (condition.iconType == 'Token Marker') {
if (markers.length == 0) {
markers = getTokenMarkers();
}
markers.forEach((marker) => {
markerDropdown += '|'+marker.name+','+marker.name
})
}
markerDropdown += '}';
if (condition.iconType == 'Token Condition') {
listItems.push(makeTextButton('Icon', condition.icon, '!cmaster --config,condition='+key+',key=icon,value=?{Token Condition|} --show,condition='+key))
} else {
listItems.push(makeTextButton('Icon', getDefaultIcon(condition.iconType,condition.icon), '!cmaster --config,condition='+key+',key=icon,value='+markerDropdown+' --show,condition='+key))
}
listItems.push(makeTextButton('Duration', condition.duration, '!cmaster --config,condition='+key+',key=duration,value=?{Duration|1} --show,condition='+key))
listItems.push(makeTextButton('Direction', condition.direction, '!cmaster --config,condition='+key+',key=direction,value=?{Direction|0} --show,condition='+key))
listItems.push(makeTextButton('Override', condition.override, '!cmaster --config,condition='+key+',key=override,value='+!condition.override+' --show,condition='+key))
listItems.push(makeTextButton('Favorites', condition.favorite, '!cmaster --config,condition='+key+',key=favorite,value='+!condition.favorite+' --show,condition='+key))
listItems.push(makeTextButton('Message', condition.message, '!cmaster --config,condition='+key+',key=message,value=?{Message} --show,condition='+key))
listItems.push(makeTextButton('Targeted', condition.targeted, '!cmaster --config,condition='+key+',key=targeted,value='+!condition.targeted+' --show,condition='+key))
listItems.push('
Adding Condition
' )
listItems.push(makeTextButton('API', condition.addAPI, '!cmaster --config,condition='+key+',key=addAPI,value=?{API Command|} --show,condition='+key))
listItems.push(makeTextButton('Roll20AM', condition.addRoll20AM, '!cmaster --config,condition='+key+',key=addRoll20AM,value=?{Roll20AM Command|} --show,condition='+key))
listItems.push(makeTextButton('FX', condition.addFX, '!cmaster --config,condition='+key+',key=addFX,value=?{FX|} --show,condition='+key))
listItems.push(makeTextButton('Macro', condition.addMacro, '!cmaster --config,condition='+key+',key=addMacro,value=?{Macro|} --show,condition='+key))
listItems.push(makeTextButton('Persistent Macro', condition.addPersistentMacro, '!cmaster --config,condition='+key+',key=addPersistentMacro,value='+!condition.addPersistentMacro+' --show,condition='+key))
listItems.push('
Removing Condition
' )
listItems.push(makeTextButton('API', condition.remAPI, '!cmaster --config,condition='+key+',key=remAPI,value=?{API Command|} --show,condition='+key))
listItems.push(makeTextButton('Roll20AM', condition.remRoll20AM, '!cmaster --config,condition='+key+',key=remRoll20AM,value=?{Roll20AM Command|} --show,condition='+key))
listItems.push(makeTextButton('FX', condition.remFX, '!cmaster --config,condition='+key+',key=remFX,value=?{FX|} --show,condition='+key))
listItems.push(makeTextButton('Macro', condition.remMacro, '!cmaster --config,condition='+key+',key=remMacro,value=?{Macro|} --show,condition='+key))
let contents = makeList(listItems)+'
'+descriptionButton+'
Description:'+condition.description+removeButton+'
'+backButton
makeAndSendMenu(contents,titleText,'gm');
},
showConditions = function (selectedTokens) {
let tokenObj, characterObj, target
if (selectedTokens) {
selectedTokens.forEach(token => {
if (token._type == 'graphic') {
if (token._id != getOrCreateMarker(false).get('id') && token._id != getOrCreateMarker(true).get('id')) {
announcePlayer(getObj('graphic', token._id), false, false, true);
}
}
})
}
},
importCombatMaster = function (config) {
let json
let backButton = makeBigButton('Back', '!cmaster --back,setup');
json = JSON.parse(config.replace('config=',''));
if (['cmaster','cm'].includes(json.command)) {
state[combatState].config = json
state[combatState].conditions = [];
setDefaults()
makeAndSendMenu('Current Combat Master detected and imported.' + backButton, 'Import Setup');
} else if (json.config.command == 'condition') {
state[combatState].config.conditions = json.conditions
setDefaults()
makeAndSendMenu('Prior Combat Tracker detected and conditions were imported.' + backButton, 'Import Setup');
}
},
exportConditions = function () {
let backButton = makeBigButton('Back', '!cmaster --back,setup')
makeAndSendMenu('
Copy the entire content above and save it on your pc.
'+HE(JSON.stringify(state[combatState].config))+'
'+backButton+'
', 'Export Configs');
},
targetedCondition = function (id, key) {
if (debug) {
log('Targeted Condition')
}
let title = 'Select Targets'
let condition = state[combatState].conditions[key]
let addButton = makeImageButton('!cmaster --add,target,id='+id+',condition='+key,tagImage,'Targeted Icons','transparent',18,'white')
title += '
'+addButton+'
'
let contents = 'Select target tokens to assign this condition and hit the button above when ready'
makeAndSendMenu(contents,title,'gm');
},
//*************************************************************************************************************
//SESSION STATE MAINTENANCE
//*************************************************************************************************************
editCombatState = function (cmdDetails) {
if(cmdDetails.details.initiative){
state[combatState].config.initiative[cmdDetails.details.key] = cmdDetails.details.value;
}
else if(cmdDetails.details.timer){
state[combatState].config.timer[cmdDetails.details.key] = cmdDetails.details.value;
}
else if (cmdDetails.details.turnorder){
if (cmdDetails.details.key === 'initiativeDie') {
cmdDetails.details.value = parseInt(value);
}
state[combatState].config.turnorder[cmdDetails.details.key] = cmdDetails.details.value;
}
else if (cmdDetails.details.announcements){
state[combatState].config.announcements[cmdDetails.details.key] = cmdDetails.details.value;
}
else if (cmdDetails.details.status){
state[combatState].config.status[cmdDetails.details.key] = cmdDetails.details.value;
}
else {
if (cmdDetails.details.key === 'name' && cmdDetails.details.value.replace(/\s/g, '').toLowerCase() !== state[combatState].config.conditions[cmdDetails.details.condition]) {
state[combatState].config.conditions[cmdDetails.details.value.toLowerCase()] = state[combatState].config.conditions[cmdDetails.details.condition];
state[combatState].config.conditions[cmdDetails.details.value.toLowerCase()].key = cmdDetails.details.value.toLowerCase()
state[combatState].config.conditions[cmdDetails.details.value.toLowerCase()].name = cmdDetails.details.value
delete state[combatState].config.conditions[cmdDetails.details.condition];
sendConditionMenu(cmdDetails.details.value.toLowerCase())
} else {
if (cmdDetails.details.key == 'description') {
cmdDetails.details.value = cmdDetails.details.value
}
state[combatState].config.conditions[cmdDetails.details.condition][cmdDetails.details.key] = cmdDetails.details.value;
}
}
},
editFavoriteState = function (value) {
state[combatState].config.status.showConditions = value;
},
//*************************************************************************************************************
//CONDITIONS
//*************************************************************************************************************
newCondition = function (name) {
if (debug) {
log ('Create Condition')
}
if(!name){
sendConditionsMenu('You didn\'t give a condition name, eg.
!condition add Prone.');
} else if (state[combatState].config.conditions[name.toLowerCase()]) {
sendConditionsMenu('The condition `'+name+'` already exists.');
} else {
state[combatState].config.conditions[name.toLowerCase()] = {
name: name,
key: name.toLowerCase(),
icon: 'red',
iconType: 'Combat Master',
description: ' ',
duration: 1,
direction: 0,
message: 'None',
tageted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None'
}
sendConditionMenu(name.toLowerCase());
}
},
deleteCondition = function (key, confirm) {
if (debug) {
log ('Delete Condition')
}
if (confirm === 'yes') {
if(!key){
sendConditionsMenu('You didn\'t give a condition name, eg.
!cmaster --delete,condition=Prone.');
} else if( !state[combatState].config.conditions[key]){
sendConditionsMenu('The condition `'+key+'` does\'t exist.');
} else {
delete state[combatState].config.conditions[key];
sendConditionsMenu('The condition `'+key+'` is removed.');
}
}
sendConditionsMenu('Condition was deleted')
},
getConditionByMarker = function (marker) {
if (debug) {
log('Get Condition By Marker')
}
let key
for (key in state[combatState].config.conditions) {
if (marker.includes(state[combatState].config.conditions[key].icon)) {
return state[combatState].config.conditions[key]
}
}
return false;
},
getConditionByKey = function(key) {
return state[combatState].config.conditions[key];
},
getConditions = function () {
return state[combatState].config.conditions;
},
verifyCondition = function(token,key) {
let condition = getConditionByKey(key)
if (debug) {
log('Verify Condition')
}
if (!condition) {
return true
}
if (typeof condition.direction == 'undefined' || typeof condition.duration == 'undefined') {
makeAndSendMenu('The condition you are trying to use has not be setup yet', '', 'gm');
return false;
}
if (!key) {
makeAndSendMenu('No condition name was given.', '', 'gm');
return false;
}
if (!token || !token.length) {
makeAndSendMenu('No tokens were selected.', '', 'gm');
return false;
}
if (token == getOrCreateMarker().get('id')) {
return false;
}
if (token == getOrCreateMarker(true).get('id')) {
return false;
}
return true;
},
addCondition = function(cmdDetails,selectedTokens,playerID) {
if (debug) {
log('Add Condition')
}
if (selectedTokens) {
selectedTokens.forEach(token => {
if (token._type == 'graphic') {
addConditionToToken(getObj(token._type, token._id),cmdDetails.details.condition,cmdDetails.details.duration,cmdDetails.details.direction,cmdDetails.details.message)
doAddConditionCalls(getObj(token._type, token._id),cmdDetails.details.condition,playerID)
}
});
} else {
makeAndSendMenu('No tokens were selected.', '', 'gm');
}
},
removeCondition = function (cmdDetails,selectedTokens) {
if (debug) {
log('Remove Condition')
}
if (cmdDetails.details.id) {
let token = getObj('graphic', cmdDetails.details.id)
removeConditionFromToken(token, cmdDetails.details.condition)
doRemoveConditionCalls(token,cmdDetails.details.condition)
} else if (selectedTokens) {
selectedTokens.forEach(token => {
if (token._type == 'graphic') {
removeConditionFromToken(getObj(token._type, token._id), cmdDetails.details.condition)
doRemoveConditionCalls(getObj(token._type, token._id),cmdDetails.details.condition)
}
});
}
},
addConditionToToken = function(tokenObj,key,duration,direction,message) {
let defaultCondition = getConditionByKey(key)
let newCondition = {}
if (!tokenObj) {
return
}
if (debug) {
log('Add Condition To Token')
}
if (verifyCondition(tokenObj.get("_id"), key)) {
let removed = removeConditionFromToken(tokenObj, key);
newCondition.id = tokenObj.get("_id")
newCondition.key = key
newCondition.target = []
newCondition.tokenConditionID = null
if (defaultCondition) {
newCondition.name = defaultCondition.name
newCondition.icon = defaultCondition.icon
newCondition.iconType = defaultCondition.iconType
newCondition.addMacro = defaultCondition.addMacro
newCondition.addPersistentMacro = defaultCondition.addPersistentMacro
} else {
newCondition.name = key
newCondition.icon = null
newCondition.iconType = null
newCondition.addMacro = null
newCondition.addPersistentMacro = null
}
let icon
if (newCondition.iconType) {
icon = getIconTag(newCondition.iconType, newCondition.icon)
}
if (newCondition.iconType == 'Token Condition') {
let characterObj = findObjs({name: newCondition.icon, _type: 'character'})[0];
characterObj.get("defaulttoken", function(defaulttoken) {
let newToken = JSON.parse(defaulttoken)
let condition = createObj('graphic', {
subtype:'token',
name: newToken.name,
imgsrc: getCleanImgsrc(newToken.imgsrc),
pageid: tokenObj.get('pageid'),
represents: characterObj.id,
layer: tokenObj.get('layer'),
left: tokenObj.get('left'),
top: tokenObj.get('top'),
width: tokenObj.get('width'),
height: tokenObj.get('height')
});
let result = TokenCondition.AttachConditionToToken(condition.id,tokenObj.id);
if(result.success) {
newCondition.tokenConditionID = condition.id
} else {
log(`Attach failed. Message: ${result.reason}`);
}
});
}
if (!duration && defaultCondition) {
newCondition.duration = parseInt(defaultCondition.duration)
} else {
newCondition.duration = parseInt(duration)
}
if (!direction && defaultCondition) {
newCondition.direction = parseInt(defaultCondition.direction)
} else {
newCondition.direction = parseInt(direction)
}
if (!message) {
newCondition.message = 'None'
} else {
newCondition.message = message
}
setTimeout(function() {
state[combatState].conditions.push(newCondition)
},500)
if (icon) {
if (newCondition.key == 'dead' || newCondition.duration <= 1) {
addMarker(tokenObj,icon)
} else {
if (newCondition.duration >= 10) {
addMarker(tokenObj,icon)
} else {
addMarker(tokenObj,icon,newCondition.duration)
}
}
}
if (state[combatState].config.status.sendConditions && !removed && defaultCondition) {
sendConditionToChat(newCondition.key)
}
if (defaultCondition && defaultCondition.targeted) {
targetedCondition(newCondition.id, key)
}
}
},
getCleanImgsrc = function (imgsrc) {
let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/);
if(parts) {
return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`);
}
return;
},
removeConditionFromToken = function(tokenObj,key) {
if (debug) {
log('Remove Condition From Token')
}
if (!tokenObj) {
return;
}
let removed = false
let icon
[...state[combatState].conditions].forEach((condition, i) => {
if (condition.id == tokenObj.get('_id') && condition.key == key) {
if (condition.iconType) {
icon = getIconTag(condition.iconType, condition.icon)
}
if (condition.hasOwnProperty('target')) {
if (condition.target.length > 0) {
condition.target.forEach((target, j) => {
if (icon) {
removeMarker(getObj('graphic', target),icon)
} else if (condition.iconType == 'Token Condition') {
removeTokenCondition(condition.tokenConditionID)
}
})
}
}
if (icon) {
removeMarker(tokenObj,icon)
} else if (condition.iconType == 'Token Condition') {
removeTokenCondition(condition.tokenConditionID)
}
state[combatState].conditions.splice(i,1)
removed = true
}
});
return removed
},
removeTokenCondition = function (id) {
if (debug) {
log('Remove Token Condition')
}
let conditionToken = findObjs({_id:id,_pageid:Campaign().get("playerpageid"), _type: 'graphic'})[0];
conditionToken.remove()
},
sendConditionToChat = function (key) {
if (debug) {
log("Send Condition To Chat")
}
let condition = getConditionByKey(key)
let icon
if (['Combat Master','Token Marker'].includes(condition.iconType)) {
icon = getDefaultIcon(condition.iconType,condition.icon, 'margin-right: 5px; margin-top: 5px; display: inline-block;');
}
makeAndSendMenu(condition.description,icon+condition.name,(state[combatState].config.status.sendOnlyToGM) ? 'gm' : '');
},
addTargetsToCondition = function(selectedTokens,id,key) {
if (debug) {
log("Add Targets to Condition")
}
let icon
[...state[combatState].conditions].forEach((condition,i) => {
if (condition.id == id && condition.key == key) {
selectedTokens.forEach(token => {
let installed = verifyInstalls(condition.iconType)
if (installed) {
icon = getIconTag(condition.iconType,condition.icon)
state[combatState].conditions[i].target.push(token._id)
addMarker(getObj('graphic', token._id),icon)
}
})
}
});
makeAndSendMenu('Selected Tokens Added',"Selected Tokens",'gm');
},
//*************************************************************************************************************
//START/STOP COMBAT
//*************************************************************************************************************
verifySetup = function(selectedTokens, initiative) {
let initAttributes, turnorder, attribute, whisper, characterObj, verified=true, i, tokenObj
if (debug) {
log('Verify Setup')
}
if ((!selectedTokens || selectedTokens.length == 0) && !state[combatState].config.hold.held) {
makeAndSendMenu('No tokens selected. Combat not started',' ', whisper);
return false
}
if (initiative.rollInitiative == 'None') {
turnorder = getTurnorder()
if (turnorder.length == 0) {
makeAndSendMenu('Auto Roll Initiative has been set to None and your turn order is currently empty',' ', whisper);
verified=false
return
}
}
if (initiative.rollInitiative == 'CombatMaster' && !state[combatState].config.hold.held) {
selectedTokens.forEach(token => {
if (token._type == 'graphic') {
tokenObj = getObj('graphic', token._id)
if (tokenObj) {
whisper = (tokenObj.layer == 'gmlayer') ? 'gm ' : ''
characterObj = getObj('character', tokenObj.get('represents'))
if (!characterObj) {
makeAndSendMenu('A token was found not assigned to a character sheet',' ', whisper);
}
}
}
})
}
return verified
},
startCombat = function (selectedTokens, who) {
if (debug) {
log('Start Combat')
}
let initiative = state[combatState].config.initiative
let turnorder = state[combatState].config.turnorder
let verified = verifySetup(selectedTokens, initiative)
let hold = state[combatState].config.hold
if (!verified && !hold.held) {
return
}
Campaign().set('initiativepage', Campaign().get('playerpageid'));
paused = false;
if (hold.held) {
restartCombat(hold, who)
} else {
if(initiative.rollInitiative == 'CombatMaster'){
rollInitiative(selectedTokens, initiative);
} else if (initiative.rollInitiative == 'Group-Init') {
rollGroupInit(selectedTokens)
} else if (!getTurnorder()) {
makeAndSendMenu('You must have a populated turnorder before starting Combat Master','');
return
}
}
setTimeout(function() {
doRoundCalls()
doTurnorderChange()
},2000)
log(hold)
},
restartCombat = function (hold,who) {
if (debug) {
log('Restart Combat')
}
round = hold.round;
setTurnorder(hold.turnorder);
let tokenObj
let iconTag;
[...hold.conditions].forEach((condition, i) => {
tokenObj = getObj('graphic', condition.id)
if (tokenObj) {
addConditionToToken(tokenObj,condition.key,condition.duration,condition.direction,condition.message);
condition.target.forEach((target) => {
addMarker(getObj('graphic', target),icon)
})
}
})
setTimeout(function() {
clearHold(hold)
sendMainMenu(who)
},2000)
},
stopCombat = function (who) {
if (debug) {
log('Stop Combat')
}
clearHold(state[combatState].config.hold)
if (state[combatState].config.status.clearConditions) {
[...state[combatState].conditions].forEach((condition) => {
if (condition.id != getOrCreateMarker(true).get('id') && condition.id != getOrCreateMarker(false).get('id')) {
removeConditionFromToken(getObj('graphic',condition.id), condition.key)
}
})
}
removeMarkers();
stopTimer();
Campaign().set({initiativepage:false,turnorder:''});
round = 1;
setTimeout(function() {
sendMainMenu(who)
state[combatState].conditions = [];
},2000)
},
holdCombat = function (who) {
if (debug) {
log('Hold Combat')
}
let hold = state[combatState].config.hold
hold.held = true;
hold.turnorder = getTurnorder();
hold.round = round;
hold.conditions = [...state[combatState].conditions]
if (state[combatState].config.status.clearConditions) {
[...state[combatState].conditions].forEach((condition) => {
if (condition.id != getOrCreateMarker(true).get('id') && condition.id != getOrCreateMarker(false).get('id')) {
removeConditionFromToken(getObj('graphic',condition.id), condition.key)
}
})
}
Campaign().set({initiativepage:false,turnorder:''});
pauseTimer()
setTimeout(function() {
state[combatState].conditions = [];
sendMainMenu(who)
},2000)
log(hold)
},
clearHold = function (hold) {
if (debug) {
log('Clear Hold')
}
hold.held = false
hold.round = 1
hold.turnorder = []
hold.conditions = []
},
rollInitiative = function (selectedTokens, initiative) {
let tokenObj, whisper, initiativeTemp, initiativeRoll, characterObj, initAttributes, initiativeMod, i, advantageAttrib, initiativeAdv1, initiativeAdv2
//loop through selected tokens
selectedTokens.forEach(token => {
if (token._type == 'graphic') {
tokenObj = getObj('graphic', token._id)
if (tokenObj) {
characterObj = getObj('character', tokenObj.get('represents'))
if (characterObj) {
whisper = (tokenObj.get('layer') === 'gmlayer') ? 'gm ' : ''
initiativeRoll = (initiative.initiativeDie) ? randomInteger(initiative.initiativeDie) : 0;
initAttributes = initiative.initiativeAttributes.split(',')
initiativeMod = 0
initAttributes.forEach((attributes) => {
initiativeTemp = getAttrByName(characterObj.id,attributes,'current')
initiativeMod += parseFloat(initiativeTemp)
})
//check for advantage initiative rolling (OGL)
advantageAttrib = getAttrByName(characterObj.id, 'initiative_style', 'current');
if (typeof advantageAttrib != 'undefined') {
// roll advantage for initiative
initiativeAdv1 = (initiative.initiativeDie) ? randomInteger(initiative.initiativeDie) : 0;
initiativeAdv2 = (initiative.initiativeDie) ? randomInteger(initiative.initiativeDie) : 0;
// this is the value if in OGL if rolling advantage
if (advantageAttrib == '{@{d20},@{d20}}kh1') {
//determine which value is higher
if (initiativeAdv1 >= initiativeAdv2) {
initiativeRoll = initiativeAdv1
} else {
initiativeRoll = initiativeAdv2
}
//pass in both values and modifier for display
if (initiative.showInitiative) {
sendInitiativeChat(tokenObj.get('name'),initiativeAdv1,initiativeMod,initiativeAdv2,whisper)
}
} else if (initiative.showInitiative) {
// if not rolling advantage, use first roll
initiativeRoll = initiativeAdv1
sendInitiativeChat(tokenObj.get('name'),initiativeRoll,initiativeMod,null,whisper)
}
} else if (initiative.showInitiative) {
// if everything else then pass in for display
sendInitiativeChat(tokenObj.get('name'),initiativeRoll,initiativeMod,null,whisper)
}
//add to turnorder
if (Number.isInteger(initiativeMod+initiativeRoll)) {
addToTurnorder({id:tokenObj.id,pr:(initiativeMod+initiativeRoll),custom:'',pageid:tokenObj.get("pageid")});
} else {
addToTurnorder({id:tokenObj.id,pr:(initiativeMod+initiativeRoll).toFixed(2),custom:'',pageid:tokenObj.get("pageid")});
}
}
}
}
});
if(state[combatState].config.turnorder.sortTurnOrder){
sortTurnorder();
}
},
rollGroupInit = function (selectedTokens) {
let giRoll = () => sendChat('',`/w gm
GroupInitiative.RollForTokenIDs() is not supported.`);
if('undefined' !== typeof GroupInitiative && GroupInitiative.RollForTokenIDs){
GroupInitiative.RollForTokenIDs(
(selectedTokens||[]).map(s=>s._id),{manualBonus: 0}
);
}
},
sendInitiativeChat = function (name,rollInit,bonus,rollInit1,whisper) {
let contents = ''
if (rollInit1) {
contents = '
\
\
| Modifier | \
'+bonus+' | \
\
\
\
\
[['+rollInit+'+'+bonus+']]
\
\
\
\
\
[['+rollInit1+'+'+bonus+']]
\
\
'
} else {
contents = '
\
\
| Modifier | \
'+bonus+' | \
\
\
\
\
[['+rollInit+'+'+bonus+']]
\
\
'
}
makeAndSendMenu(contents, name + ' Initiative', whisper);
},
//*************************************************************************************************************
//MARKERS
//*************************************************************************************************************
addMarker = function(tokenObj, marker, duration) {
if (debug) {
log('Add Marker')
}
let exists
let statusmarker
let statusmarkers
if (tokenObj.get('statusmarkers')) {
statusmarkers = tokenObj.get('statusmarkers').split(',')
} else {
statusmarkers = []
}
if (duration) {
statusmarker = marker+'@'+duration
} else {
statusmarker = marker
}
[...statusmarkers].forEach((a, i) => {
if (a.indexOf(marker) > -1) {
statusmarkers.splice(i,0)
exists = true
}
});
if (!exists) {
statusmarkers.push(statusmarker)
}
tokenObj.set('statusmarkers', statusmarkers.join())
},
removeMarker = function(tokenObj, marker) {
if (debug) {
log('Remove Marker')
}
let statusmarkers = tokenObj.get('statusmarkers').split(',');
[...statusmarkers].forEach((a, i) => {
if (a.indexOf(marker) > -1) {
statusmarkers.splice(i,1)
}
});
tokenObj.set('statusmarkers', statusmarkers.join())
},
resetMarker = function (next=false) {
let marker = getOrCreateMarker(next),
turnorder = state[combatState].config.turnorder;
if (debug) {
log('Reset Marker')
}
marker.set({
name: (next) ? 'NextMarker' : 'Round ' + round,
imgsrc: (next) ? turnorder.nextExternalMarkerURL : turnorder.externalMarkerURL,
pageid: Campaign().get('playerpageid'),
layer: 'gmlayer',
left: 35, top: 35,
width: 70, height: 70
});
return marker;
},
getOrCreateMarker = function (next=false) {
let pageid = Campaign().get('playerpageid')
let turnorder = state[combatState].config.turnorder
if (debug) {
log('Get or Create Marker')
}
let imgsrc
if (turnorder.markerType == 'External URL') {
imgsrc = (next) ? turnorder.nextExternalMarkerURL : turnorder.externalMarkerURL
} else {
imgsrc = (next) ? turnorder.nextTokenMarkerURL : turnorder.tokenMarkerURL
}
let markers = (next) ? findObjs({pageid,imgsrc,name: 'NextMarker'}) : findObjs({pageid,imgsrc});
markers.forEach((marker, i) => {
if(i > 0 && !next) marker.remove();
});
let marker = markers.shift();
if(!marker) {
marker = createObj('graphic', {
name: (next) ? 'NextMarker' : 'Round 1',
imgsrc,
pageid,
layer: 'gmlayer',
showplayers_name: true,
left: 35, top: 35,
width: 70, height: 70
});
}
if(!next) checkMarkerturn(marker);
toBack(marker);
return marker;
},
checkMarkerturn = function (marker) {
let turnorder = getTurnorder(),
hasTurn = false;
if (debug) {
log ('Check Marker Turn')
}
turnorder.forEach(turn => {
if(turn.id === marker.get('id')) hasTurn = true;
});
if(!hasTurn){
turnorder.push({ id: marker.get('id'), pr: -1, custom: '', pageid: marker.get('pageid') });
Campaign().set('turnorder', JSON.stringify(turnorder));
}
},
removeMarkers = function () {
stopRotate();
getOrCreateMarker().remove();
getOrCreateMarker(true).remove();
},
changeMarker = function (token, next=false) {
let marker = getOrCreateMarker(next);
if (debug) {
log('Change Marker')
}
if(!token){
resetMarker(next);
return;
}
let position = {
top: token.get('top'),
left: token.get('left'),
width: token.get('width')+(token.get('width')*0.35),
height: token.get('height')+(token.get('height')*0.35),
};
if(token.get('layer') !== marker.get('layer')) {
if(marker.get('layer') === 'gmlayer') {
marker.set(position);
setTimeout(() => {
if (state[combatState].config.turnorder.useMarker) {
marker.set({ layer: 'objects' });
}
}, 500);
} else {
marker.set({ layer: 'gmlayer' });
setTimeout(() => {
marker.set(position);
}, 500);
}
} else {
marker.set(position);
}
toBack(marker);
},
centerToken = function (token) {
if(state[combatState].config.turnorder.centerToken) {
if (token.get('layer') != 'gmlayer') {
sendPing(token.get('left'), token.get('top'), token.get('pageid'), null, true);
}
}
},
handleStatusMarkerChange = function (obj, prev) {
if (debug) {
log ('Handle Status Marker Change')
}
prev.statusmarkers = (typeof prev.get === 'function') ? prev.get('statusmarkers') : prev.statusmarkers;
if(typeof prev.statusmarkers === 'string'){
if(obj.get('statusmarkers') !== prev.statusmarkers){
var prevstatusmarkers = prev.statusmarkers.split(",");
var newstatusmarkers = obj.get('statusmarkers').split(",");
if (prevstatusmarkers.length > 0) {
prevstatusmarkers.forEach((marker) => {
let condition = getConditionByMarker(marker);
if(!condition) return;
if(marker !== '' && !newstatusmarkers.includes(marker)){
removeConditionFromToken(obj, condition.key);
doRemoveConditionCalls(obj,condition.key)
}
})
}
if (newstatusmarkers.length > 0 ) {
newstatusmarkers.forEach(function(marker){
let condition = getConditionByMarker(marker)
if(!condition) return;
if(marker !== "" && !prevstatusmarkers.includes(marker)){
addConditionToToken(obj,condition.key,condition.duration,condition.direction,condition.message);
doAddConditionCalls(obj,condition.key)
}
});
}
}
}
},
//*************************************************************************************************************
//TURNORDER
//*************************************************************************************************************
clearTurnorder = function () {
Campaign().set({ turnorder: '' });
state[combatState].turnorder = {};
},
verifyTurnorder = function () {
if(debug) {
log('Verify Turnorder')
}
let turnorder = getTurnorder()
if (turnorder.length == 0) {
makeAndSendMenu('The Turnorder is empty. Combat not started',null,'gm');
stopCombat()
return false
}
return true
},
doTurnorderChange = function (prev=false, delay=false) {
if(debug) {
log('Do TurnOrder Change')
}
let verified = verifyTurnorder()
if (!verified) {
return
}
let turn = getCurrentTurn()
let marker = getOrCreateMarker()
let tokenObj = getObj('graphic', turn.id)
if (turn.id === '-1') {
doRoundCalls()
nextTurn();
return;
}
if (turn.id === marker.id) {
if (prev) {
prevRound();
} else {
nextRound();
}
return;
}
if (tokenObj) {
toFront(tokenObj);
if (state[combatState].config.timer.useTimer) {
startTimer(tokenObj);
}
changeMarker(tokenObj);
announcePlayer(tokenObj, prev, delay);
centerToken(tokenObj);
doTurnCalls(tokenObj)
} else {
resetMarker();
}
if (state[combatState].config.turnorder.nextMarkerType != 'None') {
let nextTurn = getNextTurn();
if (nextTurn) {
let nextToken = getObj('graphic', nextTurn.id);
if (nextToken) {
toFront(nextToken);
changeMarker(nextToken || false, true);
} else {
resetMarker(true);
}
}
}
},
handleTurnorderChange = function (obj, prev) {
if (debug) {
log("Handle Turnorder Change")
}
if(obj.get('turnorder') === prev.turnorder) return;
let turnorder = (obj.get('turnorder') === "") ? [] : JSON.parse(obj.get('turnorder'));
let prevTurnorder = (prev.turnorder === "") ? [] : JSON.parse(prev.turnorder);
if(obj.get('turnorder') === "[]"){
stopCombat();
return;
}
if(turnorder.length && prevTurnorder.length && turnorder[0].id !== prevTurnorder[0].id){
doTurnorderChange();
}
},
sortTurnorder = function (order='DESC') {
let turnorder = getTurnorder();
turnorder.sort((a,b) => {
return (order === 'ASC') ? a.pr - b.pr : b.pr - a.pr;
});
setTurnorder(turnorder);
},
getTurnorder = function () {
return (Campaign().get('turnorder') === '') ? [] : Array.from(JSON.parse(Campaign().get('turnorder')));
},
addToTurnorder = function (turn) {
let turnorder = getTurnorder(),
justDoIt = true;
if (debug) {
log('Add to Turnorder')
}
turnorder.push(turn);
setTurnorder(turnorder);
},
setTurnorder = function (turnorder) {
Campaign().set('turnorder', JSON.stringify(turnorder));
},
//*************************************************************************************************************
//TURNS
//*************************************************************************************************************
delayTurn = function () {
let turnorder, currentTurn, nextTurn, dummy
turnorder = getTurnorder()
currentTurn = turnorder.shift();
if (getVeryNextTurn().id === getOrCreateMarker().get('id')) {
setTurnorder(turnorder)
nextRound()
turnorder = getTurnorder()
nextTurn = currentTurn
currentTurn = turnorder.shift();
turnorder.unshift(nextTurn)
turnorder.unshift(currentTurn)
setTurnorder(turnorder);
return;
}
nextTurn = turnorder.shift();
if (debug) {
log('Delay Turn')
}
turnorder.unshift(currentTurn)
turnorder.unshift(nextTurn)
setTurnorder(turnorder);
doTurnorderChange(false,true);
},
nextTurn = function() {
let turnorder, currentTurn
if (debug) {
log('Next Turn')
}
turnorder = getTurnorder(),
currentTurn = turnorder.shift()
turnorder.push(currentTurn);
setTurnorder(turnorder);
doTurnorderChange();
},
previousTurn = function() {
let turnorder = getTurnorder(),
last_turn = turnorder.pop();
turnorder.unshift(last_turn);
setTurnorder(turnorder);
doTurnorderChange(true);
},
nextRound = function () {
let marker = getOrCreateMarker(),
initiative = state[combatState].config.initiative
if (debug) {
log('Next Round')
}
round++;
marker.set({ name: 'Round ' + round});
if(state[combatState].config.announcements.announceRound){
let text = '
'+marker.get('name')+'';
makeAndSendMenu(text, ' ');
}
if(initiative.rollEachRound){
let turnorder = getTurnorder();
clearTurnorder();
checkMarkerturn(marker)
rollInitiative(turnorder.map(t => { return (t.id) ? { _type: 'graphic', _id: t.id } : false }), initiative);
doTurnorderChange()
}else{
nextTurn();
if(state[combatState].config.turnorder.sortTurnOrder){
sortTurnorder();
}
}
},
getCurrentTurn = function () {
return getTurnorder().shift();
},
getNextTurn = function () {
let returnturn;
getTurnorder().every((turn, i) => {
if(i > 0 && turn.id !== '-1' && turn.id !== getOrCreateMarker().get('id')){
returnturn = turn;
return false;
}else return true
});
return returnturn;
},
getVeryNextTurn = function () {
let turnorder, turn;
turnorder = getTurnorder();
turn = turnorder.shift()
turn = turnorder.shift()
return turn;
},
prevRound = function () {
let marker = getOrCreateMarker();
round--;
marker.set({ name: 'Round ' + round});
if(state[combatState].config.announcements.announceRound){
let text = '
'+marker.get('name')+'';
makeAndSendMenu(text);
}
previousTurn();
},
//*************************************************************************************************************
//TIMER
//*************************************************************************************************************
startTimer = function (token) {
let timer = state[combatState].config.timer,
config_time = parseInt(timer.time),
time = config_time;
paused = false;
clearInterval(intervalHandle);
if(timerObj) timerObj.remove();
if(token && timer.showTokenTimer){
timerObj = createObj('text', {
text: 'Timer: ' + time,
font_size: timer.timerFontSize,
font_family: timer.timerFont,
color: timer.timerFontColor,
pageid: token.get('pageid'),
layer: 'gmlayer'
});
}
intervalHandle = setInterval(() => {
if(paused) return;
if(timerObj && timer.showTokenTimer) timerObj.set({
top: token.get('top')+token.get('width')/2+40,
left: token.get('left'),
text: 'Timer: ' + time,
layer: token.get('layer')
});
if(state[combatState].config.timer.sendTimerToChat && (time === config_time || config_time/2 === time || config_time/4 === time || time === 10 || time === 5)){
makeAndSendMenu('', 'Time Remaining: ' + time);
}
if(time <= 0){
if(timerObj) timerObj.remove();
clearInterval(intervalHandle);
if(timer.skipTurn) nextTurn();
else if(token.get('layer') !== 'gmlayer') makeAndSendMenu(token.get('name') + "'s time ran out!", '');
}
time--;
}, 1000);
},
stopTimer = function () {
clearInterval(intervalHandle);
if(timerObj) timerObj.remove();
},
pauseTimer = function () {
paused = !paused;
},
//*************************************************************************************************************
//ANNOUNCE
//*************************************************************************************************************
announcePlayer = function (tokenObj, prev, delay=false, show) {
if (debug) {
log('Announce Player')
}
let name = tokenObj.get('name');
let imgurl = tokenObj.get('imgsrc');
let conditions = getAnnounceConditions(tokenObj, prev, delay, show);
let image = (imgurl) ? '

' : ''
name = (state[combatState].config.announcements.handleLongName) ? handleLongString(name) : name
let title = 'Next Player Up'
let doneButton = makeImageButton('!cmaster --turn,next',doneImage,'Done with Round','transparent',18,'white')
let delayButton = makeImageButton('!cmaster --turn,delay',delayImage,'Delay your Turn','transparent',18, 'white');
if (!show) {
title += '
'+doneButton+'
'
title += '
'+delayButton+'
'
}
let contents = '
'+image+'
'
if (!show) {
contents += '
'+name+'\'s Turn
'
} else {
contents += '
'+name+'
'
}
contents += conditions
let characterObj = getObj('character', tokenObj.get('represents'))
if (characterObj) {
let controlledBy = characterObj.get('controlledby')
let players = controlledBy.split(",")
if (state[combatState].config.status.userChanges) {
if (players.length > 1) {
let playerObj, displayName
players.forEach((playerID) => {
playerObj = getObj('player', playerID)
if (playerObj) {
displayName = playerObj.get('displayname')
sendMainMenu(displayName)
}
})
}
}
if (state[combatState].config.announcements.announceTurn) {
let target
if (players[0] != "") {
target = (state[combatState].config.announcements.whisperToGM) ? 'gm' : ''
} else {
target = (!state[combatState].config.announcements.showNPCTurns) ? 'gm' : ''
}
makeAndSendMenu(contents,title,target);
}
}
},
getAnnounceConditions = function (tokenObj, prev, delay, show) {
if (debug) {
log('Announce Condition')
}
let removeButton
let descriptionButton
let removed = false
let output = '
'
if (state[combatState].conditions) {
[... state[combatState].conditions].forEach(condition => {
if (condition.id == tokenObj.get("_id")) {
descriptionButton = makeButton(condition.name, '!cmaster --show,description,key='+condition.key)
if (!delay && !show) {
if (!prev) {
condition.duration = condition.duration + condition.direction
} else {
condition.duration = condition.duration - condition.direction
}
}
if (condition.duration <= 0 && condition.direction != 0) {
output += '
'+descriptionButton+' removed
';
if (!delay && !show) {
removeConditionFromToken(tokenObj, condition.key);
doRemoveConditionCalls(tokenObj,condition.key)
removed = true
}
} else if (condition.duration > 0 && condition.direction != 0) {
output += '
'+descriptionButton+' '+condition.duration+' Rounds Left
';
if (!delay && !show) {
addConditionToToken(tokenObj,condition.key,condition.duration,condition.direction,condition.message)
}
if (condition.message != 'None' && condition.message.length > 0) {
output += '
Message: '+condition.message + '
';
}
} else if (condition.direction == 0) {
output += '
'+descriptionButton+' '+condition.duration+' Permanent
';
if (condition.message != 'None' && condition.message.length > 0) {
output += '
Message: '+condition.message+ '
';
}
}
if (!removed) {
removeButton = makeImageButton('!cmaster --remove,condition='+condition.key+',id='+tokenObj.get("_id"),deleteImage,'Remove Condition','transparent',18)
output += '
'+removeButton+'
'
}
}
})
}
output += '
'
return output;
},
//*************************************************************************************************************
//MAKES
//*************************************************************************************************************
makeAndSendMenu = function (contents, title, whisper) {
whisper = (whisper && whisper !== '') ? '/w ' + whisper + ' ' : '';
title = makeTitle(title)
sendChat(script_name, whisper + '
'+title+contents+'
', null, {noarchive:true});
},
makeTitle = function (title) {
return '
'+title+'
'
},
makeBigButton = function (title, href) {
return '
';
},
makeButton = function (title, href) {
return '
'+title+'';
},
makeTextButton = function (label, value, href) {
return '
'+label+''+value+'';
},
makeImageButton = function(command, image, toolTip, backgroundColor,size,color){
if (!color) {
color = 'black'
}
return '
'
},
makeList = function (items, backButton, extraButton) {
let list;
list = '
'
items.forEach((item) => {
list += '- '+item+'
';
});
list += '
'
if (extraButton) {
list += extraButton
}
if(backButton) {
list += '
'+backButton;
}
return list;
},
//*************************************************************************************************************
//ICONS
//*************************************************************************************************************
getDefaultIcon = function (iconType, icon, style='', height, width) {
if (iconType == 'None') {
return 'None'
}
let installed = verifyInstalls(iconType)
if (iconType == 'Token Marker' && installed) {
return libTokenMarkers.getStatus(icon).getHTML(1.7);
} else if (iconType == 'Combat Master') {
let X = '';
let iconStyle = ''
let iconSize = ''
if(typeof icon_image_positions[icon] === 'undefined') return false;
if (width) {
iconStyle += 'width: '+width+'px;height: '+height+'px;';
} else {
iconStyle += 'width: 24px; height: 24px;';
}
if(Number.isInteger(icon_image_positions[icon])){
iconStyle += 'background-image: url(https://roll20.net/images/statussheet.png);'
iconStyle += 'background-repeat: no-repeat;'
iconStyle += 'background-position: -'+icon_image_positions[icon]+'px 0;'
}else if(icon_image_positions[icon] === 'X'){
iconStyle += 'color: red; margin-right: 0px;';
X = 'X';
}else{
iconStyle += 'background-color: ' + icon_image_positions[icon] + ';';
iconStyle += 'border: 1px solid white; border-radius: 50%;'
}
iconStyle += style;
return '
'+X+'
';
} else if (iconType == 'Token Condition') {
return '
TC '
}
},
getTokenMarkers = function () {
return libTokenMarkers.getOrderedList();
},
findIcon = function (icon) {
markers.forEach((marker) => {
if (marker.name == icon) {
return marker.url
}
})
},
getIconTag = function (iconType,iconName) {
if (debug) {
log('Get Icon Tag')
}
let installed = verifyInstalls(iconType)
if (!installed) {
return
}
let iconTag = null
if (iconType == 'Token Marker') {
iconTag = libTokenMarkers.getStatus(iconName).getTag()
} else if (iconType == 'Combat Master') {
iconTag = iconName
}
return iconTag
},
verifyInstalls = function(iconType) {
if (iconType == 'Token Marker' && 'undefined' == typeof libTokenMarkers) {
makeAndSendMenu('libTokenMarker API must be installed if using Custom Icons.', '', 'gm');
return false
} else if (iconType == 'Token Condition' && 'undefined' == typeof TokenCondition) {
makeAndSendMenu('Token Condition API must be installed if using Token Condition.', '', 'gm');
return false
}
return true
},
//*************************************************************************************************************
//EXTERNAL CALLS
//*************************************************************************************************************
doRoundCalls = function () {
if (debug) {
log("Do Round Calls")
}
let verified = verifyTurnorder()
if (!verified) {
return
}
let config = state[combatState].config.turnorder
let turnorder = getTurnorder()
let tokenObj, characterObj, macro
turnorder.forEach((turn) => {
if (turn.id !== getOrCreateMarker().get('id')) {
tokenObj = getObj('graphic',turn.id)
if (tokenObj) {
characterObj = getObj('character',tokenObj.get('represents'))
if (characterObj) {
if (!['None',''].includes(config.allRoundMacro)) {
macro = getMacro(tokenObj, config.allRoundMacro)
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
if ( !['None',''].includes(config.characterRoundMacro) && characterObj.get('controlledby') != '') {
macro = getMacro(tokenObj, config.characterRoundMacro)
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
if (!['None',''].includes(config.roundAPI)) {
sendCalltoChat(tokenObj,characterObj,config.roundAPI)
}
if (!['None',''].includes(config.roundRoll20AM)) {
sendCalltoChat(tokenObj,characterObj,config.roundRoll20AM)
}
if (!['None',''].includes(config.roundFX)) {
doFX(tokenObj,config.roundFX)
}
}
}
}
});
},
doTurnCalls = function (tokenObj) {
if (debug) {
log("Do Turn External Calls")
}
let config = state[combatState].config.turnorder
let characterObj = getObj('character',tokenObj.get('represents'));
let key, condition, ability, macro
if (characterObj) {
if (!['None',''].includes(config.turnMacro)) {
ability = findObjs({_characterid:tokenObj.get('represents'), _type:'ability', name:config.turnMacro})[0]
if (ability) {
sendCalltoChat(tokenObj,characterObj,ability.get('action'))
} else {
macro = findObjs({_type:'macro', name:config.turnMacro})[0]
if (macro) {
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
}
}
for (key in state[combatState].conditions) {
condition = state[combatState].conditions[key]
if (tokenObj.get('_id') == condition.id && condition.addPersistentMacro) {
ability = findObjs({_characterid:tokenObj.get('represents'), _type:'ability', name:condition.addPersistentMacro})[0]
if (ability) {
sendCalltoChat(tokenObj,characterObj,ability.get('action'))
} else {
macro = findObjs({_type:'macro', name:condition.addPersistentMacro})[0]
if (macro) {
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
}
}
}
if (!['None',''].includes(config.turnAPI)) {
sendCalltoChat(tokenObj,characterObj,config.turnAPI)
}
if (!['None',''].includes(config.turnRoll20AM)) {
sendCalltoChat(tokenObj,characterObj,config.roundRoll20AM)
}
if (!['None',''].includes(config.turnFX)) {
doFX(tokenObj,config.turnFX)
}
}
},
doAddConditionCalls = function (tokenObj,key,playerID) {
if (debug) {
log("Do Add Condition Calls")
}
let condition = getConditionByKey(key)
if (!condition) {
return
}
let characterObj = getObj('character',tokenObj.get('represents'));
let macro
if (characterObj) {
if (!['None',''].includes(condition.addMacro)) {
macro = findObjs({_type:'macro', name:condition.addMacro})[0]
if (macro) {
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
}
if (!['None',''].includes(condition.addPersistentMacro)) {
macro = findObjs({_type:'macro', name:condition.addPersistentMacro})[0]
if (macro) {
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
}
if (!['None',''].includes(condition.addAPI)) {
sendCalltoChat(tokenObj,characterObj,condition.addAPI)
}
if (!['None',''].includes(condition.addRoll20AM)) {
sendCalltoChat(tokenObj,characterObj,condition.addRoll20AM)
}
if (!['None',''].includes(condition.addFX)) {
doFX(tokenObj,condition.addFX)
}
}
},
doRemoveConditionCalls = function (tokenObj,key) {
if (debug) {
log("Do Remove Condition Calls")
}
let condition = getConditionByKey(key)
if (!condition) {
return
}
let characterObj = getObj('character',tokenObj.get('represents'));
let macro
if (characterObj) {
if (!['None',''].includes(condition.remMacro)) {
macro = findObjs({_type:'macro', name:condition.remMacro})[0]
if (macro) {
sendCalltoChat(tokenObj,characterObj,macro.get('action'))
}
}
if (!['None',''].includes(condition.remAPI)) {
sendCalltoChat(tokenObj,characterObj,condition.remAPI)
}
if (!['None',''].includes(condition.remRoll20AM)) {
sendCalltoChat(tokenObj,characterObj,condition.remRoll20AM)
}
if (!['None',''].includes(condition.remFX)) {
doFX(tokenObj,condition.remFX)
}
}
},
sendCalltoChat = function(tokenObj,characterObj,action) {
if (debug) {
log("sendCalltoChat")
}
let substitutions = state[combatState].config.macro.substitutions
let replaceString
if (substitutions) {
substitutions.forEach((substitution) => {
replaceString = new RegExp(substitution.action, "g");
if (substitution.type == 'CharName') {
action = action.replace(replaceString, characterObj.get('name'), 'g');
} else if (substitution.type == 'CharID') {
action = action.replace(replaceString, characterObj.get('_id'), 'g')
} else if (substitution.type == 'TokenID') {
action = action.replace(replaceString, tokenObj.get('_id'), 'g')
} else if (substitution.type == 'PlayerID') {
action = action.replace(replaceString, state[combatState].config.gmPlayerID, 'g')
}
})
}
sendChat(tokenObj.get('name'), action, null, {noarchive:true});
},
doFX = function (tokenObj, fx) {
if(tokenObj.get('layer') === 'gmlayer') return;
let pos = {x: tokenObj.get('left'), y: tokenObj.get('top')};
spawnFxBetweenPoints(pos, pos, fx, tokenObj.get('pageid'));
},
getMacro = function(tokenObj, name) {
let macro = findObjs({_characterid:tokenObj.get('represents'), _type:'ability', name:name})[0]
if (!action) {
macro = findObjs({_type:'macro', name:config.turnMacro})[0]
}
return macro
},
//*************************************************************************************************************
//SUBSTITUTIONS
//*************************************************************************************************************
newSubstitution = function(cmdDetails) {
if (debug) {
log('Add Substitution')
}
let substitution = {
type: cmdDetails.details.type,
action: cmdDetails.details.action
}
state[combatState].config.macro.substitutions.push(substitution)
log(state[combatState].config.macro.substitutions)
sendMacroMenu();
},
removeSubstitution = function(cmdDetails) {
if (debug) {
log('Remove Substitution')
}
state[combatState].config.macro.substitutions.forEach((substitution, i) => {
if (substitution.action == cmdDetails.details.action) {
state[combatState].config.macro.substitutions.splice(i,1)
}
})
sendMacroMenu();
},
inFight = function () {
return (Campaign().get('initiativepage') !== false);
},
updatePR = function (turn, modifier) {
let turnorder = getTurnorder();
turnorder.forEach((t, i) => {
if(turn.id === t.id && turn.custom === t.custom){
turnorder[i].pr = parseInt(t.pr) + modifier;
}
});
setTurnorder(turnorder);
},
handleLongString = function (str, max=8) {
str = str.split(' ')[0];
return (str.length > max) ? str.slice(0, max) + '...' : str;
},
stopRotate = function () {
clearInterval(rotationInterval);
},
randomBetween = function (min, max) {
return Math.floor(Math.random()*(max-min+1)+min);
},
handeIniativePageChange = function (obj,prev) {
if((obj.get('initiativepage') !== prev.initiativepage && !obj.get('initiativepage'))){
//stopCombat();
}
},
observeTokenChange = function(handler){
if(handler && _.isFunction(handler)){
observers.tokenChange.push(handler);
}
},
notifyObservers = function(event,obj,prev){
_.each(observers[event],function(handler){
handler(obj,prev);
});
},
handleGraphicMovement = function (obj /*, prev */) {
if (debug) {
log ('Handle Graphic Movement')
}
if(!inFight()) return;
let turnorder = getTurnorder()
if (obj.hasOwnProperty("id") && turnorder.length > 0) {
if(getCurrentTurn().id === obj.get('id')){
changeMarker(obj);
}
}
},
handleShapedSheet = function (characterid, condition, add) {
if (debug) {
log ('Handle Shaped Sheet Change')
}
let character = getObj('character', characterid);
if(character){
let sheet = getAttrByName(character.get('id'), 'character_sheet', 'current');
if(!sheet || !sheet.toLowerCase().includes('shaped')) return;
if(!shaped_conditions.includes(condition)) return;
let attributes = {};
attributes[condition] = (add) ? '1': '0';
setAttrs(character.get('id'), attributes);
}
},
//return an array of objects according to key, value, or key and value matching
getObjects = function (obj, key, val) {
var objects = [];
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (typeof obj[i] == 'object') {
objects = objects.concat(getObjects(obj[i], key, val));
} else
//if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not)
if (i == key && obj[i] == val || i == key && val == '') { //
objects.push(obj);
} else if (obj[i] == val && key == ''){
//only add if the object is not already in the array
if (objects.lastIndexOf(obj) == -1){
objects.push(obj);
}
}
}
return objects;
},
esRE = function (s) {
var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g;
return s.replace(escapeForRegexp,"\\$1");
},
HE = (function(){
var entities={
//' ' : '&'+'nbsp'+';',
'<' : '&'+'lt'+';',
'>' : '&'+'gt'+';',
"'" : '&'+'#39'+';',
'@' : '&'+'#64'+';',
'{' : '&'+'#123'+';',
'|' : '&'+'#124'+';',
'}' : '&'+'#125'+';',
'[' : '&'+'#91'+';',
']' : '&'+'#93'+';',
'"' : '&'+'quot'+';'
},
re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g');
return function(s){
return s.replace(re, function(c){ return entities[c] || c; });
};
}()),
ucFirst = function (string) {
return string.charAt(0).toUpperCase() + string.slice(1);
},
buildParty = function(){
var partyList = '|All,all';
_.each(findObjs({type:'player'}),player=>{
let who = getObj('player', player.id).get('displayname');
if (playerIsGM(player.id)){
who = 'gm'
}
partyList += '|'+who+','+who
});
return partyList;
},
setDefaults = function (reset) {
let key, condition
if (debug) {
log ('Set Defaults')
}
const combatDefaults = {
conditions: [],
advantages: {},
config: {
command: 'cmaster',
duration: false,
favorite: false,
previousPage: null,
gmPlayerID: null,
hold: {
held: false,
turnorder: [],
conditions: [],
round: 1
},
concentration: {
command: 'concentration',
iconType: 'Roll20 Defaults',
statusmarker: 'stopwatch',
bar: 1,
send_reminder_to: 'everyone', // character,gm,
auto_add_concentration_marker: true,
auto_roll_save: true,
advantage: false,
bonus_attribute: 'constitution_save_bonus',
show_roll_button: true
},
initiative: {
rollInitiative: 'CombatMaster',
initiativeDie: 20,
initiativeAttributes: 'initiative_bonus',
showInitiative: false,
rollEachRound: false,
apiTargetTokens: 'None'
},
turnorder: {
useMarker: true,
markerType: 'External URL',
externalMarkerURL: 'https://s3.amazonaws.com/files.d20.io/images/52550079/U-3U950B3wk_KRtspSPyuw/thumb.png?1524507826',
nextMarkerType: 'External URL',
nextExternalMarkerURL: 'https://s3.amazonaws.com/files.d20.io/images/66352183/90UOrT-_Odg2WvvLbKOthw/thumb.png?1541422636',
tokenMarkerName: 'None',
tokenMarkerURL: null,
nextTokenMarkerName: 'None',
nextTokenMarkerURL: null,
sortTurnOrder: true,
centerToken: true,
turnAPI: 'None',
turnRoll20AM: 'None',
turnFX: 'None',
turnMacro: 'None',
roundAPI: 'None',
roundRoll20AM: 'None',
roundFX: 'None',
roundMacro: 'None',
characterRoundMacro: 'None',
allRoundMacro: 'None',
},
timer: {
useTimer: true,
time: 120,
skipTurn: true,
sendTimerToChat: true,
showTokenTimer: true,
timerFont: 'Candal',
timerFontSize: 16,
timerFontColor: 'rgb(255, 0, 0)'
},
announcements: {
announceTurn: true,
whisperToGM: false,
announceRound: true,
handleLongName: true,
showNPCTurns: false,
},
macro: {
substitutions: [],
},
status: {
userAllowed: false,
userChanges: false,
sendOnlyToGM: false,
sendConditions: true,
clearConditions: false,
showConditions: 'all',
useMessage: false,
access: null,
},
conditions: {
blinded: {
name: 'Blinded',
key: 'blinded',
description: '
A blinded creature cannot see and automatically fails any ability check that requires sight.
Attack rolls against the creature have advantage, and the creature making Attack rolls have disadvantage.
',
icon: 'bleeding-eye',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
charmed: {
name: 'Charmed',
key: 'charmed',
description: "
A charmed creature can't Attack the charmer or target the charmer with harmful Abilities or magical effects.
The charmer has advantage on any ability check to interact socially with the creature.
",
icon: 'broken-heart',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
deafened: {
name: 'Deafened',
key: 'deafened',
description: "
A deafened creature can't hear and automatically fails any ability check that requires hearing.
",
icon: 'edge-crack',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
frightened: {
name: 'Frightened',
key: 'frightened',
description: "
A frightened creature has disadvantage on Ability Checks and Attack rolls while the source of its fear is within line of sight.
The creature can't willingly move closer to the source of its fear.
",
icon: 'screaming',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
grappled: {
name: 'Grappled',
key: 'grappled',
description: "
A grappled creature's speed becomes 0, and it can’t benefit from any bonus to its speed.
The condition ends if the Grappler is incapacitated.
The condition also ends if an effect removes the grappled creature from the reach of the Grappler or Grappling effect, such as when a creature is hurled away by the Thunderwave spell.
",
icon: 'grab',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
incapacitated: {
name: 'Incapacitated',
key: 'incapacitated',
description: "
An incapacitated creature can't take actions or reactions.
",
icon: 'interdiction',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
inspiration: {
name: 'Inspiration',
key: 'inspiration',
description: "
If you have inspiration, you can expend it when you make an Attack roll, saving throw, or ability check. Spending your inspiration gives you advantage on that roll.
Additionally, if you have inspiration, you can reward another player for good roleplaying, clever thinking, or simply doing something exciting in the game. When another player character does something that really contributes to the story in a fun and interesting way, you can give up your inspiration to give that character inspiration.
",
icon: 'black-flag',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
invisibility: {
name: 'Invisibility',
key: 'invisibility',
description: "
An invisible creature is impossible to see without the aid of magic or a Special sense. For the purpose of Hiding, the creature is heavily obscured. The creature's location can be detected by any noise it makes or any tracks it leaves.
Attack rolls against the creature have disadvantage, and the creature's Attack rolls have advantage.
",
icon: 'ninja-mask',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
paralyzed: {
name: 'Paralyzed',
key: 'paralyzed',
description: "
A paralyzed creature is incapacitated and can't move or speak.
The creature automatically fails Strength and Dexterity saving throws.
Attack rolls against the creature have advantage.
Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.
",
icon: 'pummeled',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
petrified: {
name: 'Petrified',
key: 'petrified',
description: "
A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.
The creature is incapacitated, can't move or speak, and is unaware of its surroundings.
Attack rolls against the creature have advantage.
The creature automatically fails Strength and Dexterity saving throws.
The creature has Resistance to all damage.
The creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.
",
icon: 'frozen-orb',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
poisoned: {
name: 'Poisoned',
key: 'poisoned',
description: '
A poisoned creature has disadvantage on Attack rolls and Ability Checks.
',
icon: 'chemical-bolt',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
prone: {
name: 'Prone',
key: 'prone',
description: "
A prone creature's only Movement option is to crawl, unless it stands up and thereby ends the condition.
The creature has disadvantage on Attack rolls.
An Attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the Attack roll has disadvantage.
",
icon: 'back-pain',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
restrained: {
name: 'Restrained',
key: 'restrained',
description: "
A restrained creature's speed becomes 0, and it can't benefit from any bonus to its speed.
Attack rolls against the creature have advantage, and the creature's Attack rolls have disadvantage.
The creature has disadvantage on Dexterity saving throws.
",
icon: 'fishing-net',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
stunned: {
name: 'Stunned',
key: 'stunned',
description: "
A stunned creature is incapacitated, can't move, and can speak only falteringly.
The creature automatically fails Strength and Dexterity saving throws.
Attack rolls against the creature have advantage.
",
icon: 'fist',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
unconscious: {
name: 'Unconscious',
key: 'unconscious',
description: "
An unconscious creature is incapacitated, can't move or speak, and is unaware of its surroundings.
The creature drops whatever it's holding and falls prone.
The creature automatically fails Strength and Dexterity saving throws.
Attack rolls against the creature have advantage.
Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.
",
icon: 'sleepy',
iconType: 'Combat Master',
duration: 1,
direction: -1,
override: true,
favorite: false,
message: 'None',
targeted: false,
addAPI: 'None',
addRoll20AM: 'None',
addFX: 'None',
addMacro: 'None',
addPersistentMacro: false,
remAPI: 'None',
remRoll20AM: 'None',
remFX: 'None',
remMacro: 'None',
},
},
},
};
if(!state[combatState].config || typeof state[combatState].config == 'undefined' || reset) {
state[combatState].config = combatDefaults.config;
} else {
if(!state[combatState].config.hasOwnProperty('command')){
state[combatState].config.command = combatDefaults.config.command;
}
if(!state[combatState].config.hasOwnProperty('favorite')){
state[combatState].config.favorite = combatDefaults.config.favorite;
}
if(!state[combatState].config.hasOwnProperty('previousPage')){
state[combatState].config.previousPage = combatDefaults.config.previousPage;
}
if(!state[combatState].config.hasOwnProperty('hold')){
state[combatState].config.hold = combatDefaults.config.hold;
} else {
if(!state[combatState].config.hold.hasOwnProperty('held')){
state[combatState].config.hold.held = combatDefaults.config.hold.held;
}
if(!state[combatState].config.hold.hasOwnProperty('turnorder')){
state[combatState].config.hold.turnorder = combatDefaults.config.hold.turnorder;
}
if(!state[combatState].config.hold.hasOwnProperty('conditions')){
state[combatState].config.hold.conditions = combatDefaults.config.hold.conditions;
}
if(!state[combatState].config.hold.hasOwnProperty('round')){
state[combatState].config.hold.round = combatDefaults.config.hold.round;
}
}
if(!state[combatState].config.hasOwnProperty('initiative')){
state[combatState].config.initiative = combatDefaults.config.initiative;
} else {
if(!state[combatState].config.initiative.hasOwnProperty('initiativeAttributes')){
state[combatState].config.initiative.initiativeAttributes = combatDefaults.config.initiative.initiativeAttributes;
}
if(!state[combatState].config.initiative.hasOwnProperty('rollInitiative')){
state[combatState].config.initiative.rollInitiative = combatDefaults.config.initiative.rollInitiative;
}
if(!state[combatState].config.initiative.hasOwnProperty('initiativeDie')){
state[combatState].config.initiative.initiativeDie = combatDefaults.config.initiative.initiativeDie;
}
if(!state[combatState].config.initiative.hasOwnProperty('rollEachRound')){
state[combatState].config.initiative.rollEachRound = combatDefaults.config.initiative.rollEachRound;
}
if(!state[combatState].config.initiative.hasOwnProperty('apiTargetTokens')){
state[combatState].config.initiative.apiTargetTokens = combatDefaults.config.initiative.apiTargetTokens;
}
}
if(!state[combatState].config.hasOwnProperty('turnorder')){
state[combatState].config.turnorder = combatDefaults.config.turnorder;
} else {
if(!state[combatState].config.turnorder.hasOwnProperty('useMarker')){
state[combatState].config.turnorder.useMarker = combatDefaults.config.turnorder.useMarker;
}
if(!state[combatState].config.turnorder.hasOwnProperty('markerType')){
state[combatState].config.turnorder.markerType = combatDefaults.config.turnorder.markerType;
}
if(!state[combatState].config.turnorder.hasOwnProperty('externalMarkerURL')){
state[combatState].config.turnorder.externalMarkerURL = combatDefaults.config.turnorder.externalMarkerURL;
}
if(!state[combatState].config.turnorder.hasOwnProperty('nextMarkerType')){
state[combatState].config.turnorder.nextMarkerType = combatDefaults.config.turnorder.nextMarkerType;
}
if(!state[combatState].config.turnorder.hasOwnProperty('nextExternalMarkerURL')){
state[combatState].config.turnorder.nextExternalMarkerURL = combatDefaults.config.turnorder.nextExternalMarkerURL;
}
if(!state[combatState].config.turnorder.hasOwnProperty('tokenMarkerName')){
state[combatState].config.turnorder.tokenMarkerName = combatDefaults.config.turnorder.tokenMarkerName;
}
if(!state[combatState].config.turnorder.hasOwnProperty('tokenMarkerURL')){
state[combatState].config.turnorder.tokenMarkerURL = combatDefaults.config.turnorder.tokenMarkerURL;
}
if(!state[combatState].config.turnorder.hasOwnProperty('nextTokenMarkerName')){
state[combatState].config.turnorder.nextTokenMarkerName = combatDefaults.config.turnorder.nextTokenMarkerName;
}
if(!state[combatState].config.turnorder.hasOwnProperty('nextTokenMarkerURL')){
state[combatState].config.turnorder.nextTokenMarkerURL = combatDefaults.config.turnorder.nextTokenMarkerURL;
}
if(!state[combatState].config.turnorder.hasOwnProperty('centerToken')){
state[combatState].config.turnorder.centerToken = combatDefaults.config.turnorder.centerToken;
}
if(!state[combatState].config.turnorder.hasOwnProperty('sortTurnOrder')){
state[combatState].config.turnorder.sortTurnOrder = combatDefaults.config.turnorder.sortTurnOrder;
}
if(!state[combatState].config.turnorder.hasOwnProperty('turnAPI')){
state[combatState].config.turnorder.turnAPI = combatDefaults.config.turnorder.turnAPI;
}
if(!state[combatState].config.turnorder.hasOwnProperty('turnRoll20AM')){
state[combatState].config.turnorder.turnRoll20AM = combatDefaults.config.turnorder.turnRoll20AM;
}
if(!state[combatState].config.turnorder.hasOwnProperty('turnFX')){
state[combatState].config.turnorder.turnFX = combatDefaults.config.turnorder.turnFX;
}
if(!state[combatState].config.turnorder.hasOwnProperty('turnMacro')){
state[combatState].config.turnorder.turnMacro = combatDefaults.config.turnorder.turnMacro;
}
if(!state[combatState].config.turnorder.hasOwnProperty('roundAPI')){
state[combatState].config.turnorder.roundAPI = combatDefaults.config.turnorder.roundAPI;
}
if(!state[combatState].config.turnorder.hasOwnProperty('roundRoll20AM')){
state[combatState].config.turnorder.roundRoll20AM = combatDefaults.config.turnorder.roundRoll20AM;
}
if(!state[combatState].config.turnorder.hasOwnProperty('roundFX')){
state[combatState].config.turnorder.roundFX = combatDefaults.config.turnorder.roundFX;
}
if(!state[combatState].config.turnorder.hasOwnProperty('characterRoundMacro')){
state[combatState].config.turnorder.characterRoundMacro = combatDefaults.config.turnorder.characterRoundMacro;
}
if(!state[combatState].config.turnorder.hasOwnProperty('allRoundMacro')){
state[combatState].config.turnorder.allRoundMacro = combatDefaults.config.turnorder.allRoundMacro;
}
}
if(!state[combatState].config.hasOwnProperty('timer')){
state[combatState].config.timer = combatDefaults.config.timer;
} else {
if(!state[combatState].config.timer.hasOwnProperty('useTimer')){
state[combatState].config.timer.useTimer = combatDefaults.config.timer.useTimer;
}
if(!state[combatState].config.timer.hasOwnProperty('time')){
state[combatState].config.timer.time = combatDefaults.config.timer.time;
}
if(!state[combatState].config.timer.hasOwnProperty('skipTurn')){
state[combatState].config.timer.skipTurn = combatDefaults.config.timer.skipTurn;
}
if(!state[combatState].config.timer.hasOwnProperty('sendTimerToChat')){
state[combatState].config.timer.sendTimerToChat = combatDefaults.config.timer.sendTimerToChat;
}
if(!state[combatState].config.timer.hasOwnProperty('showTokenTimer')){
state[combatState].config.timer.showTokenTimer = combatDefaults.config.timer.showTokenTimer;
}
if(!state[combatState].config.timer.hasOwnProperty('timerFont')){
state[combatState].config.timer.timerFont = combatDefaults.config.timer.timerFont;
}
if(!state[combatState].config.timer.hasOwnProperty('timerFontSize')){
state[combatState].config.timer.timerFontSize = combatDefaults.config.timer.timerFontSize;
}
if(!state[combatState].config.timer.hasOwnProperty('timerFontColor')){
state[combatState].config.timer.timerFontColor = combatDefaults.config.timer.timerFontColor;
}
}
if(!state[combatState].config.hasOwnProperty('announcements')){
state[combatState].config.announcements = combatDefaults.config.announcements;
} else {
if(!state[combatState].config.announcements.hasOwnProperty('announceTurn')){
state[combatState].config.announcements.announceTurn = combatDefaults.config.announcements.announceTurn;
}
if(!state[combatState].config.announcements.hasOwnProperty('whisperToGM')){
state[combatState].config.announcements.whisperToGM = combatDefaults.config.announcements.whisperToGM;
}
if(!state[combatState].config.announcements.hasOwnProperty('announceRound')){
state[combatState].config.announcements.announceRound = combatDefaults.config.announcements.announceRound;
}
if(!state[combatState].config.announcements.hasOwnProperty('handleLongName')){
state[combatState].config.announcements.handleLongName = combatDefaults.config.announcements.handleLongName;
}
if(!state[combatState].config.announcements.hasOwnProperty('showNPCTurns')){
state[combatState].config.announcements.showNPCTurns = combatDefaults.config.announcements.showNPCTurns;
}
}
if(!state[combatState].config.hasOwnProperty('macro')){
state[combatState].config.macro = combatDefaults.config.macro;
}
if(!state[combatState].config.hasOwnProperty('status')) {
state[combatState].config.status = combatDefaults.config.status;
} else {
if(!state[combatState].config.status.hasOwnProperty('userChanges')){
state[combatState].config.status.userChanges = combatDefaults.config.status.userChanges;
}
if(!state[combatState].config.status.hasOwnProperty('sendOnlyToGM')){
state[combatState].config.status.sendOnlyToGM = combatDefaults.config.status.sendOnlyToGM;
}
if(!state[combatState].config.status.hasOwnProperty('sendConditions')){
state[combatState].config.status.sendConditions = combatDefaults.config.status.sendConditions;
}
if(!state[combatState].config.status.hasOwnProperty('clearConditions')){
state[combatState].config.status.clearConditions = combatDefaults.config.status.clearConditions;
}
if(!state[combatState].config.status.hasOwnProperty('useMessage')){
state[combatState].config.status.useMessage = combatDefaults.config.status.useMessage;
}
if(!state[combatState].config.status.hasOwnProperty('showConditions')){
state[combatState].config.status.showConditions = combatDefaults.config.status.showConditions;
}
}
}
if(!state[combatState].hasOwnProperty('conditions')){
state[combatState].conditions = [];
}
if(state[combatState].config.hasOwnProperty('conditions') && !reset){
for (key in state[combatState].config.conditions) {
condition = getConditionByKey(key)
if (!condition.hasOwnProperty('key')) {
condition.key = key
}
if (!condition.hasOwnProperty('duration')) {
condition.duration = 1
}
if (!condition.hasOwnProperty('direction')) {
condition.direction = 0
}
if (!condition.hasOwnProperty('override')) {
condition.override = true
}
if (!condition.hasOwnProperty('favorite')) {
condition.favorite = false
}
if (!condition.hasOwnProperty('message')) {
condition.message = 'None'
}
if (!condition.hasOwnProperty('targeted')) {
condition.targeted = false
}
if (!condition.hasOwnProperty('iconType')) {
condition.iconType = 'Combat Master'
}
if (!condition.hasOwnProperty('addAPI')) {
condition.addAPI = 'None'
}
if (!condition.hasOwnProperty('addRoll20AM')) {
condition.addRoll20AM = 'None'
}
if (!condition.hasOwnProperty('addFX')) {
condition.addFX = 'None'
}
if (!condition.hasOwnProperty('addMacro')) {
condition.addMacro = 'None'
}
if (!condition.hasOwnProperty('addPersistentMacro')) {
condition.addPersistentMacro = false
}
if (!condition.hasOwnProperty('remAPI')) {
condition.remAPI = 'None'
}
if (!condition.hasOwnProperty('remRoll20AM')) {
condition.remRoll20AM = 'None'
}
if (!condition.hasOwnProperty('remFX')) {
condition.remFX = 'None'
}
if (!condition.hasOwnProperty('remMacro')) {
condition.remMacro = 'None'
}
};
} else if (!state[combatState].config.hasOwnProperty('conditions') || reset) {
state[combatState].config.conditions = combatDefaults.config.conditions;
}
},
showHelp = function(cmdDetails) {
let handout
let title
if (cmdDetails.details.held) {
title = 'Main Menu Held'
} else if (cmdDetails.details.started) {
title = 'Main Menu Started'
} else if (cmdDetails.details.stopped) {
title = 'Main Menu Stopped'
} else if (cmdDetails.details.setup) {
title = 'Setup Menu'
} else if (cmdDetails.details.initiative) {
title = 'Initiative Menu'
} else if (cmdDetails.details.turnorder) {
title = 'Turnorder Menu'
} else if (cmdDetails.details.timer) {
title = 'Timer Menu'
} else if (cmdDetails.details.announcements) {
title = 'Announcements Menu'
} else if (cmdDetails.details.macro) {
title = 'Macro & API Menu'
} else if (cmdDetails.details.status) {
title = 'Status Menu'
} else if (cmdDetails.details.conditions) {
title = 'Conditions Menu'
} else if (cmdDetails.details.condition) {
title = 'Condition Menu'
}
handout = findHandout(title)
log(handout)
makeAndSendMenu(`
View Help`,title,'gm')
},
buildHelp = function() {
log('Building Help')
let mainStarted = createHandout('Main Menu Started')
let mainStopped = createHandout('Main Menu Stopped')
let mainHeld = createHandout('Main Menu Held')
let menuSetup = createHandout('Setup Menu')
let menuInitiative = createHandout('Initiative Menu')
let menuTurnorder = createHandout('Turnorder Menu')
let menuTimer = createHandout('Timer Menu')
let menuAnnouncements = createHandout('Accouncements Menu')
let menuMacro = createHandout('Macro & API Menu')
let menuStatus = createHandout('Status Menu')
let menuConditions = createHandout('Conditions Menu')
let menuCondition = createHandout('Condition Menu')
let menuExport = createHandout('Export Menu')
setTimeout(function() {
buildMainMenuStarted(mainStarted,menuSetup.id,menuCondition.id)
buildMainMenuStopped(mainStopped,menuSetup.id,menuCondition.id)
buildMainMenuHeld(mainHeld,menuSetup.id,menuCondition.id)
buildSetupMenu(menuSetup,menuInitiative.id,menuTurnorder.id,menuTimer.id,menuAnnouncements.id,menuMacro.id,menuStatus.id,menuConditions.id,menuExport.id)
buildInitiativeMenu(menuInitiative,menuSetup.id)
buildTurnorderMenu(menuTurnorder,menuSetup.id)
buildTimerMenu(menuTimer,menuSetup.id)
buildAnnouncementsMenu(menuAnnouncements,menuSetup.id)
buildMacroMenu(menuMacro,menuSetup.id)
buildStatusMenu(menuStatus,menuSetup.id)
buildConditionsMenu(menuConditions,menuSetup.id)
buildConditionMenu(menuCondition,menuSetup.id)
},1000)
},
findHandout = function (title) {
let handout = findObjs({_type:'handout', name:title})
return handout
},
createHandout = function (title) {
let handout = findHandout(title)
if (handout[0]) {
handout[0].remove()
}
handout = createObj('handout', {
name:title,
archived:true
})
return handout
},
buildMainMenuStarted = function(handout,setupID,conditionID) {
let notes
notes = `
- Start Combat — Starts up combat. Must have tokens selected if using CM to roll initiative.
- Favorites Menu — Show favorites menu (the default is to display all conditions).
- Set a condition as favorite by clicking on the globe icon next to it.
- If clicked, a star icon appears and the condition will show up on favorites menu.
- Favorites menu shows only conditions tagged as favorite.
- Setup — Shows the Setup Menu.
- Started — A green bar indicates that combat is started.
- Help — All pages have a Help button which opens the handout related to the page being viewed.
`
notes += buildMainConditions('https://s3.amazonaws.com/files.d20.io/images/133804430/JJ--U559pOgsd9UBpUb06g/original.png?15892970605',conditionID)
notes += `
`
handout.set({notes:notes});
},
buildMainMenuStopped = function(handout,setupID,conditionID) {
let notes
notes = `
- Start Combat — Starts up combat. Must have tokens selected if using CM to roll initiative.
- Favorites Menu — Show favorites menu (the default is to display all conditions).
- Set a condition as favorite by clicking on the globe icon next to it.
- If clicked, a star icon appears and the condition will show up on favorites menu.
- Favorites menu shows only conditions tagged as favorite.
- Setup — Shows the Setup Menu.
- Stopped — A red bar indicates that combat is stopped.
- Help — All pages have a Help button which opens the handout related to the page being viewed.
`
notes += buildMainConditions('https://s3.amazonaws.com/files.d20.io/images/133804645/pmJuadcB01opW3Lg8lyOYA/original.png?15892970725',conditionID)
notes += `
`
handout.set({notes:notes});
},
buildMainMenuHeld = function(handout,setupID,conditionID) {
let notes
notes = `
- Start Combat — Starts up combat. Must have tokens selected if using CM to roll initiative.
- Favorites Menu — Show favorites menu (the default is to display all conditions).
- Set a condition as favorite by clicking on the globe icon next to it.
- If clicked, a star icon appears and the condition will show up on favorites menu.
- Favorites menu shows only conditions tagged as favorite.
- Setup — Shows the Setup Menu.
- Held — A yellow bar indicates that combat is being held.
- Help — All pages have a Help button which opens the handout related to the page being viewed.
`
notes += buildMainConditions('https://s3.amazonaws.com/files.d20.io/images/133804415/0Te1DEzFMolSiIj7DfZfTw/original.png?15892970575', conditionID)
notes += `
`
handout.set({notes:notes});
},
buildMainConditions = function(image,conditionID) {
let notes = `
- Icon — The default or custom token marker assigned to the condition is displayed here. If the condition uses the Token Condition script, it will simply show "TC" here.
- Name — The name of the condition.
- Add — Add the condition to the selected token(s). Will use the conditions settings for Duration, Default, Override, and Messages. Will invoke any API commands and/or Macros assigned to the condition.
- Remove — Removes the condition from the selected token(s).
- Favorite — If a star is displayed, the condition will show in the favorites menu. If a globe is displayed, the condition will only show in the all conditions menu. Clicking on either the star or globe icon for each condition will toggle if it's a favorite or not.
- Edit — Shows the Condition Menu for that condition.
`
return notes
},
buildSetupMenu = function(handout,initiativeID,turnorderID,timerID,announceID,macroID,statusID,conditionsID,exportID) {
let notes = `
- Combat Setup
- Initiative — Configure how CombatMaster will roll Initiative.
- Turnorder — Configure how the turnorder is managed.
- Timer — Configure a timer, its length, and how it is displayed.
- Announce — Configure how turns are announced in chat.
- Macro & API — Configure substitution strings for use in macros and API commands.
- Status Setup
- Status — Configure how conditions are managed and displayed.
- Conditions — A list of all conditions in CombatMaster; here, you can edit existing conditions or add new ones.
- Export — Puts a configuration code in chat to copy so you can import your conditions and settings into another game with CombatMaster. Simply triple-click the code to select it entirely (this also avoids selecting anything outside the code block). Save it in a handout to easily transmogrify to other games, or save it as a file on your computer.
'
- Import — Import your configuration from another game.
NOTE: If migrating from CombatMaster to another CombatMaster, it will copy the entire CombatMaster configuration. If coming from CombatTracker, it will only copy the conditions and you’ll have to reconfigure everything else. Importing from StatusInfo is not supported.
- Reset CombatMaster — This resets the entire session state. It defaults the conditions to D&D 5e.
- Back — Return to the Main Menu.
`
handout.set({notes:notes});
},
buildInitiativeMenu = function(handout,setupID) {
let notes = `
- Roll Initiative — Choose how you want CombatMaster to roll Initiative.
- None — CombatMaster may be configured to not roll initiative. You can have each character roll initiative on their own.
NOTE: If you choose to not roll initiative from CombatMaster, the turn order will need to be set before starting combat.
- CombatMaster — CombatMaster has its own initiative roller. To use it, select the tokens involved in the encounter, then click the Start button in the Main Menu.
- Roll Each Round — Rerolls initiative at the end of each round.
- Initiative Attr — Accepts a comma delimited list of attributes that make up initiative. The attribute name must match the attribute names in the character sheet.
- Initiative Die — Set the type of dice that CombatMaster will roll for each character.
- Show Initiative in Chat — Displays the initiative rolls in chat.
- Group-Init — Calls on GroupInitiative to build the turnorder when you click the Start button in the Main Menu.
NOTE: If you choose Group-Init, the GroupInitiative script must be installed in your game and configured outside of CombatMaster. - Roll Each Round — Rerolls initiative at the end of each round.
- Target Tokens — Not functional yet>
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildTurnorderMenu = function(handout,setupID) {
let notes = `
- Turnorder Setup — configure what happens when progressing through the turnorder.
- Sort Turnorder — Sorts the turnorder in descending sequence (only) once created.
- Center Map on Token — Will center the map for all players on the token currently active in the turnorder using the Ping function. This will not center the map if the token is on the GM Layer.
- Use Marker — Determines if the marker is visible to players or always stays on the GM Layer. If visible, the marker will only move to the GM Layer if a token in the turnorder is on the GM Layer. It will do switch layers before moving to that token, and after moving to the next token, so as not to give away the position of any tokens hidden from players.
- Marker Type — Set to External URL (default) or can be set to Token Marker. If Token Marker is selected a suitable token must be uploaded to your game.
- Marker — A thumbnail of what will be used to highlight the current active character.
- Use Next Marker — If set to true will display another marker around the player that is next in the turnorder. If set to false, then the next player up is not highlighted.
- Next Marker — A thumbnail of what will be used to highlight the next active character.
`
notes += buildExternalCallMenu('
Beginning of Each Round — Set various external calls which will be invoked at the start of each round.')
notes += `
`
notes += buildExternalCallMenu("
Beginning of Each Turn — Set various external calls which will be invoked at the start of each token's turn. Same limitations apply as for
Beginning of Each Round.")
notes += `
Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildExternalCallMenu = function(title,round,condition) {
let notes = `
- ${title}
- API — Must be a full API command. You must use brackets {{ and }} around the command and around each parameter when entering the command. Any inline rolls must be written like [#[1d6]#] instead of [[1d6]].
- Roll20AM — Must be a full Roll20AM command. You must use brackets {{ and }} around the command and around each parameter when entering the command.
- FX — Must be a valid FX command.
`
if (round) {
notes += `- Characters Macro — This uses a global macro substituting in all player characters on the map. Follows other macro rules (see below)
- All Tokens Macro — This uses a global macro substituting in all tokens on the map. Follows other macro rules (see below)
`
} else {
notes += `- Macro — Must be the full macro name (without the #). Any inline rolls within the macro must be written like [#[1d6]#] instead of [[1d6]].
`
}
if (condition) {
notes += `- Persistent Macro — Determines if the assigned macro is repeated at the start of an affected token's turn.`
}
notes += `
`
return notes
},
buildTimerMenu = function(handout,setupID) {
let notes = `
- Turn Timer — Setting to true turns on the timer. The timer displays a red second by second countdown under the current active token in turnorder.
<Image of a token with the timer below it>
- Time — Determine the total time in seconds that the active token has to complete the turn.
- Skip Turn — Automatically advances to the next turn when the timer reaches 0.
- Send to Chat — Sends intermittent alerts to chat when the timer starts, when it reaches the halfway point, when it reaches 10 seconds, and when it reaches 5 seconds.
- Show on Token — Choose whether to display the timer underneath the active token.
- Token Font — Set the font for the displayed timer.
- Token Font Size — Set the font size for the displayed timer.
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildAnnouncementsMenu = function(handout,setupID) {
let notes = `
- Announce Rounds — Sends a message to chat when a new round has started.
- Announce Turns — Sends a message to chat with the current active token if it is not on the GM Layer, plus any assigned conditions or messages.

- Down Arrow Icon — Delays Player Turn.
- CheckBox Icon — Ends Player Turn.
- Condition Name — Click on it to view Condition Description.
- Trashcan Icon — Click on it to remove Condition from Token.
- Whisper GM Only — Choose whether all announcements are only sent to the GM.
- Shorten Long Names — Shortens the token name as displayed in the turn announcement.
- Show NPC Conditions — Choose whether NPC turn announcements are only sent to the GM.
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildMacroMenu = function(handout,setupID) {
let notes = `
This menu is for setting up strings to substitute for various types of calls in Macros and APIs. For example, if you want CombatMaster to run a macro that would normally use @{selected|character_id}, you would need to set up a substitution string for CharID, then use that string in place of @{selected|character_id} in the macro itself.
Substitution strings work best as unique terms that won't be used elsewhere in a command or macro, otherwise CombatMaster may insert a substituted call somewhere it doesn't belong. So you'd want the TokenID substitute to be something like 'tokenidentifier' since that isn't likely to be used anywhere else, whereas 'name' is not a good substitute, because it is a word that is likely to be used in other contexts.
The PlayerID substitution string is specifically for use in TokenMod commands. If you set the PlayerID substitution to something like 'playeridentifier', then a TokenMod command in CombatMaster would look like this:
!token-mod --api-as playeridentifier --ids tokenidentifier --on showname
<Image of the Macro & API menu with labels>
- Type — The type of call being substituted.
- String — The substitution string you have set up, for use in API commands and macros.
- Delete — Delete the substitution on this line.
- Add Substitution — Create a new substitution string.
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildStatusMenu = function(handout,setupID) {
let notes = `
- Whisper GM Only — Choose whether condition descriptions are only sent to the GM.
- Player Allowed Changes — When this is turned on, the player active in the turnorder receives a Menu where they can add or remove conditions from their token.
- Send Changes to Chat — Choose whether condition descriptions are sent to chat when a condition is added to a token.
- Clear Conditions on Close — Choose whether stopping combat removes conditions from all tokens.
- Use Messages — Enables messages to be included with conditions; will query for a message whenever a condition is added to a token.
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildConditionsMenu = function(handout,setupID) {
let notes = `
- Icon — The default or custom token marker assigned to the condition is displayed here. If the condition uses the TokenCondition script, it will simply show "TC" here.
- Name — The name of the condition.
- Edit — Shows the Condition Editing Menu for that condition.
- Add Condition — Create a new condition. You will first be prompted for a condition name, then it will show you the new condition's Editing Menu.
- Back — Return to the Setup Menu.
`
handout.set({notes:notes});
},
buildConditionMenu = function(handout,setupID) {
let notes = `
- Name — The name of the condition
- Icon Type — Determines what options are presented when clicking Icon.
- CombatMaster — Lets you pick from Roll20 default markers only.
- TokenMarker — Requires the libTokenMarker script; lets you pick from any marker sets in your game, including Roll20 default.
NOTE: Token marker names with spaces may cause issues, it is recommended to avoid spaces in token marker names.
- TokenCondition — Requires the TokenCondition script; lets you pick from any characters used by this script.
- Icon — Thumbnail of the marker that will be used by the current condition; click to change (options based on your selection for Icon Type). If using TokenCondition for the condition, thumbnail will be replaced by "TC".
- Duration — Defines the length of the condition before it is removed.
- Direction — Defines how quickly the duration is reduced each round. Set to a negative number to reduce the duration, positive number if it increases over time, or 0. If 0, it remains permanently on the token until manually removed.
- Override — Determines if the direction/duration can be overridden when assigning the condition to the token. For conditions that do not change, set override to false and the direction/duration roll queries do not display when assigning the condition.
- Favorites — Determines if the condition shows in the Favorites menu. This can also be set on the Main Menu. The Favorites menu shows only conditions marked as Favorite.
- Message — Set a default message that will show along with the condition. It can be overridden when assigning the condition. If you have commas in the description, use brackets {{ and }} around it when entering it.
- Targeted — Determines if the condition applies to another token; useful for effects that affect one or more targets but have a duration based on the caster's turn. Applies the condition's marker to the target token(s). Rather than using the @{target} feature, the GM will see the following message in chat:
<Image of the Select Targets message>
- Edit Description — Edit the description that will appear for the condition.
- Delete Condition — Delete the condition from CombatMaster.
- Back — Return to the previous menu.
`
handout.set({notes:notes});
},
buildExportMenu = function() {
let notes = `
This configuration code can be copied so you can import your conditions and settings into another game with CombatMaster. Simply triple-click the code to select it entirely (this also avoids selecting anything outside the code block). Save it in a handout to easily transmogrify to other games, or save it as a file on your computer.
NOTE: If migrating from CombatMaster to another CombatMaster, it will copy the entire CombatMaster configuration. If coming from CombatTracker, it will only copy the conditions and you’ll have to reconfigure everything else. Importing from StatusInfo is not supported.
`
},
checkInstall = function () {
if(!_.has(state, combatState)){
state[combatState] = state[combatState] || {};
}
setDefaults();
buildHelp();
log(script_name + ' Ready! Command: !cmaster --main');
},
registerEventHandlers = function() {
on('chat:message', inputHandler);
on('change:campaign:turnorder', handleTurnorderChange);
on('change:graphic:statusmarkers', handleStatusMarkerChange);
on('change:campaign:initiativepage', handeIniativePageChange);
on('change:graphic:top', handleGraphicMovement);
on('change:graphic:left', handleGraphicMovement);
on('change:graphic:layer', handleGraphicMovement);
if('undefined' !== typeof DeathTracker && DeathTracker.ObserveTokenChange){
DeathTracker.ObserveTokenChange(function(obj,prev) {
handleStatusMarkerChange(obj,prev);
});
}
if('undefined' !== typeof InspirationTracker && InspirationTracker.ObserveTokenChange){
InspirationTracker.ObserveTokenChange(function(obj,prev) {
handleStatusMarkerChange(obj,prev);
});
}
if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange) {
TokenMod.ObserveTokenChange(function(obj,prev) {
handleStatusMarkerChange(obj,prev);
});
}
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers,
ObserveTokenChange: observeTokenChange,
getConditions,
getConditionByKey,
sendConditionToChat,
getDefaultIcon
};
})();
on('ready',function() {
'use strict';
CombatMaster.CheckInstall();
CombatMaster.RegisterEventHandlers();
});