const version = "Hashtag DnD v0.2.2" const rollSynonyms = ["roll"] const createSynonyms = ["create", "generate", "start", "begin", "setup", "party", "member", "new"] const renameCharacterSynonyms = ["renamecharacter", "renameperson"] const cloneCharacterSynonyms = ["clone", "clonecharacter", "cloneperson", "copycharacter", "copyperson", "duplicatecharacter", "duplicateperson", "dupecharacter", "dupeperson"] const bioSynonyms = ["bio", "biography", "summary", "character", "charactersheet", "statsheet"] const setClassSynonyms = ["setclass", "class"] const setSummarySynonyms = ["setsummary", "summary"] const trySynonyms = ["try", "tryto", "tries", "triesto", "attempt", "attemptto", "attemptsto", "do"] const setHealthSynonyms = ["sethealth"] const healSynonyms = ["heal", "mend", "restore"] const damageSynonyms = ["damage", "hurt", "harm", "injure"] const restSynonyms = ["rest", "longrest", "shortrest", "sleep", "nap"] const setExperienceSynonyms = ["setexperience", "setexp", "setxp", "setexperiencepoints"] const addExperienceSynonyms = ["addexperience", "addexp", "addxp", "addexperiencepoints", "experience", "exp", "gainxp", "gainexperience", "xp", "experiencepoints"] const levelUpSynonyms = ["levelup", "level"] const setStatSynonyms = ["setstat", "setstatistic", "setattribute", "setability", "changestat", "changestatistic", "changeattribute", "changeability", "updatestat", "updatestatistic", "updateattribute", "updateability", "stat", "attribute", "ability"] const showStatsSynonym = ["showstats", "stats", "viewstats", "showabilities", "abilities", "viewabilities", "showstatistics", "statistics", "viewstatistics", "showattributes", "attributes", "viewattributes"] const removeStatSynonyms = ["removestat", "deletestat", "cancelstat", "removeability", "deleteability", "cancelAbility", "removestatistic", "deletestatistic", "cancelstatistic", "removeattribute", "deleteattribute", "cancelattribute"] const clearStatsSynonyms = ["clearstats", "clearabilities", "clearstatistics", "clearattributes"] const setSpellStatSynonyms = ["setspellstat", "setspellstatistic", "setspellability", "setspellcastingability", "changespellstat", "changespellstatistic", "changespellability", "changespellcastingability"] const setSkillSynonyms = ["setskill", "changeskill", "updateskill", "skill"] const showSkillsSynonyms = ["showskills", "skills"] const removeSkillSynonyms = ["removeskill", "deleteskill", "cancelskill"] const clearSkillsSynonyms = ["clearskills"] const checkSynonyms = ["check", "checkstat", "checkstatistic", "checkattribute", "checkability", "checkskill", "skillcheck", "abilitycheck"] const showNotesSynonyms = ["notes", "shownotes", "viewnotes"] const noteSynonyms = ["note", "takenote", "setnote", "createnote", "remember"] const clearNotesSynonyms = ["clearnotes"] const eraseNoteSynonyms = ["erasenote", "removenote", "deletenote", "cancelnote"] const takeSynonyms = ["take", "steal", "get", "grab", "receive", "loot"] const buySynonyms = ["buy", "purchase", "barter", "trade", "swap", "exchange"] const sellSynonyms = ["sell"] const dropSynonyms = ["remove", "discard", "drop", "leave", "dispose", "toss", "throw", "throwaway", "trash", "donate", "eat", "consume", "use", "drink", "pay", "lose"] const giveSynonyms = ["give", "handover", "hand", "gift"] const renameItemSynonyms = ["renameitem", "renameobject", "renamegear", "renameequipment"] const inventorySynonyms = ["inv", "inventory", "backpack", "gear", "showinv", "showinventory", "viewinventory", "viewinv"] const clearInventorySynonyms = ["clearinventory", "clearinv", "emptyinventory", "emptybackpack", "clearbackpack", "emptygear", "cleargear"] const learnSpellSynonyms = ["learnspell", "learnmagic", "learnincantation", "learnritual", "memorizespell", "memorizemagic", "memorizeincantation", "memorizeritual", "learnsspell", "learnsmagic", "learnsincantation", "learnsritual", "memorizesspell", "memorizesmagic", "memorizesincantation", "memorizesritual", "learn"] const forgetSpellSynonyms = ["forgetspell", "forgetmagic", "forgetincantation", "forgetritual", "forgetsspell", "forgetsmagic", "forgetsincantation", "forgetsritual", "deletespell", "deletemagic", "deleteincantation", "deleteritual", "deletesspell", "deletesmagic", "deletesincantation", "deletesritual", "cancelspell", "cancelmagic", "cancelincantation", "cancelritual", "cancelsspell", "cancelsmagic", "cancelsincantation", "cancelsritual", "removespell", "removemagic", "removeincantation", "removeritual", "removesspell", "removesmagic", "removesincantation", "removesritual", "forget"] const castSpellSynonyms = ["cast", "castspell", "castmagic", "castincantation", "castritual", "castsspell", "castsmagic", "castsincantation", "castsritual"] const clearSpellsSynonyms = ["clearspells", "clearmagic", "clearincantations", "clearrituals", "forgetallspells", "forgetallmagic", "forgetallincantation", "forgetallritual"] const spellbookSynonyms = ["spellbook", "spells", "listspells", "showspells", "spelllist", "spellcatalog", "spellinventory"] const resetSynonyms = ["reset", "cleandata", "cleardata", "resetdata", "resetsettings", "clearsettings", "profile"] const allSynonyms = ["all", "every", "each", "every one", "everyone"] const attackSynonyms = ["attack", "strike", "ambush", "assault", "fireat", "fireon"] const setMeleeStatSynonyms = ["setmeleestat", "setmeleestatistic", "setmeleeability", "changemeleestat", "changemeleestatistic", "changemeleeability"] const setrangedStatSynonyms = ["setrangedstat", "setrangedstatistic", "setrangedability", "changerangedstat", "changerangedstatistic", "changerangedability"] const showCharactersSynonyms = ["showcharacters", "showparty", "showteam", "characters", "party", "team"] const removeCharacterSynonyms = ["removecharacter", "deletecharacter", "erasecharacter"] const setAutoXpSynonyms = ["setautoxp", "autoxp"] const showAutoXpSynonyms = ["showautoxp"] const setDefaultDifficultySynonyms = ["setdefaultdifficulty", "defaultdifficulty", "setdefaultdc", "defaultdc", "setdefaultac", "defaultac", "setdifficulty", "difficulty", "dc"] const showDefaultDifficultySynonyms = ["showdefaultdifficulty", "showdefaultdc", "showdefaultac"] const generateNameSynonyms = ["generatename", "name", "randomname", "makename", "createname"] const createLocationSynonyms = ["createlocation", "makelocation", "generatelocation", "addlocation", "setlocation", "createplace", "makeplace", "generateplace", "addplace", "setplace", "createtown", "maketown", "generatetown", "addtown", "settown", "createvillage", "makevillage", "generatevillage", "addvillage", "setvillage", "createcity", "makecity", "generatecity", "addcity", "setcity", "updatelocation", "updateplace", "updatetown", "updatevillage", "updatecity"] const goToLocationSynonyms = ["gotolocation", "golocation", "movetolocation", "traveltolocation", "travellocation", "gotoplace", "goplace", "movetoplace", "traveltoplace", "travelplace", "gototown", "gotown", "movetotown", "traveltotown", "traveltown", "gotovillage", "govillage", "movetovillage", "traveltovillage", "travelvillage", "gotocity", "gocity", "movetocity", "traveltocity", "travelcity", "goto", "go", "moveto", "move", "travelto", "travel"] const removeLocationSynonyms = ["removelocation", "deletelocation", "eraselocation", "removeplace", "deleteplace", "eraseplace", "removetown", "deletetown", "erasetown", "removevillage", "deletevillage", "erasevillage", "removecity", "deletecity", "erasecity"] const showLocationsSynonyms = ["showlocations", "showplaces", "showtowns", "showvillages", "showcities", "locations", "places", "towns", "villages", "cities"] const getLocationSynonyms = ["getlocation", "location", "getcoordinates", "coordinates", "getcoords", "coords", "showlocation"] const clearLocationsSynonyms = ["clearlocations", "eraselocations", "deletelocations", "resetlocations"] const mapSynonyms = ["map", "showmap"] const goNorthSynonyms = ["gonorth", "north", "goup", "up", "n"] const goSouthSynonyms = ["gosouth", "south", "godown", "down", "s"] const goEastSynonyms = ["goeast", "east", "goright", "right", "e"] const goWestSynonyms = ["gowest", "west", "goleft", "left", "w"] const showDaySynonyms = ["showday", "showdate", "day", "date"] const setDaySynonyms = ["setday", "setdate"] const encounterSynonyms = ["encounter", "startencounter"] const showEnemiesSynonyms = ["showenemies", "enemies"] const addEnemySynonyms = ["addenemy"] const removeEnemySynonyms = ["removeenemy"] const clearEnemiesSynonyms = ["clearenemies", "resetenemies", "removeenemies"] const initiativeSynonyms = ["initiative"] const setAcSynonyms = ["setac", "setarmorclass", "ac", "armorclass"] const turnSynonyms = ["turn", "doturn", "taketurn"] const fleeSynonyms = ["flee", "retreat", "runaway", "endcombat"] const versionSynonyms = ["version", "ver", "showversion"] const setupEnemySynonyms = ["setupenemy", "createenemy"] const setDamageSynonyms = ["setdamage"] const setProficiencySynonyms = ["setproficiency", "setweaponproficiency"] const helpSynonyms = ["help"] const modifier = (text) => { init() const rawText = text if (state.createStep != null) { text = handleCreateStep(text) if (state.createStep != null) return { text } else text = rawText } if (state.setupEnemyStep != null) { state.setupEnemyStep text = handleSetupEnemyStep(text) if (state.setupEnemyStep != null) return { text } else text = rawText } if (state.initialized == null || !text.includes("#")) { state.initialized = true; return { text } } state.characterName = getCharacterName(rawText) text = sanitizeText(text) var lineBreakIndex = text.indexOf("\n") if (lineBreakIndex > -1) { state.flavorText = text.substring(lineBreakIndex + 1) if (!state.flavorText.startsWith(" ")) state.flavorText = " " + state.flavorText text = text.substring(0, lineBreakIndex) } else { state.flavorText = null } command = text.substring(text.search(/#/) + 1) var commandName = getCommandName(command).toLowerCase().replaceAll(/[^a-z0-9\s]*/gi, "") if (state.characterName == null || !hasCharacter(state.characterName)) { var found = processCommandSynonyms(command, commandName, createSynonyms, function () {return true}) if (state.characterName == null && found) { state.show = "none" text = `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n` return { text } } if (!found) found = processCommandSynonyms(command, commandName, helpSynonyms.concat(rollSynonyms, noteSynonyms, eraseNoteSynonyms, showNotesSynonyms, clearNotesSynonyms, showCharactersSynonyms, removeCharacterSynonyms, generateNameSynonyms, setDefaultDifficultySynonyms, showDefaultDifficultySynonyms, renameCharacterSynonyms, cloneCharacterSynonyms, createLocationSynonyms, showLocationsSynonyms, goToLocationSynonyms, removeLocationSynonyms, getLocationSynonyms, clearLocationsSynonyms, goNorthSynonyms, goSouthSynonyms, goEastSynonyms, goWestSynonyms, encounterSynonyms, showEnemiesSynonyms, addEnemySynonyms, removeEnemySynonyms, clearEnemiesSynonyms, initiativeSynonyms, turnSynonyms, fleeSynonyms, versionSynonyms, setupEnemySynonyms, healSynonyms, damageSynonyms, restSynonyms, addExperienceSynonyms, resetSynonyms), function () {return true}) if (found == null) { if (state.characterName == null) { state.show = "none" text = `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n` return { text } } else { state.show = "none" text = `\n[Error: Character ${state.characterName} does not exist. Type #setup to create this character]\n` return { text } } } } text = processCommandSynonyms(command, commandName, rollSynonyms, doRoll) if (text == null) text = processCommandSynonyms(command, commandName, createSynonyms, doCreate) if (text == null) text = processCommandSynonyms(command, commandName, showCharactersSynonyms, doShowCharacters) if (text == null) text = processCommandSynonyms(command, commandName, removeCharacterSynonyms, doRemoveCharacter) if (text == null) text = processCommandSynonyms(command, commandName, bioSynonyms, doBio) if (text == null) text = processCommandSynonyms(command, commandName, setClassSynonyms, doSetClass) if (text == null) text = processCommandSynonyms(command, commandName, setSummarySynonyms, doSetSummary) if (text == null) text = processCommandSynonyms(command, commandName, setHealthSynonyms, doSetHealth) if (text == null) text = processCommandSynonyms(command, commandName, healSynonyms, doHeal) if (text == null) text = processCommandSynonyms(command, commandName, damageSynonyms, doDamage) if (text == null) text = processCommandSynonyms(command, commandName, restSynonyms, doRest) if (text == null) text = processCommandSynonyms(command, commandName, setExperienceSynonyms, doSetExperience) if (text == null) text = processCommandSynonyms(command, commandName, addExperienceSynonyms, doAddExperience) if (text == null) text = processCommandSynonyms(command, commandName, levelUpSynonyms, doLevelUp) if (text == null) text = processCommandSynonyms(command, commandName, showStatsSynonym, doShowStats) if (text == null) text = processCommandSynonyms(command, commandName, setStatSynonyms, doSetStat) if (text == null) text = processCommandSynonyms(command, commandName, setSpellStatSynonyms, doSetSpellStat) if (text == null) text = processCommandSynonyms(command, commandName, showSkillsSynonyms, doShowSkills) if (text == null) text = processCommandSynonyms(command, commandName, setSkillSynonyms, doSetSkill) if (text == null) text = processCommandSynonyms(command, commandName, checkSynonyms, doCheck) if (text == null) text = processCommandSynonyms(command, commandName, trySynonyms, doTry) if (text == null) text = processCommandSynonyms(command, commandName, showNotesSynonyms, doShowNotes) if (text == null) text = processCommandSynonyms(command, commandName, noteSynonyms, doNote) if (text == null) text = processCommandSynonyms(command, commandName, clearNotesSynonyms, doClearNotes) if (text == null) text = processCommandSynonyms(command, commandName, eraseNoteSynonyms, doEraseNote) if (text == null) text = processCommandSynonyms(command, commandName, takeSynonyms, doTake) if (text == null) text = processCommandSynonyms(command, commandName, dropSynonyms, doDrop) if (text == null) text = processCommandSynonyms(command, commandName, giveSynonyms, doGive) if (text == null) text = processCommandSynonyms(command, commandName, renameItemSynonyms, doRenameItem) if (text == null) text = processCommandSynonyms(command, commandName, inventorySynonyms, doInventory) if (text == null) text = processCommandSynonyms(command, commandName, clearInventorySynonyms, doClearInventory) if (text == null) text = processCommandSynonyms(command, commandName, learnSpellSynonyms, doLearnSpell) if (text == null) text = processCommandSynonyms(command, commandName, forgetSpellSynonyms, doForgetSpell) if (text == null) text = processCommandSynonyms(command, commandName, castSpellSynonyms, doCastSpell) if (text == null) text = processCommandSynonyms(command, commandName, clearSpellsSynonyms, doClearSpells) if (text == null) text = processCommandSynonyms(command, commandName, spellbookSynonyms, doSpellbook) if (text == null) text = processCommandSynonyms(command, commandName, removeStatSynonyms, doRemoveStat) if (text == null) text = processCommandSynonyms(command, commandName, clearStatsSynonyms, doClearStats) if (text == null) text = processCommandSynonyms(command, commandName, removeSkillSynonyms, doRemoveSkill) if (text == null) text = processCommandSynonyms(command, commandName, clearSkillsSynonyms, doClearSkills) if (text == null) text = processCommandSynonyms(command, commandName, attackSynonyms, doAttack) if (text == null) text = processCommandSynonyms(command, commandName, setMeleeStatSynonyms, doSetMeleeStat) if (text == null) text = processCommandSynonyms(command, commandName, setrangedStatSynonyms, doSetRangedStat) if (text == null) text = processCommandSynonyms(command, commandName, buySynonyms, doBuy) if (text == null) text = processCommandSynonyms(command, commandName, sellSynonyms, doSell) if (text == null) text = processCommandSynonyms(command, commandName, resetSynonyms, doReset) if (text == null) text = processCommandSynonyms(command, commandName, setAutoXpSynonyms, doSetAutoXp) if (text == null) text = processCommandSynonyms(command, commandName, showAutoXpSynonyms, doShowAutoXp) if (text == null) text = processCommandSynonyms(command, commandName, setDefaultDifficultySynonyms, doSetDefaultDifficulty) if (text == null) text = processCommandSynonyms(command, commandName, showDefaultDifficultySynonyms, doShowDefaultDifficulty) if (text == null) text = processCommandSynonyms(command, commandName, generateNameSynonyms, doGenerateName) if (text == null) text = processCommandSynonyms(command, commandName, createLocationSynonyms, doCreateLocation) if (text == null) text = processCommandSynonyms(command, commandName, goToLocationSynonyms, doGoToLocation) if (text == null) text = processCommandSynonyms(command, commandName, clearLocationsSynonyms, doClearLocations) if (text == null) text = processCommandSynonyms(command, commandName, removeLocationSynonyms, doRemoveLocation) if (text == null) text = processCommandSynonyms(command, commandName, showLocationsSynonyms, doShowLocations) if (text == null) text = processCommandSynonyms(command, commandName, getLocationSynonyms, doGetLocation) if (text == null) text = processCommandSynonyms(command, commandName, mapSynonyms, doMap) if (text == null) text = processCommandSynonyms(command, commandName, goNorthSynonyms, doGoNorth) if (text == null) text = processCommandSynonyms(command, commandName, goSouthSynonyms, doGoSouth) if (text == null) text = processCommandSynonyms(command, commandName, goEastSynonyms, doGoEast) if (text == null) text = processCommandSynonyms(command, commandName, goWestSynonyms, doGoWest) if (text == null) text = processCommandSynonyms(command, commandName, renameCharacterSynonyms, doRenameCharacter) if (text == null) text = processCommandSynonyms(command, commandName, cloneCharacterSynonyms, doCloneCharacter) if (text == null) text = processCommandSynonyms(command, commandName, showDaySynonyms, doShowDay) if (text == null) text = processCommandSynonyms(command, commandName, setDaySynonyms, doSetDay) if (text == null) text = processCommandSynonyms(command, commandName, versionSynonyms, doVersion) if (text == null) text = processCommandSynonyms(command, commandName, setAcSynonyms, doSetAc) if (text == null) text = processCommandSynonyms(command, commandName, encounterSynonyms, doEncounter) if (text == null) text = processCommandSynonyms(command, commandName, showEnemiesSynonyms, doShowEnemies) if (text == null) text = processCommandSynonyms(command, commandName, removeEnemySynonyms, doRemoveEnemy) if (text == null) text = processCommandSynonyms(command, commandName, clearEnemiesSynonyms, doClearEnemies) if (text == null) text = processCommandSynonyms(command, commandName, addEnemySynonyms, doAddEnemy) if (text == null) text = processCommandSynonyms(command, commandName, initiativeSynonyms, doInitiative) if (text == null) text = processCommandSynonyms(command, commandName, fleeSynonyms, doFlee) if (text == null) text = processCommandSynonyms(command, commandName, turnSynonyms, doTurn) if (text == null) text = processCommandSynonyms(command, commandName, setupEnemySynonyms, doSetupEnemy) if (text == null) text = processCommandSynonyms(command, commandName, setDamageSynonyms, doSetDamage) if (text == null) text = processCommandSynonyms(command, commandName, setProficiencySynonyms, doSetProficiency) if (text == null) text = processCommandSynonyms(command, commandName, helpSynonyms, doHelp) if (text == null) { var character = getCharacter() var statNames = [] character.stats.forEach(x => { statNames.push(x.name.toLowerCase()) }) character.skills.forEach(x => { statNames.push(x.name.toLowerCase()) }) text = processCommandSynonyms(command, commandName, statNames, doFlipCommandAbility) } if (state.flavorText != null) text += state.flavorText return { text } } function handleCreateStep(text) { state.show = "create" if (/^\s*>.*says? ".*/.test(text)) { text = text.replace(/^\s*>.*says? "/, "") text = text.replace(/"\s*$/, "") } else if (/^\s*>\s.*/.test(text)) { text = text.replace(/\s*> /, "") for (var i = 0; i < info.characters.length; i++) { var matchString = info.characters[i] == "" ? "You " : `${info.characters[i]} ` if (text.startsWith(matchString)) { text = text.replace(matchString, "") break } } text = text.replace(/\.?\s*$/, "") } else { text = text.replace(/^\s+/, "") } if (text.toLowerCase() == "q") { state.createStep = null return text } switch (state.createStep) { case 0: text = text.toLowerCase(); if (text.startsWith("y")) state.createStep = 100 else if (text.startsWith("n")) state.createStep++ break case 1: if (text.length > 0) { state.tempCharacter.className = text state.createStep++ state.statDice = [] for (var i = 0; i < 6; i++) { var dice = [] for (var j = 0; j < 4; j++) { dice.push(parseInt(calculateRoll("d6"))) } dice.sort(function(a, b) { return b - a; }); dice.splice(3, 1) state.statDice.push(dice[0] + dice[1] + dice[2]) } state.statDice.sort(function(a, b) { return b - a }) } return text break case 2: if (text.length > 0) { var choices = text.split(/\D+/) choices = [...new Set(choices)]; if (choices.length != 6) break for (var i = 0; i < 6; i++) { const stat = { name: "temp", value: state.statDice[i] } switch (parseInt(choices[i])) { case 1: stat.name = "Strength" break case 2: stat.name = "Dexterity" break case 3: stat.name = "Constitution" break case 4: stat.name = "Intelligence" break case 5: stat.name = "Wisdom" break case 6: stat.name = "Charisma" break default: return text } state.tempCharacter.stats.push(stat) } state.createStep++ } return text case 3: if (text.length == 0) state.createStep++ if (!isNaN(text)) { switch (parseInt(text)) { case 1: state.tempCharacter.spellStat = "Intelligence" break case 2: state.tempCharacter.spellStat = "Wisdom" break case 3: state.tempCharacter.spellStat = "Charisma" break case 4: state.tempCharacter.spellStat = null } state.createStep++ } return text case 4: if (text.length > 0) { state.tempCharacter.summary = text state.createStep = 500 } return text case 100: if (!isNaN(text)) { state.createStep = 500 switch (parseInt(text)) { case 1: state.tempCharacter.className = "Fighter" state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 9}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 13}, {name: "Charisma", value: 14}] state.tempCharacter.inventory.push({name: "Greatsword", quantity: 1}, {name: "Javelin", quantity: 2}) state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2; state.tempCharacter.summary = "A skilled melee warrior specializing in weapons and armor." break case 2: state.tempCharacter.className = "Cleric" state.tempCharacter.stats = [{name: "Strength", value: 14}, {name: "Dexterity", value: 12}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 18}, {name: "Charisma", value: 14}] state.tempCharacter.inventory.push({name: "Mace", quantity: 1}, {name: "Light Crossbow", quantity: 1}, {name: "Bolts", quantity: 10}) state.tempCharacter.spells = ["Spiritual Weapon", "Mass Healing Word"] state.tempCharacter.spellStat = "Wisdom" state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Medicine").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2; state.tempCharacter.summary = "A follower of a deity that can call on divine power." break case 3: state.tempCharacter.className = "Rogue" state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 16}, {name: "Constitution", value: 12}, {name: "Intelligence", value: 13}, {name: "Wisdom", value: 10}, {name: "Charisma", value: 16}] state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Dagger", quantity: 1}, {name: "Hand Crossbow", quantity: 1}, {name: "Bolts", quantity: 10}) state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Investigation").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2; state.tempCharacter.summary = "An expert in stealth, subterfuge, and exploitation." break case 4: state.tempCharacter.className = "Ranger" state.tempCharacter.stats = [{name: "Strength", value: 12}, {name: "Dexterity", value: 17}, {name: "Constitution", value: 13}, {name: "Intelligence", value: 10}, {name: "Wisdom", value: 15}, {name: "Charisma", value: 8}] state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Longbow", quantity: 1}, {name: "Arrows", quantity: 20}) state.tempCharacter.skills.find((element) => element.name == "Animal Handling").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Nature").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Survival").modifier = 2; state.tempCharacter.summary = "A talented hunter adept in tracking, survival, and animal handling." break case 5: state.tempCharacter.className = "Barbarian" state.tempCharacter.stats = [{name: "Strength", value: 17}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 8}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 10}] state.tempCharacter.inventory.push({name: "Greataxe", quantity: 1}, {name: "Javelin", quantity: 1}) state.tempCharacter.skills.find((element) => element.name == "Animal Handling").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Intimidation").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.summary = "Combat expert focused on brute strength and raw fury." break case 6: state.tempCharacter.className = "Bard" state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 15}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 13}, {name: "Wisdom", value: 10}, {name: "Charisma", value: 15}] state.tempCharacter.inventory.push({name: "Rapier", quantity: 1}, {name: "Lute", quantity: 1}) state.tempCharacter.spells = ["Vicious Mockery", "Charm Person", "Healing Word"] state.tempCharacter.spellStat = "Charisma" state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2; state.tempCharacter.summary = "A musician that can transform song and word into magic." break case 7: state.tempCharacter.className = "Druid" state.tempCharacter.stats = [{name: "Strength", value: 11}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 16}, {name: "Intelligence", value: 14}, {name: "Wisdom", value: 16}, {name: "Charisma", value: 9}] state.tempCharacter.spells = ["Druidcraft", "Animal Friendship", "Healing Word"] state.tempCharacter.spellStat = "Wisdom" state.tempCharacter.inventory.push({name: "Quarterstaff", quantity: 1}, {name: "Small Knife", quantity: 1}) state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Medicine").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Nature").modifier = 2; state.tempCharacter.summary = "Commands the natural world to cast spells and harness its power." break case 8: state.tempCharacter.className = "Monk" state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 14}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 8}, {name: "Wisdom", value: 17}, {name: "Charisma", value: 10}] state.tempCharacter.inventory.push({name: "Dart", quantity: 5}, {name: "Shortsword", quantity: 1}) state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2; state.tempCharacter.summary = "A martial artist who has mastered melee and unarmed combat." break case 9: state.tempCharacter.className = "Paladin" state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 9}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 13}, {name: "Charisma", value: 14}] state.tempCharacter.spells = ["Thunderous Smite", "Divine Favor", "Cure Wounds"] state.tempCharacter.spellStat = "Charisma" state.tempCharacter.inventory.push({name: "Longsword", quantity: 1}, {name: "Javelin", quantity: 2}) state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2; state.tempCharacter.summary = "A virtuous holy warrior with expertise in armor and mysticism." break case 10: state.tempCharacter.className = "Wizard" state.tempCharacter.stats = [{name: "Strength", value: 10}, {name: "Dexterity", value: 15}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 16}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 8}] state.tempCharacter.inventory.push({name: "Quarterstaff", quantity: 1}, {name: "Spellbook", quantity: 1}) state.tempCharacter.spells = ["Fire Bolt", "Mage Hand", "Magic Missile"] state.tempCharacter.spellStat = "Intelligence" state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Investigation").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2; state.tempCharacter.summary = "An expert in magic ability who found their power through arcane knowledge." break case 11: state.tempCharacter.className = "Sorcerer" state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 16}, {name: "Constitution", value: 13}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 15}] state.tempCharacter.inventory.push({name: "Dagger", quantity: 1}, {name: "Bag of Holding", quantity: 1}) state.tempCharacter.spells = ["Ray of Frost", "Minor Illusion", "Shield"] state.tempCharacter.spellStat = "Charisma" state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Intimidation").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2; state.tempCharacter.summary = "A masterful spellcaster deriving their power from an innate source." break case 12: state.tempCharacter.className = "Warlock" state.tempCharacter.stats = [{name: "Strength", value: 9}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 14}, {name: "Wisdom", value: 11}, {name: "Charisma", value: 16}] state.tempCharacter.spells = ["Eldritch Blast", "Witch Bolt", "Thunderwave"] state.tempCharacter.spellStat = "Charisma" state.tempCharacter.inventory.push({name: "Dagger", quantity: 1}, {name: "Orb", quantity: 1}) state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2; state.tempCharacter.summary = "A magic user granted ability by a pact with a powerful patron." break case 13: state.tempCharacter.className = "Artificer" state.tempCharacter.stats = [{name: "Strength", value: 10}, {name: "Dexterity", value: 14}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 17}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 8}] state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Hand Crossbow", quantity: 1}, {name: "Bolts", quantity: 20}) state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2; state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2; state.tempCharacter.summary = "An inventor and alchemist capable of imbuing objects with magic." break } } return text case 500: state.show = null state.createStep = null var character = getCharacter(state.tempCharacter.name) character.className = state.tempCharacter.className character.experience = 0 character.stats = [...state.tempCharacter.stats] character.inventory = [...state.tempCharacter.inventory] character.skills = [...state.tempCharacter.skills] character.spells = [...state.tempCharacter.spells] character.health = getHealthMax() character.spellStat = state.tempCharacter.spellStat character.meleeStat = state.tempCharacter.meleeStat character.rangedStat = state.tempCharacter.rangedStat character.summary = state.tempCharacter.summary break } return text } function handleSetupEnemyStep(text) { state.show = "setupEnemy" if (/^\s*>.*says? ".*/.test(text)) { text = text.replace(/^\s*>.*says? "/, "") text = text.replace(/"\s*$/, "") } else if (/^\s*>\s.*/.test(text)) { text = text.replace(/\s*> /, "") for (var i = 0; i < info.characters.length; i++) { var matchString = info.characters[i] == "" ? "You " : `${info.characters[i]} ` if (text.startsWith(matchString)) { text = text.replace(matchString, "") break } } text = text.replace(/\.?\s*$/, "") } else { text = text.replace(/^\s+/, "") } if (text.toLowerCase() == "q") { state.setupEnemyStep = null return text } switch (state.setupEnemyStep) { case 0: text = text.toLowerCase(); if (text.startsWith("y")) state.setupEnemyStep = 100 else if (text.startsWith("n")) state.setupEnemyStep++ break case 1: if (text.length > 0) { state.tempEnemy.name = text state.setupEnemyStep++ } return text case 2: if (text.length > 0) { if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { state.tempEnemy.health = calculateRoll(text) state.setupEnemyStep++ } else if (!isNaN(text)) { state.tempEnemy.health = Math.max(0, parseInt(text)) state.setupEnemyStep++ } } return text case 3: if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { state.tempEnemy.ac = calculateRoll(text) state.setupEnemyStep++ } else if (!isNaN(text)) { state.tempEnemy.ac = Math.max(0, parseInt(text)) state.setupEnemyStep++ } return text case 4: if (!isNaN(text)) { state.tempEnemy.hitModifier = Math.max(0, parseInt(text)) state.setupEnemyStep++ } return text case 5: if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { state.tempEnemy.damage = text state.setupEnemyStep++ } else if (!isNaN(text)) { state.tempEnemy.damage = Math.max(0, parseInt(text)) state.setupEnemyStep++ } return text case 6: if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { state.tempEnemy.initiative = calculateRoll(text) state.setupEnemyStep++ } else if (!isNaN(text)) { state.tempEnemy.initiative = Math.max(0, parseInt(text)) state.setupEnemyStep++ } return text case 7: if (text.toLowerCase() == "s") { state.setupEnemyStep = 500 } else if (text.length > 0) { state.tempEnemy.spells.push(text) state.setupEnemyStep++ } return text case 8: if (text.toLowerCase() == "s") { state.setupEnemyStep = 500 } else if (text.length > 0) { state.tempEnemy.spells.push(text) } return text case 100: if (/^\d+(\s.*)?$/gi.test(text)) { state.setupEnemyStep = 500 var value = text.match(/^\d+/gi)[0] var nameMatches = text.match(/(?<=\s).*/gi) switch (parseInt(value)) { case 1: state.tempEnemy = createEnemy("Animated Armor", calculateRoll("6d8+6"), 18, 6, "1d6+2", "d20") break case 2: state.tempEnemy = createEnemy("Awakened Shrub", calculateRoll("2d+6"), 9, -3, "1d4-1", "d20-1") break case 3: state.tempEnemy = createEnemy("Brigand", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20") break case 4: state.tempEnemy = createEnemy("Black Bear", calculateRoll("3d8+6"), 11, 6, "2d4+2", "d20") break case 5: state.tempEnemy = createEnemy("Boar", calculateRoll("2d8+2"), 11, 4, "1d6+1", "d20") break case 6: state.tempEnemy = createEnemy("Cockatrice", calculateRoll("6d6+6"), 11, 1, "1d4+1", "d20+1", "Petrifying Bite1d4+1") break case 7: state.tempEnemy = createEnemy("Snake", calculateRoll("2d10+2"), 12, 6, "1d8+2", "d20+2", "Poison Bite2d4+1") break case 8: state.tempEnemy = createEnemy("Dire Wolf", calculateRoll("5d10+10"), 14, 8, "2d6+3", "d20+2") break case 9: state.tempEnemy = createEnemy("Ghoul", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2") break case 10: state.tempEnemy = createEnemy("Giant Centipede", calculateRoll("1d6+1"), 13, 1, "1d4+2", "d20+2") break case 11: state.tempEnemy = createEnemy("Giant Rat", calculateRoll("2d6"), 12, 2, "1d4+2", "d20+2") break case 12: state.tempEnemy = createEnemy("Giant Wolf Spider", calculateRoll("2d8+2"), 13, 4, "1d6+1", "d20+3", "Poison Bite1d6+8") break case 13: state.tempEnemy = createEnemy("Gnoll", calculateRoll("5d8"), 15, 6, "1d8+2", "d20+1") break case 14: state.tempEnemy = createEnemy("Goblin", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2") break case 15: state.tempEnemy = createEnemy("Harpy", calculateRoll("7d8+7"), 11, 4, "2d4+1", "d20+1", "Luring Song") break case 16: state.tempEnemy = createEnemy("Hobgoblin", calculateRoll("2d8+2"), 18, 4, "1d8+1", "d20+1") break case 17: state.tempEnemy = createEnemy("Kobold", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2") break case 18: state.tempEnemy = createEnemy("Orc", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1") break case 19: state.tempEnemy = createEnemy("Satyr", calculateRoll("5d8"), 15, 4, "1d8+2", "d20+3") break case 20: state.tempEnemy = createEnemy("Skeleton", calculateRoll("2d8+4"), 13, 5, "1d6+2", "d20+2") break case 21: state.tempEnemy = createEnemy("Stirge", calculateRoll("1d4"), 14, 2, "1d4+3", "d20+1", "Blood Drain2d4+6") break case 22: state.tempEnemy = createEnemy("Warhorse", calculateRoll("3d10+3"), 11, 10, "2d6+4", "d20+1", "Charge") break case 23: state.tempEnemy = createEnemy("Wolf", calculateRoll("2d+2"), 13, 6, "2d4+2", "d20+2") break case 24: state.tempEnemy = createEnemy("Worg", calculateRoll("4d10+4"), 13, 8, "2d6+3", "d20+1") break case 25: state.tempEnemy = createEnemy("Zombie", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2") break case 26: state.tempEnemy = createEnemy("Air Elemental", calculateRoll("12d10+24"), 15, 8, "2d8+5", "d20+5", "Whirlwind3d8+2") break case 27: state.tempEnemy = createEnemy("Basilisk", calculateRoll("8d8+16"), 15, 5, "2d6+3", "d20-1") break case 28: state.tempEnemy = createEnemy("Berserker", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1") break case 29: state.tempEnemy = createEnemy("Chuul", calculateRoll("11d10+33"), 16, 6, "2d6+4", "d20", "Tentacles") break case 30: state.tempEnemy = createEnemy("Doppelganger", calculateRoll("8d8+16"), 14, 6, "1d6+4", "d20+4", "Shapechange") break case 31: state.tempEnemy = createEnemy("Druid", calculateRoll("5d8+5"), 11, 4, "1d8", "d20+1", "Produce Flame3d6", "Bark Skin", "Entangle") break case 32: state.tempEnemy = createEnemy("Earth Elemental", calculateRoll("12d10+60"), 17, 8, "2d8+5", "d20-1", "Earth Glide") break case 33: state.tempEnemy = createEnemy("Fire Elemental", calculateRoll("12d10+36"), 13, 6, "2d6+3", "d20+3", "Fire Form") break case 34: state.tempEnemy = createEnemy("Gorgon", calculateRoll("12d8+48"), 19, 8, "2d12+5", "d20", "Petrifying Breath") break case 35: state.tempEnemy = createEnemy("Green Hag", calculateRoll("11d8+33"), 17, 6, "2d8+4", "d20+1", "Minor Illusion", "Invisible Passage") break case 36: state.tempEnemy = createEnemy("Griffon", calculateRoll("7d10+21"), 12, 6, "1d8+4", "d20+2") break case 37: state.tempEnemy = createEnemy("Hell Hound", calculateRoll("7d8+14"), 15, 5, "1d8+3", "d20+1", "Fire Breath6d6") break case 38: state.tempEnemy = createEnemy("Hill Giant", calculateRoll("10d12+40"), 13, 8, "3d8+5", "d20-1", "Throw Rock3d10+5") break case 39: state.tempEnemy = createEnemy("Manticore", calculateRoll("8d10+24"), 14, 5, "1d8+3", "d20+3") break case 40: state.tempEnemy = createEnemy("Minotaur", calculateRoll("9d10+27"), 14, 6, "2d12+4", "d20") break case 41: state.tempEnemy = createEnemy("Mimic", calculateRoll("9d8+18"), 12, 5, "1d8+3", "d20+1", "Grapple") break case 42: state.tempEnemy = createEnemy("Ogre", calculateRoll("7d10+21"), 11, 6, "2d8+4", "d20-1") break case 43: state.tempEnemy = createEnemy("Owlbear", calculateRoll("7d10+21"), 13, 7, "1d10+5", "initiative") break case 44: state.tempEnemy = createEnemy("Red Dragon Wyrmling", calculateRoll("10d8+30"), 17, 6, "1d10+4", "d20") break case 45: state.tempEnemy = createEnemy("Spectator", calculateRoll("6d8+12"), 14, 1, "1d6-1", "d20+2", "Confusion Ray", "Paralyzing Ray", "Fear Ray", "Wounding Ray3d10") break case 46: state.tempEnemy = createEnemy("Troll", calculateRoll("8d10+40"), 15, 7, "1d6+4", "d20+1") break case 47: state.tempEnemy = createEnemy("Wererat", calculateRoll("6d8+6"), 12, 4, "1d4+2", "d20+2") break case 48: state.tempEnemy = createEnemy("Werewolf", calculateRoll("9d8+18"), 12, 4, "18+2", "d20+1") break case 49: state.tempEnemy = createEnemy("Vampire Spawn", calculateRoll("11d8+33"), 15, 6, "2d4+3", "d20+3", "Bite3d6+3") break case 50: state.tempEnemy = createEnemy("Wight", calculateRoll("6d8+18"), 14, 4, "1d8+2", "d20+1", "Life Drain4d6+3") break case 51: state.tempEnemy = createEnemy("Aboleth", calculateRoll("18d10"), 17, 9, "6d6+15", "d20-1", "Enslave", "Psychic Drain3d6") break case 52: state.tempEnemy = createEnemy("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3") break case 53: state.tempEnemy = createEnemy("Chimera", calculateRoll("12d10+48"), 14, 7, "2d6+4", "d20", "Fire Breath7d8") break case 54: state.tempEnemy = createEnemy("Cloud Giant", calculateRoll("16d12+96"), 14, 12, "6d8+16", "d20", "Throw Rock4d10+8", "Control Weather") break case 55: state.tempEnemy = createEnemy("Cyclops", calculateRoll("12d12+60"), 14, 9, "3d8+6", "d20") break case 56: state.tempEnemy = createEnemy("Deva", calculateRoll("16d8+64"), 17, 8, "2d6+8", "d20+4") break case 57: state.tempEnemy = createEnemy("Drider", calculateRoll("13d10+52"), 19, 6, "3d8", "1d10+3", "Poison Bite2d8") break case 58: state.tempEnemy = createEnemy("Frost Giant", calculateRoll("12d12+60"), 15, 9, "6d12+12", "d20-1") break case 59: state.tempEnemy = createEnemy("Hydra", calculateRoll("15d12+75"), 15, 8, "3d10+15", "d20+1") break case 60: state.tempEnemy = createEnemy("Insane Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Cone of Cold8d8", "Greater Invisibility", "Fireball8d6", "Shield") break case 61: state.tempEnemy = createEnemy("Medusa", calculateRoll("17d8+51"), 15, 5, "1d6+2", "d20+2", "Petrifying Gaze", "Snake Hair5d6") break case 62: state.tempEnemy = createEnemy("Shield Guardian", calculateRoll("15d10+60"), 17, 7, "4d6+4", "d20-1", "Shield") break case 63: state.tempEnemy = createEnemy("Spirit Naga", calculateRoll("10d10+20"), 15, 7, "8d8+4", "d20+3", "Dominate Person", "Lightning Bolt9d6") break case 64: state.tempEnemy = createEnemy("Stone Golem", calculateRoll("17d10+85"), 17, 10, "6d8+12", "d20-1") break case 65: state.tempEnemy = createEnemy("Treant", calculateRoll("12d12+60"), 16, 10, "6d6+12", "d20-1") break case 66: state.tempEnemy = createEnemy("Young Black Dragon", calculateRoll("15d10+45"), 18, 7, "4d6+8", "d20+2", "Acid Breath11d8") break case 67: state.tempEnemy = createEnemy("Young Blue Dragon", calculateRoll("16d10+64"), 18, 9, "12d6+10", "d20", "Lightning Breath10d10") break case 68: state.tempEnemy = createEnemy("Young Brass Dragon", calculateRoll("13d10+39"), 17, 7, "2d10+4", "d20", "Fire Breath12d6", "Sleep Breath") break case 69: state.tempEnemy = createEnemy("Young Bronze Dragon", calculateRoll("15d10+60"), 18, 8, "4d6+10", "d20+1", "Lightning Breath10d10", "Repulsion Breath") break case 70: state.tempEnemy = createEnemy("Young Copper Dragon", calculateRoll("14d10+42"), 17, 7, "4d6+8", "d20+1", "Acid Breath9d8", "Slowing Breath") break case 71: state.tempEnemy = createEnemy("Young Gold Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20+2", "Fire Breath10d10", "Weakening Breath") break case 72: state.tempEnemy = createEnemy("Young Green Dragon", calculateRoll("16d10+48"), 18, 7, "4d6+8", "d20+1", "Poison Breath12d6") break case 73: state.tempEnemy = createEnemy("Young Red Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20", "Fire Breath16d6") break case 74: state.tempEnemy = createEnemy("Young Silver Dragon", calculateRoll("16d10+8-"), 18, 10, "4d6+12", "d20", "Cold Breath12d8", "Paralyzing Breath") break case 75: state.tempEnemy = createEnemy("Young White Dragon", calculateRoll("14d10+56"), 17, 7, "2d10+4", "d20", "Cold Breath10d8", "Ice Walk") break case 76: state.tempEnemy = createEnemy("Adult Black Dragon", calculateRoll("17d12+85"), 19, 11, "6d6+18", "d20+2", "Acid Breath12d8", "Frightful Presence", "Wing Attack2d6+6") break case 77: state.tempEnemy = createEnemy("Adult Bronze Dragon", calculateRoll("17d12+102"), 19, 12, "6d6+21", "d20", "Repulsion Breath", "Lightning Breath12d10", "Wing Attack2d6+6") break case 78: state.tempEnemy = createEnemy("Adult Copper Dragon", calculateRoll("16d12+80"), 18, 11, "6d6+18", "d20+1", "Acid Breath12d8", "Slowing Breath", "Wing Attack2d6+6") break case 79: state.tempEnemy = createEnemy("Adult Green Dragon", calculateRoll("18d12+90"), 19, 11, "6d6+18", "d20+1", "Poison Breath16d6") break case 80: state.tempEnemy = createEnemy("Animated Statue", calculateRoll("10d12+20"), 17, 7, "2d10+4", "d20-2") break case 81: state.tempEnemy = createEnemy("Arch Mage", calculateRoll("18d8+18"), 12, 4, "1d4+2", "d20+14", "Time Stop", "Globe of Invulnerability", "Lightning Bolt8d6", "Banishment", "Cone of Cold8d8", "Teleport") break case 82: state.tempEnemy = createEnemy("Behir", calculateRoll("16d12+64"), 17, 10, "5d10+12", "d20+3", "Lightning Breath12d10", "Swallow6d6", "Constrict2d10+6") break case 83: state.tempEnemy = createEnemy("Boneclaw", calculateRoll("17d10+34"), 16, 8, "6d10+8", "d20+3", "Shadow Jump5d12+2", "Deadly Reach") break case 84: state.tempEnemy = createEnemy("Deathwolf", calculateRoll("18d8+72"), 15, 10, "6d8+15", "d20+3", "Phantom Deathwolf6d6") break case 85: state.tempEnemy = createEnemy("Djinni", calculateRoll("14d10+84"), 17, 9, "2d6+8", "d20+2") break case 86: state.tempEnemy = createEnemy("Drow Inquisitor", calculateRoll("23d8+46"), 16, 10, "12d8+24", "d20+2", "Spectral Dagger1d8+5") break case 87: state.tempEnemy = createEnemy("Efreeti", calculateRoll("16d10+112"), 17, 10, "4d6+12", "d20+1", "Hurl Flame5d6") break case 88: state.tempEnemy = createEnemy("Elder Brain", calculateRoll("20d10+100"), 10, 7, "5d8+7", "d20", "Mind Blast5d10+5") break case 89: state.tempEnemy = createEnemy("Erinyes", calculateRoll("18d8+72"), 18, 8, "1d10+4", "d20+3") break case 90: state.tempEnemy = createEnemy("Ice Devil", calculateRoll("19d10+76"), 18, 10, "6d4+15", "d20+2", "Wall of Ice") break case 91: state.tempEnemy = createEnemy("Jabberwock", calculateRoll("10d12+50"), 18, 10, "6d10+10", "d20+1", "Regenderation") break case 92: state.tempEnemy = createEnemy("Megapede", calculateRoll("13d20+39"), 15, 10, "6d10+12", "d20", "LifeDrain3d10", "Psychic Bomb5d8") break case 93: state.tempEnemy = createEnemy("Mummy Lord", calculateRoll("13d8+39"), 17, 9, "3d6+4", "d20", "Hold Person", "Silence", "Harm14d6", "Blinding Dust", "Whirlwind of Sand") break case 94: state.tempEnemy = createEnemy("Purple Worm", calculateRoll("15d20+90"), 18, 14, "6d6+18", "d20-2", "Tail Stinger12d6+19") break case 95: state.tempEnemy = createEnemy("Remorhaz", calculateRoll("17d12+85"), 17, 11, "6d10+7", "d20+1", "Swallow6d6") break case 96: state.tempEnemy = createEnemy("Skull Lord", calculateRoll("15d8+45"), 18, 8, "24d6", "d20+3", "Deathly Ray5d8+5") break case 97: state.tempEnemy = createEnemy("Spider Dragon", calculateRoll("15d10+5"), 23, 9, "3d12+12", "d20+8", "Silk Spit", "Spider Breath7d10") break case 98: state.tempEnemy = createEnemy("Storm Giant", calculateRoll("20d12+100"), 16, 14, "12d6+18", "d20+2", "Control Weather", "Lightning Strike12d8") break case 99: state.tempEnemy = createEnemy("Vampire", calculateRoll("17d8+68"), 16, 9, "3d8+8", "d20+4", "Charm", "Shape Change") break case 100: state.tempEnemy = createEnemy("Zikran", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Cone of Cold 8d8", "Lightning Bolt 8d6") break case 126: state.tempEnemy = createEnemy("Commoner", calculateRoll("1d8"), 10, 2, "1d4", "d20") break case 127: state.tempEnemy = createEnemy("Bandit", calculateRoll("2d8+2"), 12, 3, "1d6+1", "d20+1") break case 128: state.tempEnemy = createEnemy("Guard", calculateRoll("2d8+2"), 16, 3, "1d6+1", "d20+1") break case 129: state.tempEnemy = createEnemy("Cultist", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1", "Dark Devotion") break case 130: state.tempEnemy = createEnemy("Acolyte", calculateRoll("2d8"), 10, 2, "1d4", "d20", "Sacred Flame1d8", "Cure Wounds") break case 131: state.tempEnemy = createEnemy("Apprentice", calculateRoll("3d8"), 10, 4, "1d10+2", "d20", "Burning Hands3d6") break case 132: state.tempEnemy = createEnemy("Witch", calculateRoll("3d8+3"), 10, 3, "1d6+2", "d20", "Ray of Sickness2d8", "Tashas Hideous Laughter", "Invisibility", "Ray of Frost2d8")// break case 133: state.tempEnemy = createEnemy("Buccaneer", calculateRoll("8d8+24"), 14, 5, "1d6+3", "d20+2", "Invade") break case 134: state.tempEnemy = createEnemy("Spy", calculateRoll("6d8"), 12, 4, "1d6+2", "d20+2", "Sneak Attack2d6+2") break case 135: state.tempEnemy = createEnemy("Captain", calculateRoll("10d8+20"), 15, 5, "3d6+9", "initiative") break case 136: state.tempEnemy = createEnemy("Bard", calculateRoll("8d8+8"), 15, 4, "1d6+2", "d20+2", "Charm Person", "Shatter3d8", "Thunderwave2d8", "Vicious Mockery1d4") break case 137: state.tempEnemy = createEnemy("Berserker", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1") break case 138: state.tempEnemy = createEnemy("Priest", calculateRoll("5d8+5"), 13, 2, "1d6", "d20", "Spirit Guardians3d8", "Spiritual Weapon1d8", "Guiding Bolt4d6", "Cure Wounds") break case 139: state.tempEnemy = createEnemy("Knight", calculateRoll("8d8+16"), 18, 5, "4d6+6", "d20", "Leadership") break case 140: state.tempEnemy = createEnemy("Archer", calculateRoll("10d8+30"), 16, 6, "2d8+8", "d20+4") break case 141: state.tempEnemy = createEnemy("Warrior", calculateRoll("6d8+12"), 16, 6, "1d8+3", "d20+1") break case 142: state.tempEnemy = createEnemy("Conjurer", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Conjure Elemental", "Cloud Kill5d8", "Cloud of Daggers5d8", "Poison Spray1d12") break case 143: state.tempEnemy = createEnemy("Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Greater Invisibility", "Ice Storm4d6", "Fireball8d6", "Magic Missile3d4+3") break case 144: state.tempEnemy = createEnemy("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3", "Sneak Attack6d6+6") break case 145: state.tempEnemy = createEnemy("Evoker", calculateRoll("12d8+12"), 12, 3, "1d6-1", "d20+2", "Chain Lightning10d8", "Wall of Ice", "Counter Spell", "Shatter3d8", "Magic Missile6d4+6") break case 146: state.tempEnemy = createEnemy("Necromancer", calculateRoll("12d8+12"), 12, 7, "2d4", "d20+2", "Circle of Death8d6", "Blight8d8", "Cloudkill5d8", "Animate Dead", "Chill Touch1d8") break case 147: state.tempEnemy = createEnemy("Champion", calculateRoll("22d8+44"), 18, 9, "6d6+15", "d20+2", "Second Wind") break case 148: state.tempEnemy = createEnemy("Warlord", calculateRoll("27d8+108"), 18, 9, "4d6+10", "d20+3", "Command Ally", "Frighten Foe") break case 149: state.tempEnemy = createEnemy("Archmage", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Lightning Bolt8d6", "Cone of Cold8d8", "Shocking Grasp1d8") break case 150: state.tempEnemy = createEnemy("Archdruid", calculateRoll("24d8+24"), 16, 6, "1d6+2", "d20+2", "Fire Storm7d10", "Sunbeam6d8", "Wall of Fire", "Beast Sense", "Conjure Animals") break } if (nameMatches != null) state.tempEnemy.name = nameMatches[0] } return text case 500: state.show = null state.setupEnemyStep = null var enemy = createEnemy(state.tempEnemy.name, state.tempEnemy.health, state.tempEnemy.ac, state.tempEnemy.hitModifier, state.tempEnemy.damage, state.tempEnemy.initiative) enemy.spells = [...state.tempEnemy.spells] var enemyMatches = state.enemies.filter(x => x.name.toLowerCase() == enemy.name.toLowerCase() || x.name.toLowerCase() == `${enemy.name.toLowerCase()} a`) if (enemyMatches.length > 0) { enemy.name = getUniqueName(enemy.name) if (enemy.name.endsWith("A")) { enemyMatches[0].name = enemy.name enemy.name = enemy.name.substring(0, enemy.name.length - 1) + "B" } } state.enemies.push(enemy) break } return text } function resetTempCharacterSkills() { state.tempCharacter.skills = [ {name: "Acrobatics", stat: "Dexterity", modifier: 0}, {name: "Animal Handling", stat: "Wisdom", modifier: 0}, {name: "Arcana", stat: "Intelligence", modifier: 0}, {name: "Athletics", stat: "Strength", modifier: 0}, {name: "Deception", stat: "Charisma", modifier: 0}, {name: "History", stat: "Intelligence", modifier: 0}, {name: "Insight", stat: "Wisdom", modifier: 0}, {name: "Intimidation", stat: "Charisma", modifier: 0}, {name: "Investigation", stat: "Intelligence", modifier: 0}, {name: "Medicine", stat: "Wisdom", modifier: 0}, {name: "Nature", stat: "Intelligence", modifier: 0}, {name: "Perception", stat: "Wisdom", modifier: 0}, {name: "Performance", stat: "Charisma", modifier: 0}, {name: "Persuasion", stat: "Charisma", modifier: 0}, {name: "Religion", stat: "Intelligence", modifier: 0}, {name: "Sleight of Hand", stat: "Dexterity", modifier: 0}, {name: "Stealth", stat: "Dexterity", modifier: 0}, {name: "Survival", stat: "Wisdom", modifier: 0}, ] } function processCommandSynonyms(command, commandName, synonyms, func) { text = null synonyms.forEach(x => { if (commandName == x || commandName == x + "s") { text = func(command) } }) return text } function init() { if (state.tempCharacter == null) { state.tempCharacter = { name: "template", className: "adventurer", summary: "Template character not meant to be used.", inventory: [], spells: [], stats: [], spellStat: null, meleeStat: null, rangedStat: null, experience: 0, health: 10, ac: 10, damage: "1d6", proficiency: 2 } } if (state.tempEnemy == null) state.tempEnemy = createEnemy("enemy", 10, 10, "2d6", 10) if (state.characters == null) state.characters = [] if (state.notes == null) state.notes = [] if (state.locations == null) state.locations = [] if (state.x == null) state.x = 0 if (state.y == null) state.y = 0 if (state.autoXp == null) state.autoXp = 0 if (state.defaultDifficulty == null) state.defaultDifficulty = 10 if (state.day == null) state.day = 0 if (state.enemies == null) state.enemies = [] if (state.initiativeOrder == null) state.initiativeOrder = [] state.show = null state.prefix = null state.critical = null state.characters.forEach(x => { if (x.ac == null) x.ac = 10 if (x.damage == null) x.damage = "1d6" if (x.proficiency == null) x.proficiency = 2 }) } function doRoll(command) { var rollType = searchArgument(command, /^(advantage)|(disadvantage)$/gi) if (rollType == null) rollType = "normal" var dice = searchArgument(command, /^.*\d.*$/gi) if (dice == null) dice = "d20" dice = formatRoll(dice) var addition = getAddition(dice) var roll = calculateRoll(dice) - addition if (rollType == "advantage") roll = Math.max(roll, calculateRoll(dice) - addition) if (rollType == "disadvantage") roll = Math.min(roll, calculateRoll(dice) - addition) state.show = "none" var text = `\n[You roll a ${dice}` if (rollType != "normal") text += ` with ${rollType}` text += `. Score: ${roll}` if (roll == 20) text += " Critical Success!" else if (roll == 1) text += " Critical Failure!" else if (addition > 0) text += ` + ${addition} = ${roll + addition}` else if (addition < 0) text += ` - ${Math.abs(addition)} = ${roll + addition}` text += "]\n" return text } function doCreate(command) { if (!hasCharacter(state.characterName)) createCharacter(state.characterName) var character = getCharacter() state.createStep = 0 state.tempCharacter.name = character.name resetTempCharacterSkills() state.tempCharacter.stats = [] state.tempCharacter.spells = [] state.tempCharacter.inventory = [{name: "Gold", quantity: 50}, {name: "Rope", quantity: 1}, {name: "Ration", quantity: 10}, {name: "Torch", quantity: 1}] state.tempCharacter.spellStat = null state.tempCharacter.meleeStat = "Strength" state.tempCharacter.rangedStat = "Dexterity" state.tempCharacter.ac = 10 state.tempCharacter.damage = "1d6" state.tempCharacter.proficiency = 2 state.show = "create" return " " } function doSetupEnemy(command) { state.setupEnemyStep = 0 state.tempEnemy = createEnemy("enemy", 20, 10, 0, "2d6", 10) state.show = "setupEnemy" return " " } function doBio(command) { state.show = "bio" return " " } function doRenameCharacter(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var possessiveName = getPossessiveName(character.name) state.show = "none" var text = `\n[${possessiveName} name has been changed to ${arg0}]\n` character.name = arg0 return text } function doCloneCharacter(command) { var character = getCharacter() var possessiveName = getPossessiveName(character.name) var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (!hasCharacter(arg0)) createCharacter(arg0) var newCharacter = getCharacter(arg0) copyCharacter(character, newCharacter) state.show = "none" var text = `\n[${character.name} has been cloned to a new character called ${newCharacter.name}]\n` return text } function doSetStat(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = clamp(parseInt(getArgument(command, 1)), 1, 100) var possessiveName = getPossessiveName(character.name) const stat = { name: arg0, value: arg1 } var index = character.stats.findIndex((element) => element.name.toLowerCase() == stat.name.toLowerCase()) if (index == -1) { character.stats.push(stat) } else { var existingStat = character.stats[index] existingStat.value = parseInt(stat.value) } state.show = "none" return `\n[${possessiveName} ${toTitleCase(arg0)} ability is now ${arg1}]\n` } function doSetSpellStat(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } character.spellStat = arg0 state.show = "none" return `\nSpellcasting Ability is set to ${arg0}\n` } function doSetMeleeStat(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } character.meleeStat = arg0 state.show = "none" return `\nMelee Ability is set to ${arg0}\n` } function doSetRangedStat(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } character.rangedStat = arg0 state.show = "none" return `\nRanged Ability is set to ${arg0}\n` } function doSetAutoXp(command) { var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (isNaN(arg0)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } state.autoXp = Math.max(0, arg0) state.show = "none" return state.autoXp <= 0 ? `\n[Auto XP is disabled]\n` : `\n[Auto XP is set to ${state.autoXp}]\n` } function doShowAutoXp(command) { state.show = "none" return state.autoXp <= 0 ? `\n[Auto XP is disabled]\n` : `\n[Auto XP is set to ${state.autoXp}]\n` } function doSetDefaultDifficulty(command) { const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var difficulty = getArgument(command, 0) if (difficulty == null) difficulty = "easy" var difficultyIndex = difficultyNames.indexOf(difficulty) if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) { difficulty = difficultyScores[difficultyIndex] } state.defaultDifficulty = Math.max(0, difficulty) state.show = "none" return `\n[The default difficulty is set to ${state.defaultDifficulty}]\n` } function doShowDefaultDifficulty(command) { state.show = "none" return `\n[The default difficulty is set to ${state.defaultDifficulty}]\n` } function doSetSkill(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = getArgument(command, 1) if (arg1 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg2 = getArgument(command, 2) if (arg2 == null) { arg2 = (clamp(parseInt(arg1, 1, 100))) arg1 = null } else { arg2 = clamp(parseInt(arg2), 1, 100) } var possessiveName = getPossessiveName(character.name) const skill = { name: arg0, stat: arg1, modifier: arg2 } var index = character.skills.findIndex((element) => element.name.toLowerCase() == skill.name.toLowerCase()) if (index == -1) { if (arg1 == null) { state.show = "none" return "\n[Error: New skills must have an ability specified. See #help]\n" } character.skills.push(skill) } else { var existingSkill = character.skills[index] existingSkill.modifier = parseInt(skill.modifier) if (arg1 != null) existingSkill.stat = skill.stat } state.show = "none" return `\n[${possessiveName} ${toTitleCase(arg0)} skill is now ${arg2 >= 0 ? "+" + arg2 : "-" + arg2} and based on ${toTitleCase(arg1)}]\n` } function doSetAc(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (isNaN(arg0)) { state.show = "none" return "\n[Error: Not a number. See #help]\n" } var possessiveName = getPossessiveName(character.name) character.ac = parseInt(arg0) state.show = "none" return `\n[${possessiveName} armor class is set to ${character.ac}]\n` } function doSetExperience(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (isNaN(arg0)) { state.show = "none" return "\n[Error: Not a number. See #help]\n" } var possessiveName = getPossessiveName(character.name) character.experience = parseInt(arg0) state.show = "none" return `\n[${possessiveName} experience is set to ${character.experience}]\n` } function doAddExperience(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } arg0 = searchArgument(command, /\d+/gi) if (arg0 == null) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } arg0 = parseInt(arg0) var arg1 = searchArgument(command, /party/gi) if (arg1 == null && character == null) { state.show = "none" return `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n` } if (state.characters.length == 0) { state.show = "none" return `\n[Error: There are no characters. Type #setup to create a character]\n` } state.prefix = "\n" characters = arg1 == null ? [character] : state.characters for (var c of characters) { var possessiveName = getPossessiveName(c.name) var level = getLevel(c.experience) c.experience += arg0 var newLevel = getLevel(c.experience) if (newLevel > level) state.prefix += `[${possessiveName} experience is increased to ${c.experience}. LEVEL UP! Level: ${newLevel}, Health Max: ${getHealthMax(c)}. Next level at ${getNextLevelXp(c.experience)}]\n` else state.prefix += `[${possessiveName} experience is increased to ${c.experience}. Next level at ${getNextLevelXp(c.experience)}]\n` } state.show = "prefixOnly" return " " } function doLevelUp(command) { var character = getCharacter() var level = getLevel(character.experience) var experience = level >= levelSplits.length ? 0 : levelSplits[level] - character.experience return doAddExperience(`${command} ${experience}`) } function doSetClass(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var possessiveName = getPossessiveName(character.name) character.className = arg0 state.show = "none" return `\n[${possessiveName} class is set to "${character.className}"]\n` } function doSetSummary(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var possessiveName = getPossessiveName(character.name) character.summary = arg0 state.show = "none" return `\n[${possessiveName} summary is set]\n` } function doSetHealth(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var possessiveName = getPossessiveName(character.name) character.health = arg0 character.health = clamp(character.health, 0, getHealthMax()) state.show = "none" return `\n[${possessiveName} health is set to ${character.health} health]\n` } function doHeal(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = getArgumentRemainder(command, 1) if (arg1 == null) { if (character == null) { state.show = "none" return "\n[Error: Character must be specified. See #help]\n" } var healing var healingMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi) if (healingMatches != null) healing = calculateRoll(healingMatches[0]) else { healingMatches = arg0.match(/\d+/g) if (healingMatches != null) healing = parseInt(healingMatches[healingMatches.length - 1]) } if (healing == null) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } var haveWord = character.name == "You" ? "have" : "has" character.health += healing character.health = clamp(character.health, 0, getHealthMax()) state.show = "none" return `\n[${character.name} ${haveWord} been healed for ${healing} hp to a total of ${character.health}]\n` } else { var healing var healingMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi) if (healingMatches != null) healing = calculateRoll(healingMatches[0]) else { healingMatches = arg0.match(/\d+/g) log(healingMatches) if (healingMatches != null) healing = parseInt(healingMatches[0]) } if (healing == null) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } for (var enemy of state.enemies) { if (enemy.name.toLowerCase() == arg1.toLowerCase()) { enemy.health = Math.max(0, enemy.health + healing) return `\n[${toTitleCase(enemy.name)} has been healed for ${healing} hp to a total of ${enemy.health}]\n` } } for (var character of state.characters) { if (character.name.toLowerCase() == arg1.toLowerCase()) { character.health += healing character.health = clamp(character.health, 0, getHealthMax(character)) return `\n[${toTitleCase(character.name)} has been healed for ${healing} hp to a total of ${character.health}]\n` } } state.show = "none" return `\n[Error: Could not find an enemy or character matching the name ${arg1}. Type #enemies or #characters to see a list]` } } function doDamage(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = getArgumentRemainder(command, 1) if (arg1 == null) { if (character == null) { state.show = "none" return "\n[Error: Character must be specified. See #help]\n" } var damage var damageMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi) if (damageMatches != null) damage = calculateRoll(damageMatches[0]) else { damageMatches = arg0.match(/\d+/g) if (damageMatches != null) damage = parseInt(damageMatches[damageMatches.length - 1]) } if (damage == null) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } var haveWord = character.name == "You" ? "have" : "has" character.health -= damage character.health = clamp(character.health, 0, getHealthMax()) state.show = "none" return `\n[${character.name} ${haveWord} been damaged for ${damage} hp with ${character.health} remaining] ${character.health == 0 ? " You are unconscious" : ""}\n` } else { var damage var damageMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi) if (damageMatches != null) damage = calculateRoll(damageMatches[0]) else { damageMatches = arg0.match(/\d+/g) log(damageMatches) if (damageMatches != null) damage = parseInt(damageMatches[0]) } if (damage == null) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } for (var enemy of state.enemies) { if (enemy.name.toLowerCase() == arg1.toLowerCase()) { enemy.health = Math.max(0, enemy.health - damage) return `\n[${toTitleCase(enemy.name)} has been damaged for ${damage} hp with ${enemy.health} remaining] ${enemy.health == 0 ? " " + toTitleCase(enemy.name) + " has been defeated!" : ""}\n` } } for (var character of state.characters) { if (character.name.toLowerCase() == arg1.toLowerCase()) { character.health = Math.max(0, character.health - damage) return `\n[${toTitleCase(character.name)} has been damaged for ${damage} hp with ${character.health} remaining] ${character.health == 0 ? " " + toTitleCase(character.name) + " is unconcious!" : ""}\n` } } state.show = "none" return `\n[Error: Could not find an enemy matching the name ${arg1}. Type #enemies or #characters to see a list]` } } function doRest(command) { var commandName = getCommandName(command) state.day++ state.enemies = [] var healingFactor = 1 var text if (commandName.toLowerCase() == "shortrest") { state.day-- healingFactor = .5 text = `\n[All characters have healed 50%]\n` } else { text = `\n[All characters have rested and feel rejuvinated. It's now day ${state.day}]\n` } state.characters.forEach(function(character) { var max = getHealthMax(character) character.health += Math.floor(max * healingFactor) if (character.health > max) character.health = max }) state.show = "none" return text } function doFlipCommandAbility(command) { var ability = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) return; var remainder = getArgumentRemainder(command, 1) command = `${arg0} "${ability}"${remainder == null ? "" : " " + remainder}` text = processCommandSynonyms(command, arg0, checkSynonyms, doCheck) if (text == null) text = processCommandSynonyms(command, arg0, trySynonyms, doTry) return text } function doCheck(command) { const advantageNames = ["normal", "advantage", "disadvantage"] const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] var character = getCharacter() var arg0 = null if (character.stats.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.stats)) if (arg0 == null && character.skills.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.skills)) if (arg0 == null) arg0 = "Ability" arg0 = toTitleCase(arg0) var arg1 = searchArgument(command, arrayToOrPattern(advantageNames)) if (arg1 == null) arg1 = "normal" else arg1 = arg1.toLowerCase() const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var arg2 = searchArgument(command, arrayToOrPattern(difficultyPatternNames)) if (arg2 == null) arg2 = state.defaultDifficulty else arg2 = arg2.toLowerCase() var die1 = calculateRoll("1d20") var die2 = calculateRoll("1d20") var score = arg1 == "advantage" ? Math.max(die1, die2) : arg1 == "disadvantage" ? Math.min(die1, die2) : die1 var modifier = 0 var skill = character.skills.find(x => x.name.toLowerCase() == arg0.toLowerCase()) if (skill != null) { var stat = character.stats.find((element) => element.name.toLowerCase() == skill.stat.toLowerCase()) if (stat != null) modifier = skill.modifier + getModifier(stat.value) } else { var stat = character.stats.find((element) => element.name.toLowerCase() == arg0.toLowerCase()) if (stat != null) modifier = getModifier(stat.value) } var target = 15 if (/^\d+$/.test(arg2)) target = arg2 else { var targetIndex = difficultyNames.indexOf(arg2) if (targetIndex >= 0 && targetIndex < difficultyNames.length) target = difficultyScores[targetIndex] } state.show = "none" var dieText = arg1 == "advantage" || arg1 == "disadvantage" ? `${arg1}(${die1},${die2})` : die1 var text if (score == 20) text = `\n[${arg0} check DC: ${target} roll: ${dieText}. Critical Success!]\n` else if (score == 1) text = `\n[${arg0} check DC: ${target} roll: ${dieText}. Critical Failure!]\n` else if (modifier != 0) text = `\n[${arg0} check DC: ${target} roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n` else text = `\n[${arg0} check DC: ${target} roll: ${dieText}. ${score >= target ? "Success!" : "Failure!"}]\n` return text } function doTry(command) { if (getArguments(command).length <= 1) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } const advantageNames = ["normal", "advantage", "disadvantage"] const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] var character = getCharacter() var textIndex = 3 var failword = character.name == "You" ? "fail" : "fails" var arg0 = null if (character.stats.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.stats)) if (arg0 == null && character.skills.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.skills)) if (arg0 == null) { arg0 = "Ability" textIndex-- } arg0 = toTitleCase(arg0) var arg1 = searchArgument(command, arrayToOrPattern(advantageNames)) if (arg1 == null) { arg1 = "normal" textIndex-- } else arg1 = arg1.toLowerCase() const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var arg2 = searchArgument(command, arrayToOrPattern(difficultyPatternNames)) if (arg2 == null) { arg2 = state.defaultDifficulty textIndex-- } else arg2 = arg2.toLowerCase() var arg3 = getArgumentRemainder(command, textIndex) var toMatches = arg3.match(/^to\s+/gi) if (toMatches != null) arg3 = arg3.substring(toMatches[0].length) if (!/^.*(\.|!|\?)$/gi.test(arg3)) arg3 += "." var die1 = calculateRoll("1d20") var die2 = calculateRoll("1d20") var score = arg1 == "advantage" ? Math.max(die1, die2) : arg1 == "disadvantage" ? Math.min(die1, die2) : die1 var modifier = 0 var skill = character.skills.find(x => x.name.toLowerCase() == arg0.toLowerCase()) if (skill != null) { var stat = character.stats.find(x => x.name.toLowerCase() == skill.stat.toLowerCase()) if (stat != null) modifier = skill.modifier + getModifier(stat.value) } else { var stat = character.stats.find(x => x.name.toLowerCase() == arg0.toLowerCase()) if (stat != null) modifier = getModifier(stat.value) } var target = 15 if (/^\d+$/.test(arg2)) target = arg2 else { var targetIndex = difficultyNames.indexOf(arg2) if (targetIndex >= 0 && targetIndex < difficultyNames.length) target = difficultyScores[targetIndex] } var dieText = arg1 == "advantage" || arg1 == "disadvantage" ? `${arg1}(${die1},${die2})` : die1 state.show = "prefix" if (score == 20) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}]\n` else if (score == 1) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}]\n` else if (modifier != 0) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n` else state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}. ${score >= target ? "Success!" : "Failure!"}]\n` var text = `\n${character.name} ${score + modifier >= target ? "successfully" : failword + " to"} ${arg3}` if (score == 20) text += " Critical success! Your action was extremely effective." else if (score == 1) text += " Critical failure! There are dire consequences for your action." if (score + modifier >= target || score == 20) text += addXpToAll(Math.floor(state.autoXp * clamp(target, 1, 20) / 20)) + "\n" return text } function doAttack(command) { const advantageNames = ["normal", "advantage", "disadvantage"] const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] var character = getCharacter() var textIndex = 3 var missWord = character.name == "You" ? "miss" : "misses" var tryWord = character.name == "You" ? "try" : "tries" var usingDefaultDifficulty = false var statText = null statText = searchArgument(command, /ranged/gi, textIndex - 1) if (statText == null) { statText = character.meleeStat textIndex-- } else if (statText.toLowerCase() == "ranged") statText = character.rangedStat statText = toTitleCase(statText) var advantageText = searchArgument(command, arrayToOrPattern(advantageNames), textIndex - 1) if (advantageText == null) { advantageText = "normal" textIndex-- } else advantageText = advantageText.toLowerCase() const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var difficultyText = searchArgument(command, arrayToOrPattern(difficultyPatternNames), textIndex - 1) if (difficultyText == null) { difficultyText = state.defaultDifficulty usingDefaultDifficulty = true textIndex-- } else difficultyText = difficultyText.toLowerCase() var targetText = getArgumentRemainder(command, textIndex) if (targetText == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var toMatches = targetText.match(/^to\s+/gi) if (toMatches != null) targetText = targetText.substring(toMatches[0].length) targetText = stripPunctuation(targetText) var die1 = calculateRoll("1d20") var die2 = calculateRoll("1d20") var score = advantageText == "advantage" ? Math.max(die1, die2) : advantageText == "disadvantage" ? Math.min(die1, die2) : die1 var modifier = 0 var stat = character.stats.find(x => x.name.toLowerCase() == statText.toLowerCase()) modifier = character.proficiency if (stat != null) modifier += getModifier(stat.value) var targetRoll = 15 if (/^\d+$/.test(difficultyText)) targetRoll = difficultyText else { var targetIndex = difficultyNames.indexOf(difficultyText) if (targetIndex >= 0 && targetIndex < difficultyNames.length) targetRoll = difficultyScores[targetIndex] } var enemyString = "" if (state.initiativeOrder.length > 0) { var foundEnemy for (var enemy of state.enemies) { if (targetText.toLowerCase().includes(enemy.name.toLowerCase())) { foundEnemy = enemy break } } if (foundEnemy == null) { var indexMatches = targetText.match(/(?<=enemy\s*)\d+/gi) if (indexMatches != null) { foundEnemy = state.enemies[parseInt(indexMatches[0]) - 1] log(`foundEnemy:${foundEnemy}`) targetText = targetText.replace(/enemy\s*d+/gi, foundEnemy.name) } } var damage if (/^\d*d\d+((\+|-)d+)?$/gi.test(character.damage)) damage = score == 20 ? calculateRoll(character.damage) + calculateRoll(character.damage) : calculateRoll(character.damage) else damage = parseInt(character.damage) var damageMatches = targetText.match(/\d*d\d+((\+|-)d+)?/gi) if (damageMatches != null) damage = score == 20 ? calculateRoll(damageMatches[0]) + calculateRoll(damageMatches[0]) : calculateRoll(damageMatches[0]) else { damageMatches = targetText.match(/\d+/g) if (damageMatches != null) damage = score == 20 ? parseInt(damageMatches[damageMatches.length - 1]) * 2 : parseInt(damageMatches[damageMatches.length - 1]) } if (foundEnemy != null) { if (usingDefaultDifficulty) targetRoll = foundEnemy.ac if (score == 20 || score + modifier >= targetRoll) { if (score == 20) enemyString += `\nCritical Damage: ${damage}\n` else enemyString += `\nDamage: ${damage}\n` foundEnemy.health = Math.max(0, foundEnemy.health - damage) if (foundEnemy.health == 0) enemyString += ` ${toTitleCase(foundEnemy.name)} has been defeated!` else enemyString += ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!` } } } var dieText = advantageText == "advantage" || advantageText == "disadvantage" ? `${advantageText}(${die1},${die2})` : die1 state.show = "prefix" if (score == 20) state.prefix = `\n[Enemy AC: ${targetRoll} Attack roll: ${dieText}]\n` else if (score == 1) state.prefix = `\n[Enemy AC: ${targetRoll} Attack roll: ${dieText}]\n` else if (modifier != 0) state.prefix = `\n[Enemy AC: ${targetRoll} Attack roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= targetRoll ? "Success!" : "Failure!"}]\n` else state.prefix = `\n[Enemy AC: ${targetRoll} Attack roll: ${dieText}. ${score >= targetRoll ? "Success!" : "Failure!"}]\n` var text if (score + modifier >= targetRoll) text = `\n${toTitleCase(character.name)} successfully hit ${targetText}!` else text = `\n${toTitleCase(character.name)} ${tryWord} to hit ${targetText}. ${toTitleCase(character.name)} ${missWord}!` if (score == 20) text += " Critical success! Your attack is exceptionally damaging!" else if (score == 1) text += " Critical failure! Your attack missed in a spectacular way!" if (enemyString != null) text += enemyString if (score + modifier >= targetRoll || score == 20) text += addXpToAll(Math.floor(state.autoXp * clamp(targetRoll, 1, 20) / 20)) return text + "\n" } function doNote(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 != null && arg0.length > 0) { state.notes.push(arg0) state.show = "none" return "\n[Note added successfully]\n" } else { state.notes.push(history[history.length - 1].text) state.show = "none" return "\n[The last action was successfully added to the notes]\n" } } function doShowDay(command) { state.show = "none" return `\n[It is day ${state.day}]\n` } function doSetDay(command) { var arg0 = getArgument(command, 0) if (arg0 == null || isNaN(arg0)) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } state.day = parseInt(arg0) state.show = "none" return `\n[The day has been set to day ${state.day}]\n` } function doShowNotes(command) { state.show = "showNotes" return " " } function doCreateLocation(command) { command = command.replaceAll(/\s*,\s*/g, " ") var locationArgIndex = 2 var arg0 = getArgument(command, 0) var arg1 = getArgument(command, 1) if (arg0.toLowerCase() == "here") { arg0 = state.x arg1 = state.y locationArgIndex = 1 } else if (arg0.toLowerCase() == "far") { var cx = state.x var cy = state.y var coords = rotate(cx, cy, getRandomFloat(50, 100) + cx, cy, Math.random() * 360) arg0 = coords[0] arg1 = coords[1] locationArgIndex = 1 } else if (arg0 == null || isNaN(arg0)) { var cx = state.x var cy = state.y var coords = rotate(cx, cy, getRandomFloat(1, 10) + cx, cy, Math.random() * 360) arg0 = coords[0] arg1 = coords[1] locationArgIndex = 0 } else if (arg1 == null || isNaN(arg1)) { var cx = state.x var cy = state.y var coords = rotate(cx, cy, parseFloat(arg0) + cx, cy, Math.random() * 360) arg0 = coords[0] arg1 = coords[1] locationArgIndex = 1 } var arg2 = getArgumentRemainder(command, locationArgIndex) if (arg2 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var location = createLocation(arg0, arg1, arg2) state.show = "none" return `\n[Location ${toTitleCase(arg2)} has been created at (${location.x},${location.y})]\n` } function doGoNorth(command) { command = command.replaceAll(/\s*,\s*/g, " ") var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) arg0 = 1 else { if (isNaN(arg0)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } arg0 = parseInt(arg0) } return doGoToLocation(`${commandName} ${state.x} ${state.y - arg0}`) } function doGoSouth(command) { command = command.replaceAll(/\s*,\s*/g, " ") var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) arg0 = 1 else { if (isNaN(arg0)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } arg0 = parseInt(arg0) } return doGoToLocation(`${commandName} ${state.x} ${state.y + arg0}`) } function doGoEast(command) { command = command.replaceAll(/\s*,\s*/g, " ") var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) arg0 = 1 else { if (isNaN(arg0)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } arg0 = parseInt(arg0) } return doGoToLocation(`${commandName} ${state.x + arg0} ${state.y}`) } function doGoWest(command) { command = command.replaceAll(/\s*,\s*/g, " ") var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) arg0 = 1 else { if (isNaN(arg0)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } arg0 = parseInt(arg0) } return doGoToLocation(`${commandName} ${state.x - arg0} ${state.y}`) } function doGoToLocation(command) { command = command.replaceAll(/\s*,\s*/g, " ") var character = getCharacter() var characterName = character == null ? "You" : character.name var possessiveName = getPossessiveName(characterName) var travelWord = characterName == "You" ? "travel" : "travels" var locationArgIndex = 2 var arg0 = getArgument(command, 0) var arg1 = getArgument(command, 1) if (arg0 == null || isNaN(arg0)) { arg0 = state.x arg1 = state.y locationArgIndex = 0 } if (arg0 != null && (arg1 == null || isNaN(arg1))) { arg1 = null locationArgIndex = 1 } var distance = 0 var location var locationName = getArgumentRemainder(command, locationArgIndex) if (locationName == null && locationArgIndex == 0) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (!isNaN(arg0) && arg1 == null && locationName == null) { arg0 = parseInt(arg0) - 1 if (arg0 < 0 || arg0 >= state.locations.length) { state.show = "none" return "\n[Error: Incorrect location number. See #help]\n" } location = state.locations[arg0] arg0 = null arg1 = null } else if (arg1 == null && locationName != null) { var index = state.locations.findIndex(x => x.name.toLowerCase() == locationName.toLowerCase()) if (index != -1) { location = state.locations[index] var direction = pointDirection(state.x, state.y, location.x, location.y) var args = rotate(state.x, state.y, state.x + parseInt(arg0), state.y, direction) arg0 = Math.round(args[0]) arg1 = Math.round(args[1]) location = null } else { arg1 = state.y location = null } } else if (locationName == null) { var index = state.locations.findIndex(x => x.x == arg0 && x.y == arg1) if (index != -1) location = state.locations[index] } else { var index = state.locations.findIndex(x => x.name.toLowerCase() == locationName.toLowerCase()) if (index != -1) location = state.locations[index] else location = createLocation(arg0, arg1, locationName) } if (location == null) { distance = pointDistance(state.x, state.y, arg0, arg1) state.x = parseInt(arg0) state.y = parseInt(arg1) state.location = null } else { distance = pointDistance(state.x, state.y, location.x, location.y) state.x = location.x state.y = location.y state.location = location.name } distance = distance.toFixed(1) state.show = "none" if (location == null) return `\n${characterName} ${travelWord} ${distance > 0 ? distance + " units " : ""} to (${arg0},${arg1})` if (state.characters.length > 1) return `\n${possessiveName} party travels ${distance > 0 ? distance + " units " : ""}to ${toTitleCase(location.name)} at (${location.x},${location.y})\n` return `\n${characterName} ${travelWord} ${distance > 0 ? distance + " units " : ""}to ${toTitleCase(location.name)} at (${location.x},${location.y})\n` } function doGetLocation(command) { state.show = "location" return `\n[You are at ${state.location == null ? "" : "the location " + toTitleCase(state.location) + " "}(${state.x},${state.y})]` } function doClearLocations(command) { var arg0 = getArgument(command, 0) if (arg0 != null) { return doRemoveLocation(command) } state.locations = [] state.location = null state.show = "none" return "\n[The locations have been cleared]\n" } function doRemoveLocation(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (/\d+\D+(\d+\D*)+/gi.test(arg0)) { var list = arg0.split(/\D+/) list.sort(function(a, b) { return b - a; }); var text = "\n" list.forEach(x => { var num = parseInt(x) - 1 if (num >= state.locations.length) { state.show = "none" return `\n[Error: Location ${x} does not exist. See #showlocations]\n` } var location = state.locations[num] state.locations.splice(num, 1) text += `[The location ${toTitleCase(location.name)} has been removed]\n` }) state.show = "none" return text } var location if (isNaN(arg0)) arg0 = state.locations.findIndex(x => x.name.toLowerCase() == arg0.toLowerCase()) else arg0-- if (arg0 == -1) { state.show = "none" return "\n[Error: Location not found. See #showlocations]\n" } else if (arg0 >= state.locations.length || arg0 < 0) { state.show = "none" return "\n[Error: Location number out of bounds. See #showlocations]\n" } else { location = state.locations[arg0] state.locations.splice(arg0, 1) } state.show = "none" return `\n[The location ${toTitleCase(location.name)} has been removed]\n` } function doShowLocations(command) { var arg0 = searchArgument(command, /^sort$/gi) state.sortLocations = arg0 != null state.show = "locations" return " " } function doEncounter(command) { var arg0 = getArgument(command, 0) if (arg0 == null) { arg0 = "easy" } var encounter = createEncounter(arg0) state.enemies = encounter.enemies var text = `\n${encounter.text}\n` state.prefix = "\n" if (encounter.enemies.length > 0) { state.prefix += "You encounter the following enemies:\n" for (var enemy of encounter.enemies) { state.prefix += `${toTitleCase(enemy.name)} (Health: ${enemy.health} AC: ${enemy.ac} Initiative: ${enemy.initiative})\n` } } state.prefix += encounter.enemies.length > 0 ? "[Type #initiative to begin the battle]\n" : "" state.show = "prefix" return text } function doShowEnemies(command) { state.show = "showEnemies" return " " } function doRemoveEnemy(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (/\d+\D+(\d+\D*)+/gi.test(arg0)) { var list = arg0.split(/\D+/) list.sort(function(a, b) { return b - a; }); var text = "\n" list.forEach(x => { var num = parseInt(x) - 1 if (num >= state.enemies.length) { state.show = "none" return `\n[Error: Enemy ${x} does not exist. See #showenemies]\n` } var enemy = state.enemies[num] state.enemies.splice(num, 1) var index = state.initiativeOrder.indexOf(enemy) if (index >= 0) state.initiativeOrder.splice(index, 1) text += `[The enemy ${toTitleCase(enemy.name)} has been removed]\n` }) state.show = "none" return text } var enemy if (isNaN(arg0)) arg0 = state.enemies.findIndex(x => x.name.toLowerCase() == arg0.toLowerCase()) else arg0-- if (arg0 == -1) { state.show = "none" return "\n[Error: Enemy not found. See #showenemies]\n" } else if (arg0 >= state.enemies.length || arg0 < 0) { state.show = "none" return "\n[Error: Location number out of bounds. See #showenemies]\n" } else { enemy = state.enemies[arg0] state.enemies.splice(arg0, 1) } state.show = "none" return `\n[The enemy ${toTitleCase(enemy.name)} has been removed]\n` } function doClearEnemies(command) { var arg0 = getArgument(command, 0) if (arg0 != null) { return doRemoveEnemy(command) } state.enemies = [] state.initiativeOrder = [] state.show = "none" return "\n[The enemies have been cleared]\n" } function doAddEnemy(command) { var name = getArgument(command, 0) if (name == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var health = getArgument(command, 1) if (health == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(health)) { health = calculateRoll(health) } else if (isNaN(health)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } health = parseInt(health) var ac = getArgument(command, 2) if (ac == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(ac)) { ac = calculateRoll(ac) } else if (isNaN(ac)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } ac = parseInt(ac) var hitModifier = getArgument(command, 3) if (hitModifier == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(hitModifier)) { hitModifier = calculateRoll(hitModifier) } else if (isNaN(hitModifier)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } var damage = getArgument(command, 4) if (damage == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } else if (isNaN(damage) && !/^\d*d\d+((\+|-)\d+)?$/gi.test(damage)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } var initiative = getArgument(command, 5) if (initiative == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(initiative)) { initiative = calculateRoll(initiative) } else if (isNaN(initiative)) { state.show = "none" return "\n[Error: Expected a number. See #help]\n" } initiative = parseInt(initiative) var spells = [] var spell = null var index = 6 do { spell = getArgument(command, index++) if (spell != null) spells.push(spell) } while (spell != null) var enemy = createEnemy(name, health, ac, hitModifier, damage, initiative) enemy.spells = spells var enemyMatches = state.enemies.filter(x => x.name.toLowerCase() == enemy.name.toLowerCase() || x.name.toLowerCase() == `${enemy.name.toLowerCase()} a`) if (enemyMatches.length > 0) { enemy.name = getUniqueName(enemy.name) if (enemy.name.endsWith("A")) { enemyMatches[0].name = enemy.name enemy.name = enemy.name.substring(0, enemy.name.length - 1) + "B" } } state.enemies.push(enemy) state.show = "none" return `[Enemy ${toTitleCase(enemy.name)} has been created]` } function doInitiative(command) { for (character of state.characters) { var stat = character.stats.find(element => element.name.toLowerCase() == "dexterity") if (stat == null) character.calculatedInitiative = calculateRoll("d20") else character.calculatedInitiative = calculateRoll("d20") + getModifier(stat.value) } for (enemy of state.enemies) { if (isNaN(enemy.initiative)) enemy.calculatedInitiative = calculateRoll(enemy.initiative) else enemy.calculatedInitiative = enemy.initiative } if (state.enemies.length == 0) { state.show = "none" return "\n[Error: No enemies! Type #addenemy or #encounter]\n" } createInitiativeOrder() if (state.initiativeOrder.length == 0) { state.show = "none" return "\n[Error: No combatants! Ensure that your characters have health and you have added enemies. See #help]\n" } state.show = "initiative" return "\nBattle has commenced!\n" } function doFlee(command) { if (state.initiativeOrder.length == 0) { state.show = "none" return "\n[Error: Not in combat. Type #initiative first]\n" } var difficulty = getArgument(command, 0) if (difficulty != null) { const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var difficultyIndex = difficultyNames.indexOf(difficulty) if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) { difficulty = difficultyScores[difficultyIndex] } } else { difficulty = state.defaultDifficulty } var roll = calculateRoll("d20") var text = "" if (difficulty != 0) text += `\n[DC: ${difficulty} Roll: ${roll}]\n` if (roll >= difficulty) { state.initiativeOrder = [] text += `\nThe party successfuly flees from battle!\n` } else text += `\nThe party tries to flee from battle, but fails!\n` return text } function doTurn(command) { if (state.initiativeOrder.length > 0) state.initiativeOrder.splice(0, 1) var defeatedEnemies = 0 for (var enemy of state.enemies) { if (enemy.health > 0) continue defeatedEnemies++ var index = state.initiativeOrder.indexOf(enemy) if (index >= 0) state.initiativeOrder.splice(index, 1) } var defeatedCharacters = 0 for (var character of state.characters) { if (character.health > 0) continue defeatedCharacters++ var index = state.initiativeOrder.indexOf(character) if (index >= 0) state.initiativeOrder.splice(index, 1) } if (state.initiativeOrder.length == 0) createInitiativeOrder() if (state.initiativeOrder.length == 0) { return "\nDraw! All combatants have been incapacitated.\n" } if (defeatedEnemies == state.enemies.length) { state.initiativeOrder = [] return "\nVictory! The party has defeated all opponents.\n" } if (defeatedCharacters == state.characters.length) { state.initiativeOrder = [] return "\nDefeat! The entire party has been incapacitated.\n" } var activeCharacter = state.initiativeOrder[0] var activeCharacterName = toTitleCase(activeCharacter.name) var possessiveName = getPossessiveName(activeCharacter.name) if (possessiveName == "Your") possessiveName = "your" if (activeCharacter.className != null) { state.show = "none" return `\n[It is ${possessiveName} turn]\n` } else { var characters = state.characters.filter(x => x.health > 0) var target = characters[getRandomInteger(0, characters.length - 1)] var areWord = target.name == "You" ? "are" : "is" var targetNameAdjustedCase = target.name == "You" ? "you" : toTitleCase(target.name) var attack = calculateRoll(`1d20${activeCharacter.hitModifier > 0 ? "+" + activeCharacter.hitModifier : activeCharacter.hitModifier < 0 ? activeCharacter.hitModifier : ""}`) var hit = attack >= target.ac var text = `\n[It is ${possessiveName} turn]\n` if (getRandomBoolean() || activeCharacter.spells.length == 0) { if (hit) { var damage = isNaN(activeCharacter.damage) ? calculateRoll(activeCharacter.damage) : activeCharacter.damage target.health = Math.max(target.health - damage, 0) text += `\n[Character AC: ${target.ac} Attack roll: ${attack}]\n` text += `${activeCharacterName} attacks ${targetNameAdjustedCase} for ${damage} damage!\n` if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious! \n` else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n` } else text += `${activeCharacterName} attacks ${targetNameAdjustedCase} but misses!\n` } else { var spell = activeCharacter.spells[getRandomInteger(0, activeCharacter.spells.length - 1)] var diceMatches = spell.match(/(?<=^.*)\d*d\d+((\+|-)\d+)?$/gi) if (diceMatches == null) text += `${activeCharacterName} casts spell ${spell}!` else { if (hit) { var damage = calculateRoll(diceMatches[0]) var spell = spell.substring(0, spell.length - diceMatches[0].length) target.health = Math.max(target.health - damage, 0) text += `\n[Character AC: ${target.ac} Attack roll: ${attack}]\n` text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} for ${damage} damage!` if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious!\n` else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n` } else text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} but misses!\n` } } return text } } function doTake(command) { var itemIndex = 0 var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (arg0 == "the") { var tempArg = getArgument(command, 1) if (tempArg != null && !isNaN(tempArg)) { arg0 = tempArg itemIndex++ } } if (!isNaN(arg0)) itemIndex++ const item = { quantity: isNaN(arg0) ? 1 : arg0, name: getArgumentRemainder(command, itemIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true) } var character = getCharacter() var commandName = getCommandName(command) var commandNamePlural = commandName.plural(character.name == "You") var haveWord = character.name == "You" ? "have" : "has" var displayItemName = item.name.plural(item.quantity == 1) if (item.quantity < 0) item.quantity = 1 var text = "\n" if (item.quantity == 1) text += `${character.name} ${commandNamePlural} ${displayItemName.toLowerCase().startsWith("the ") ? "" : "the "}${displayItemName}.\n` else text += `${character.name} ${commandNamePlural} ${item.quantity} ${displayItemName}.\n` var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase()) if (index == -1) { character.inventory.push(item) } else { var existingItem = character.inventory[index] existingItem.quantity = parseInt(existingItem.quantity) + parseInt(item.quantity) displayItemName = existingItem.name.plural(existingItem.quantity == 1) text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n` } return text } function doMap(command) { state.show = "map" return " " } function doDrop(command) { var character = getCharacter() var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var characterNameAdjustedCase = character.name == "You" ? "you" : character.name var dontWord = character.name == "You" ? "don't" : "doesn't" var haveWord = character.name == "You" ? "have" : "has" var tryWord = character.name == "You" ? "try" : "tries" var itemArgIndex = 0 if (isNaN(arg0)) { if (allSynonyms.indexOf(arg0.toLowerCase()) > -1) { arg0 = Number.MAX_SAFE_INTEGER itemArgIndex++ } else { arg0 = 1 } } else { itemArgIndex++ } const item = { quantity: arg0, name: getArgumentRemainder(command, itemArgIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true) } var displayItemName = item.name.plural(item.quantity == 1) if (item.quantity < 0) item.quantity = 1 var text = "\n" var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase()) if (index == -1) { if (item.quantity == 1) text += `${character.name} ${tryWord} to ${commandName} the ${displayItemName}, but ${character.name} ${dontWord} have any.` else text += `${character.name} ${tryWord} to ${commandName} ${item.quantity == Number.MAX_SAFE_INTEGER ? arg0 : item.quantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.` } else { var existingItem = character.inventory[index] if (existingItem.quantity == 1) text = `\n${character.name} ${commandName.plural(character.name == "You")} the ${displayItemName.plural(true)}.\n` else if (parseInt(item.quantity) >= parseInt(existingItem.quantity)) text = `${character.name} ${commandName.plural(character.name == "You")} all ${existingItem.quantity} of the ${displayItemName}.` else text = `\n${character.name} ${commandName.plural(character.name == "You")} ${item.quantity} ${displayItemName}. \n` existingItem.quantity -= item.quantity if (existingItem.quantity <= 0) { existingItem.quantity = 0 character.inventory.splice(index, 1) } if (existingItem.quantity > 0) { displayItemName = existingItem.name.plural(existingItem.quantity == 1) text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n` } } return text } function doGive(command) { var character = getCharacter() var commandName = getCommandName(command) var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = getArgument(command, 1) if (arg1 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var foundAll = allSynonyms.indexOf(arg1) > -1 const item = { quantity: !isNaN(arg1) ? arg1 : foundAll ? Number.MAX_SAFE_INTEGER : 1, name: getArgumentRemainder(command, isNaN(arg1) && !foundAll ? 1 : 2).replace(/^((the)|(a)|(an)|(of the))\s/, "").plural(true) } var otherCharacter = getCharacter(arg0) if (otherCharacter == null || otherCharacter.name == "You" && arg0.toLowerCase() != "you") { state.show = "none" return "\n[Error: Target character does not exist. See #characters]\n" } var characterNameAdjustedCase = character.name == "You" ? "you" : character.name var dontWord = character.name == "You" ? "don't" : "doesn't" var haveWord = character.name == "You" ? "have" : "has" var tryWord = character.name == "You" ? "try" : "tries" var otherHaveWord = otherCharacter.name == "You" ? "have" : "has" var otherNameAdjustedCase = otherCharacter.name == "You" ? "you" : otherCharacter.name var displayItemName = item.name.plural(item.quantity == 1) var characterQuantityText = "" if (item.quantity < 0) item.quantity = 1 var text = "\n\n" var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase()) if (index == -1) { if (item.quantity == 1) text += `${character.name} ${tryWord} to ${commandName.plural(true)} the ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.` else text += `${character.name} ${tryWord} to ${commandName.plural(true)} ${item.quantity == Number.MAX_SAFE_INTEGER ? arg0 : item.quantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.` return text + "\n\n" } else { var existingItem = character.inventory[index] if (item.quantity >= existingItem.quantity) { item.quantity = existingItem.quantity existingItem.quantity = 0 character.inventory.splice(index, 1) } else { existingItem.quantity -= item.quantity } if (existingItem.quantity > 0) { characterQuantityText = ` ${character.name} now ${haveWord} ${existingItem.quantity} ${existingItem.name.plural(existingItem.quantity == 1)}.` } else if (item.quantity > 1) { characterQuantityText = ` ${character.name} ${dontWord} have any more.` } } if (item.quantity == 1) text += `${character.name} ${commandName.plural(character.name == "You")} ${otherNameAdjustedCase} the ${displayItemName}.` else text += `${character.name} ${commandName.plural(character.name == "You")} ${otherNameAdjustedCase} ${item.quantity} ${displayItemName}.` var otherIndex = otherCharacter.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase()) if (otherIndex == -1) { otherCharacter.inventory.push(item) } else { var existingItem = otherCharacter.inventory[otherIndex] existingItem.quantity = parseInt(existingItem.quantity) + parseInt(item.quantity) displayItemName = existingItem.name.plural(existingItem.quantity == 1) text += ` ${otherCharacter.name} now ${otherHaveWord} ${existingItem.quantity} ${displayItemName}.` } return text + characterQuantityText + "\n\n" } function doBuy(command) { var character = getCharacter() command = command.replaceAll(/\s+((for)|(with)|(the)|(a)|(an))\s+/g, " ") var args = [] args.push(getArgument(command, 0)) if (args[0] == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } args.push(getArgument(command, 1)) if (args[1] == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } args.push(getArgument(command, 2)) args.push(getArgument(command, 3)) var buyQuantity if (isNaN(args[0])) { buyQuantity = 1 } else { buyQuantity = args[0] args.splice(0, 1) } var buyName buyName = args[0].plural(true) var sellQuantity if (isNaN(args[1])) { sellQuantity = 1 } else { sellQuantity = args[1] args.splice(1, 1) } var sellName = args[1].plural(true) var characterNameAdjustedCase = character.name == "You" ? "you" : character.name var dontWord = character.name == "You" ? "don't" : "doesn't" var haveWord = character.name == "You" ? "have" : "has" var tryWord = character.name == "You" ? "try" : "tries" var tradeWord = character.name == "You" ? "trade" : "trades" var buyWord = character.name == "You" ? "buy" : "buys" var displayItemName = sellName.plural(sellQuantity == 1) var buyItemTotal = 0; var sellItemTotal = 0; if (sellQuantity < 0) sellQuantity = 1 var text = "\n\n" var index = character.inventory.findIndex((element) => element.name.toLowerCase() == sellName.toLowerCase()) if (index == -1) { if (sellQuantity == 1) text += `${character.name} ${tryWord} to trade the ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.` else text += `${character.name} ${tryWord} to trade ${sellQuantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.` return text + "\n\n" } else { var existingItem = character.inventory[index] if (sellQuantity >= existingItem.quantity) { sellQuantity = existingItem.quantity existingItem.quantity = 0 character.inventory.splice(index, 1) } else { existingItem.quantity -= sellQuantity } sellItemTotal = existingItem.quantity } var suffix = `${buyQuantity} ${buyName.plural()}` if (buyQuantity == 1) suffix = `the ${buyName.plural(true)}` if (sellQuantity == 1) text += `${character.name} ${tradeWord} the ${displayItemName} for ${suffix}.` else text += `${character.name} ${tradeWord} ${sellQuantity} ${displayItemName} for ${suffix}.` index = character.inventory.findIndex((element) => element.name.toLowerCase() == buyName.toLowerCase()) if (index == -1) { character.inventory.push({name: buyName, quantity: buyQuantity}) buyItemTotal = buyQuantity } else { var existingItem = character.inventory[index] existingItem.quantity = parseInt(existingItem.quantity) + parseInt(buyQuantity) buyItemTotal = existingItem.quantity } text += ` ${character.name} now ${haveWord} ${sellItemTotal} ${sellName.plural(sellItemTotal == 1)} and ${buyItemTotal} ${buyName.plural(buyItemTotal == 1)}.` return text + "\n\n" } function doSell(command) { command = command.replace(/\s+((for)|(with))\s+/, " ") var args = [] args.push(getArgument(command, 0)) if (args[0] == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } args.push(getArgument(command, 1)) if (args[1] == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } args.push(getArgument(command, 2)) args.push(getArgument(command, 3)) var sellQuantity if (isNaN(args[0])) { sellQuantity = 1 } else { sellQuantity = args[0] args.splice(0, 1) } var sellName sellName = args[0] var buyQuantity if (isNaN(args[1])) { buyQuantity = 1 } else { buyQuantity = args[1] args.splice(1, 1) } var buyName = args[1] return doBuy(`buy ${buyQuantity} ${buyName} ${sellQuantity} ${sellName}`) } function doRenameItem(command) { var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var arg1 = getArgument(command, 1) if (arg1 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var commandName = getCommandName(command) var character = getCharacter() var haveWord = character.name == "You" ? "have" : "has" var possessiveName = getPossessiveName(character.name) state.show = "none" var text = `\n[${possessiveName} ${arg0} has been renamed to ${arg1}]\n` var index = character.inventory.findIndex((element) => element.name.toLowerCase() == arg0.toLowerCase()) if (index >= 0 ) { var existingItem = character.inventory[index] existingItem.name = arg1 } return text } function doInventory(command) { state.show = "inventory" return " " } function doLearnSpell(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == "") { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var character = getCharacter() var tryWord = character.name == "You" ? "try" : "tries" var found = character.spells.find((element) => element == arg0) if (found != null) return `\n[${character.name} ${tryWord} to learn the spell ${arg0}, but already knows it]\n` character.spells.push(arg0) addStoryCard(arg0, "", "spell") return `\n${character.name} learned the spell ${toTitleCase(arg0)}.\n` } function doForgetSpell(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == "") { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var dontWord = character.name == "You" ? "don't" : "doesn't" var tryWord = character.name == "You" ? "try" : "tries" var found = character.spells.find(x => x.toLowerCase() == arg0.toLowerCase()) if (found == null) { state.show = "none" return `\n[${character.name} ${tryWord} to forget the spell ${arg0}, but ${character.name} ${dontWord} even know it]\n` } var index = character.spells.findIndex(x => x.toLowerCase() == arg0.toLowerCase()) character.spells.splice(index, 1) state.show = "none" return `\n[${character.name} forgot the spell ${arg0}]\n` } function doRemoveStat(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == "") { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var dontWord = character.name == "You" ? "don't" : "doesn't" var tryWord = character.name == "You" ? "try" : "tries" var found = character.stats.find((element) => element == arg0) if (found == null) return `\n[${character.name} ${tryWord} to remove the ability ${arg0}, but ${character.name} ${dontWord} even know it]\n` var index = character.stats.findIndex((element) => element.toLowerCase() == arg0.toLowerCase()) character.stats.splice(index, 1) return `\n[${character.name} removed the ability ${arg0}]\n` } function doRemoveSkill(command) { var character = getCharacter() var arg0 = getArgumentRemainder(command, 0) if (arg0 == "") { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var dontWord = character.name == "You" ? "don't" : "doesn't" var tryWord = character.name == "You" ? "try" : "tries" var found = character.skills.find((element) => element == arg0) if (found == null) return `\n[${character.name} ${tryWord} to remove the skill ${arg0}, but ${character.name} ${dontWord} even know it]\n` var index = character.skills.findIndex((element) => element.toLowerCase() == arg0.toLowerCase()) character.skills.splice(index, 1) return `\n[${character.name} removed the skill ${arg0}]\n` } function doCastSpell(command) { const advantageNames = ["normal", "advantage", "disadvantage"] const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "automatic"] const difficultyScores = [30, 25, 20, 15, 10, 5, 0] var character = getCharacter() const dontWord = character.name == "You" ? "don't" : "doesn't" const tryWord = character.name == "You" ? "try" : "tries" var usingDefaultDifficulty = false var spellIndex = 2; var advantage = searchArgument(command, arrayToOrPattern(advantageNames), spellIndex - 1) if (advantage == null) { advantage = "normal" spellIndex-- } const difficultyPatternNames = [...new Set(difficultyNames)] difficultyPatternNames.push("\\d+") var difficulty = searchArgument(command, arrayToOrPattern(difficultyPatternNames), spellIndex - 1) if (difficulty == null) { difficulty = state.defaultDifficulty usingDefaultDifficulty = true spellIndex-- } var difficultyIndex = difficultyNames.indexOf(difficulty) if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) { difficulty = difficultyScores[difficultyIndex] } var spell = getArgument(command, spellIndex) if (spell == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } var targetText = null var atWord = null var found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase()) if (found != null) { targetText = getArgumentRemainder(command, spellIndex + 1) if (targetText != null) { targetText = targetText.trim() if (!/^((at)|(on))\s+.*/.test(targetText)) targetText = "at " + targetText } } else { var remainder = getArgumentRemainder(command, spellIndex) if (/.*\s((at)|(on))\s.*/i.test(remainder)) { spell = remainder.replace(/\s+((at)|(on)).*/i, "").trim() targetText = remainder.replace(/^.*\s+(?=(at)|(on))/i, "").trim() } else { spell = getArgumentRemainder(command, spellIndex).trim() } found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase()) } if (found == null) { state.show = "none" return `\n[${toTitleCase(character.name)} ${tryWord} to cast the spell ${spell}, but ${character.name == "You" ? "you" : toTitleCase(character.name)} ${dontWord} know it]\n` } var text = `${character.name} cast the spell ${spell}${advantage != "normal" ? " with " + advantage : ""}${targetText == null ? "" : " " + targetText}.` var modifier = 0 if (character.spellStat != null) { var stat = character.stats.find((element) => element.name.toLowerCase() == character.spellStat.toLowerCase()) if (stat != null) modifier = getModifier(stat.value) } var roll1 = calculateRoll("d20") var roll2 = calculateRoll("d20") var roll = advantage == "advantage" ? Math.max(roll1, roll2) : advantage == "disadvantage" ? Math.min(roll1, roll2) : roll1 var enemyString = "" if (targetText != null && state.initiativeOrder.length > 0) { var foundEnemy for (var enemy of state.enemies) { if (targetText.toLowerCase().includes(enemy.name.toLowerCase())) { foundEnemy = enemy break } } if (foundEnemy == null) { var indexMatches = targetText.match(/(?<=enemy\s*)\d+/gi) if (indexMatches != null) { foundEnemy = state.enemies[parseInt(indexMatches[0]) - 1] targetText = targetText.replace(/enemy\s*d+/gi, foundEnemy.name) } } var damage = roll == 20 ? calculateRoll("2d6") + calculateRoll("2d6") : calculateRoll("2d6") var damageMatches = targetText.match(/\d*d\d+((\+|-)d+)?/gi) if (damageMatches != null) damage = roll == 20 ? calculateRoll(damageMatches[0]) + calculateRoll(damageMatches[0]) : calculateRoll(damageMatches[0]) else { damageMatches = targetText.match(/\d+/g) if (damageMatches != null) damage = roll == 20 ? parseInt(damageMatches[damageMatches.length - 1]) * 2 : parseInt(damageMatches[damageMatches.length - 1]) } if (foundEnemy != null) { if (usingDefaultDifficulty) difficulty = foundEnemy.ac if (roll == 20 || roll + modifier >= difficulty) { if (roll == 20) enemyString += `\nCritical Damage: ${damage}\n` else enemyString += `\nDamage: ${damage}\n` foundEnemy.health = Math.max(0, foundEnemy.health - damage) if (foundEnemy.health == 0) enemyString += ` ${toTitleCase(foundEnemy.name)} has been defeated!\n` else enemyString = ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!\n` } } } state.show = "prefix" var dieText = advantage == "advantage" || advantage == "disadvantage" ? `${advantage}(${roll1},${roll2})` : roll1 var difficultyWord = targetText == null ? "Difficulty" : "Armor" if (roll == 20) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. Critcal Success!]\n` else if (roll == 1) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. Critcal Failure!]\n` else if (modifier != 0) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${roll + modifier}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n` else state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n` if (roll == 20) text += ` Critical success!` else if (roll == 1) text += ` Critical failure! The spell ${targetText != null ? "misses" : "fails"} in a spectacular way.` else if (roll + modifier >= difficulty) text += ` The spell ${targetText != null ? "hits the target" : "is successful"}!` else text += ` The spell ${targetText != null ? "misses" : "fails"}!` if (enemyString != null) text += enemyString if (roll + modifier >= difficulty || roll == 20) text += addXpToAll(Math.floor(state.autoXp * clamp(difficulty, 1, 20) / 20)) return `\n${text}\n` } function doShowCharacters(command) { state.show = "characters" return " " } function doSpellbook(command) { state.show = "spellbook" return " " } function doShowSkills(command) { state.show = "skills" return " " } function doShowStats(command) { state.show = "stats" return " " } function doClearNotes(command) { state.notes = [] state.show = "clearNotes" return " " } function doClearInventory(command) { var character = getCharacter() character.inventory = [] state.show = "clearInventory" return " " } function doEraseNote(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) arg0 = 1 var list = arg0.split(/\D+/) list.sort(function(a, b) { return b - a; }); var text = "\n" list.forEach(x => { var num = parseInt(x) - 1 if (num >= state.notes.length) { state.show = "none" return `\n[Error: Note ${x} does not exist. Type #shownotes]\n` } state.notes.splice(num, 1) text += `[Note #${x} removed]\n` }) state.show = "none" return text } function doRemoveCharacter(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } for (var i = 0; i < state.characters.length; i++) { var character = state.characters[i] if (character.name.toLowerCase() == arg0.toLowerCase()) { state.characters.splice(i, 1) state.show = "none" return `[Character ${character.name} removed]` } } return `[Character ${arg0} was not found]` } function doGenerateName(command) { var gender = searchArgument(command, /^(male)|(female)$/gi) if (gender == null) gender = "male" var genre = searchArgument(command, /^(fantasy)|(modern)|(scifi)|(nordic)$/gi) if (genre == null) genre = "fantasy" state.show = "none" return `[The character's name is ${generateName(genre, gender.toLowerCase() == "male")}]` } function doClearSpells(command) { var character = getCharacter() character.spells = [] state.show = "clearSpells" return " " } function doClearStats(command) { var character = getCharacter() character.stats = [] state.show = "clearStats" return " " } function doClearSkills(command) { var character = getCharacter() character.skills = [] state.show = "clearSkills" return " " } function doVersion(command) { state.show = "none" return `[${version}]` } function doSetDamage(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (/^\d*d\d+((\+|-)\d+)?$/gi.test(arg0)) { character.damage = arg0 } else if (!isNaN(arg0)) { character.damage = parseInt(arg0) } else { state.show = "none" return "\n[Error: Not a number. See #help]\n" } var possessiveName = getPossessiveName(character.name) state.show = "none" return `\n[${possessiveName} attack damage is set to ${character.damage}]\n` } function doSetProficiency(command) { var character = getCharacter() var arg0 = getArgument(command, 0) if (arg0 == null) { state.show = "none" return "\n[Error: Not enough parameters. See #help]\n" } if (/^\d*d\d+((\+|-)\d+)?$/gi.test(arg0)) { character.hitModifier = calculateRoll(arg0) } else if (!isNaN(arg0)) { character.hitModifier = parseInt(arg0) } else { state.show = "none" return "\n[Error: Not a number. See #help]\n" } var possessiveName = getPossessiveName(character.name) state.show = "none" return `\n[${possessiveName} proficiency is set to ${character.hitModifier}]\n` } function doReset(command) { state.notes = [] state.characters = [] state.locations = [] state.location = null state.enemies = null state.initiativeOrder = [] state.x = null state.y = null state.defaultDifficulty = null state.autoXp = null state.day = null state.show = "reset" return " " } function doHelp(command) { state.show = "help" return " " } modifier(text)