From 0e80d726b44a2e21c05accbfe2a05b5face86007 Mon Sep 17 00:00:00 2001 From: raeleus Date: Sun, 15 Sep 2024 22:36:10 -0700 Subject: [PATCH] Initial commit --- .gitattributes | 2 + Context.js | 5 + Input.js | 1625 ++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 + Library.js | 399 ++++++++++++ Output.js | 300 +++++++++ README.md | 2 + 7 files changed, 2354 insertions(+) create mode 100644 .gitattributes create mode 100644 Context.js create mode 100644 Input.js create mode 100644 LICENSE create mode 100644 Library.js create mode 100644 Output.js create mode 100644 README.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/Context.js b/Context.js new file mode 100644 index 0000000..2304c34 --- /dev/null +++ b/Context.js @@ -0,0 +1,5 @@ +const modifier = (text) => { + return { text } +} + +modifier(text) \ No newline at end of file diff --git a/Input.js b/Input.js new file mode 100644 index 0000000..0ac1139 --- /dev/null +++ b/Input.js @@ -0,0 +1,1625 @@ +const rollSynonyms = ["roll"] +const createSynonyms = ["create", "generate", "start", "begin", "setup", "party", "member", "new"] +const bioSynonyms = ["bio", "biography", "summary", "character", "charactersheet", "statsheet", "abilities", "showabilities"] +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", "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"] +const showNotesSynonyms = ["notes", "shownotes", "viewnotes"] +const noteSynonyms = ["note", "takenote"] +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"] +const giveSynonyms = ["give", "handover", "hand", "gift"] +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"] +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"] +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 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.initialized == null || !text.includes("#")) { + state.initialized = true; + return { text } + } + + state.characterName = getCharacterName(rawText) + text = sanitizeText(text) + + command = text.substring(text.search(/#/) + 1) + var commandName = getCommandName(command).toLowerCase().replaceAll(/[^a-z0-9]*/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 with 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, 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 with 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, 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, helpSynonyms, doHelp) + + 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 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 + } + } + if (state.characters == null) state.characters = [] + if (state.notes == null) state.notes = [] + if (state.autoXp == null) state.autoXp = 0 + state.show = null + state.prefix = null + state.critical = null +} + +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 roll = calculateRoll(dice) + if (rollType == "advantage") roll = Math.max(roll, calculateRoll(dice)) + if (rollType == "disadvantage") roll = Math.min(roll, calculateRoll(dice)) + + 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!" + + 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.spellStat = null + state.tempCharacter.meleeStat = "Strength" + state.tempCharacter.rangedStat = "Dexterity" + + state.show = "create" + return " " +} + +function doBio(command) { + state.show = "bio" + return " " +} + +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} ${arg0} stat 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 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) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + 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) { + character.skills.push(skill) + } else { + var existingSkill = character.skills[index] + existingSkill.modifier = parseInt(skill.modifier) + } + + state.show = "none" + return `\n[${possessiveName} ${arg0} skill is now ${arg1}.]\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" + } + + var possessiveName = getPossessiveName(character.name) + + character.experience = 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 = parseInt(arg0) + + var possessiveName = getPossessiveName(character.name) + + var level = getLevel(character.experience) + character.experience += arg0 + var newLevel = getLevel(character.experience) + + if (newLevel > level) { + state.show = "none" + return `\n[${possessiveName} experience is increased to ${character.experience}. LEVEL UP! Level: ${newLevel}, Health Max: ${getHealthMax()}]\n` + } + + state.show = "none" + return `\n[${possessiveName} experience is increased to ${character.experience}.]\n` +} + +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" + } + arg0 = parseInt(arg0) + + var haveWord = character.name == "You" ? "have" : "has" + var areWord = character.name == "You" ? "are" : "is" + + if (character.health >= getHealthMax()) { + state.show = "none" + return `\n[${character.name} ${areWord} at maximum health]\n` + } + + character.health = character.health + arg0 + character.health = clamp(character.health, 0, getHealthMax()) + + state.show = "none" + return `\n[${character.name} ${haveWord} been healed by ${arg0} hp to ${character.health} health]\n` +} + +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 haveWord = character.name == "You" ? "have" : "has" + + character.health -= arg0 + character.health = clamp(character.health, 0, getHealthMax()) + + state.show = "none" + return `\n[${character.name} ${haveWord} been damaged for ${arg0} hp with ${character.health} remaining].${character.health == 0 ? " You are unconcious" : ""}\n` +} + +function doRest(command) { + var commandName = getCommandName(command) + state.characters.forEach(function(character) { + if (commandName.toLowerCase() == "shortrest") { + var max = getHealthMax(character) + character.health += Math.floor(max / 2) + if (character.health > max) character.health = max + } else { + character.health = getHealthMax(character) + } + + state.show = "none" + }) + return `\n[All characters have rested and feel rejuvinated]\n` +} + +function doCheck(command) { + const advantageNames = ["normal", "advantage", "disadvantage"] + const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless"] + const difficultyScores = [30, 25, 20, 15, 10, 5] + 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 = "easy" + 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: ${target}. roll: ${dieText}. Critical Success!]\n` + else if (score == 1) text = `\n[${arg0} check: ${target}. roll: ${dieText}. Critical Failure!]\n` + else if (modifier != 0) text = `\n[${arg0} check: ${target}. roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n` + else text = `\n[${arg0} check: ${target}. roll: ${dieText}. ${score >= target ? "Success!" : "Failure!"}]\n` + return text +} + +function doTry(command) { + const advantageNames = ["normal", "advantage", "disadvantage"] + const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless"] + const difficultyScores = [30, 25, 20, 15, 10, 5] + 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 = "easy" + 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: ${target}. roll: ${dieText}]\n` + else if (score == 1) state.prefix = `\n[${arg0} check: ${target}. roll: ${dieText}]\n` + else if (modifier != 0) state.prefix = `\n[${arg0} check: ${target}. roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n` + else state.prefix = `\n[${arg0} check: ${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 of 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"] + const difficultyScores = [30, 25, 20, 15, 10, 5] + var character = getCharacter() + var textIndex = 3 + var missWord = character.name == "You" ? "miss" : "misses" + var tryWord = character.name == "You" ? "try" : "tries" + + var statText = null + statText = searchArgument(command, /ranged/gi) + if (statText == null) { + statText = character.meleeStat + textIndex-- + } else if (statText.toLowerCase() == "ranged") statText = character.rangedStat + statText = toTitleCase(statText) + + var advantageText = searchArgument(command, arrayToOrPattern(advantageNames)) + if (advantageText == null) { + advantageText = "normal" + textIndex-- + } + else advantageText = advantageText.toLowerCase() + + const difficultyPatternNames = [...new Set(difficultyNames)] + difficultyPatternNames.push("\\d+") + var difficultyText = searchArgument(command, arrayToOrPattern(difficultyPatternNames)) + if (difficultyText == null) { + difficultyText = "easy" + textIndex-- + } + else difficultyText = difficultyText.toLowerCase() + + var targetText = getArgumentRemainder(command, textIndex) + var toMatches = targetText.match(/^to\s+/gi) + if (toMatches != null) targetText = targetText.substring(toMatches[0].length) + if (!/^.*(\.|!|\?)$/gi.test(targetText)) 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()) + 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 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${character.name} successfully hit the ${targetText}!` + else text = `\n${character.name} ${tryWord} to hit the ${targetText}. ${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!" + + log(`targetRoll:${targetRoll}`) + log(`autoXp:${state.autoXp}`) + 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 return doShowNotes(command) +} + +function doShowNotes(command) { + state.show = "showNotes" + return " " +} + +function doTake(command) { + var arg0 = getArgument(command, 0) + if (arg0 == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + const item = { + quantity: isNaN(arg0) ? 1 : arg0, + name: getArgumentRemainder(command, isNaN(arg0) ? 0 : 1).replace(/^((the)|(a)|(an))\s/, "").plural(true) + } + + var commandName = getCommandName(command) + var character = getCharacter() + 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} ${commandName} the ${displayItemName}.\n` + else text += `${character.name} ${commandName} ${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 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" + + const item = { + quantity: !isNaN(arg0) ? arg0 : allSynonyms.indexOf(arg0) > - 1 ? Number.MAX_SAFE_INTEGER : 1, + name: getArgumentRemainder(command, isNaN(arg0) ? 0 : 1).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 (item.quantity == 1) text = `\n${character.name} ${commandName} the ${displayItemName}.\n` + else if (item.quantity >= existingItem.quantity) text = `${character.name} ${commandName} all of the ${displayItemName}.` + else text = `\n${character.name} ${commandName} ${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 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 { + characterQuantityText = ` ${character.name} ${dontWord} have any more.` + } + } + + if (item.quantity == 1) text += `${character.name} ${commandName.plural(character.name == "You")} ${otherCharacter.name} the ${displayItemName}.` + else text += `${character.name} ${commandName.plural(character.name == "You")} ${otherCharacter.name} ${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] + + var sellQuantity + if (isNaN(args[1])) { + sellQuantity = 1 + } else { + sellQuantity = args[1] + args.splice(1, 1) + } + + var sellName = args[1] + + 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 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 ${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((element) => element == arg0) + if (found == null) return `\n[${character.name} ${tryWord} to forget the spell ${arg0}, but ${character.name} ${dontWord} even know it]\n` + + var index = character.spells.findIndex((element) => element.toLowerCase() == arg0.toLowerCase()) + character.spells.splice(index, 1) + + 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"] + const difficultyScores = [30, 25, 20, 15, 10, 5] + var character = getCharacter() + const dontWord = character.name == "You" ? "don't" : "doesn't" + const tryWord = character.name == "You" ? "try" : "tries" + + var spellIndex = 2; + + var advantage = searchArgument(command, arrayToOrPattern(advantageNames)) + if (advantage == null) { + advantage = "normal" + spellIndex-- + } + + const difficultyPatternNames = [...new Set(difficultyNames)] + difficultyPatternNames.push("\\d+") + var difficulty = searchArgument(command, arrayToOrPattern(difficultyPatternNames)) + if (difficulty == null) { + difficulty = "easy" + 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 target = null + var atWord = null + + var found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase()) + if (found != null) { + target = getArgumentRemainder(command, spellIndex + 1) + if (target != null) { + target = target.trim() + if (!/^((at)|(on))\s+.*/.test(target)) target = "at " + target + } + } else { + var remainder = getArgumentRemainder(command, spellIndex) + spell = remainder.replace(/\s+((at)|(on)).*/i, "").trim() + found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase()) + + target = remainder.replace(/^.*\s+(?=(at)|(on))/i, "").trim() + } + + if (found == null) { + state.show = "none" + return `\n[${character.name} ${tryWord} to cast the spell ${spellName}, but ${character.name == "You" ? "you" : character.name} ${dontWord} know it.]\n` + } + + var text = `${character.name} cast the spell ${spell}${advantage != "normal" ? " with " + advantage : ""}${target == null ? "" : " " + target}.` + + 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 + + state.show = "prefix" + var dieText = advantage == "advantage" || advantage == "disadvantage" ? `${advantage}(${roll1},${roll2})` : roll1 + if (roll == 20) state.prefix = `\n[Difficulty: ${difficulty}. Roll: ${dieText}. Critcal Success!]\n` + else if (roll == 1) state.prefix = `\n[Difficulty: ${difficulty}. Roll: ${dieText}. Critcal Failure!]\n` + else if (modifier != 0) state.prefix = `\n[Difficulty: ${difficulty}. Roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${roll + modifier}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n` + else state.prefix = `\n[Difficulty: ${difficulty}. Roll: ${dieText}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n` + + if (roll == 20) text += ` Critical success!` + else if (roll == 1) ` Critical failure! The spell ${target != null ? "misses" : "fails"} in a spectacular way.` + else if (roll + modifier >= difficulty) text += ` The spell ${target != null ? "hits the target" : "is successful"}!` + else text += ` The spell ${target != null ? "misses" : "fails"}!` + + 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 = getArgument(command, 0) + if (arg0 == null) arg0 = 1 + + arg0 = parseInt(arg0) - 1 + if (arg0 >= state.notes.length) { + state.show = "none" + return "\n[Error: Note does not exist. Call #showNotes.]\n" + } + + state.notes.splice(arg0, 1) + state.show = "none" + return `[Note #${arg0 + 1} removed]` +} + +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 doClearSpells(command) { + var character = getCharacter() + character.spells = [] + state.show = "clearInventory" + 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 doReset(command) { + state.notes = [] + state.characters = [] + + state.show = "reset" + return " " +} + +function doHelp(command) { + state.show = "help" + return " " +} + +modifier(text) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7504052 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 raeleus + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Library.js b/Library.js new file mode 100644 index 0000000..de4105b --- /dev/null +++ b/Library.js @@ -0,0 +1,399 @@ +function getRandomInteger(min, max) { + return Math.floor(Math.random() * (max - min + 1) ) + min; +} + +function sanitizeText(text) { + if (/^\s*>.*says? ".*/.test(text)) { + text = text.replace(/^\s*>\s/, "") + text = text.replace(/says? "/, "") + text = text.replace(/"\n$/, "") + } else if (/^\s*>\s.*/.test(text)) { + text = text.replace(/^\s*>\s/, "") + text = text.replace(/\.?\n$/, "") + } + + return text +} + +function getCharacterName(rawText) { + var matches = rawText.match(/(?<=\s+> ).*(?=(\s+#)|( says? "))/) + if (matches != null && matches[0].trim() != "") { + return matches[0].trim() + } + + matches = rawText.match(/.*(?= #)/) + if (matches != null && matches[0].trim() != "") { + return matches[0].trim() + } + + return null +} + +function getPossessiveName(name) { + var possesiveName = "Your" + if (name != "You") { + possesiveName = name + if (name.endsWith("s")) possesiveName += "'" + else possesiveName += "'s" + } + return possesiveName +} + +function getCommandName(command) { + var args = getArguments(command) + if (args.length == 0) return null + return args[0] +} + +const argumentPattern = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|$)|(?:\\\s|\S)+)/g + +function getArguments(command) { + var matches = command.match(new RegExp(argumentPattern)) + var returnValue = [] + matches.forEach(match => { + match = match.replaceAll(/(^")|("$)/g, "").replaceAll(/\\"/g, '"') + returnValue.push(match) + }) + return returnValue +} + +function getArgument(command, index) { + var args = getArguments(command) + index++ + if (index >= args.length) return null + return args[index] +} + +function getArgumentRemainder(command, index) { + var counter = 0 + + const pattern = new RegExp(argumentPattern) + while ((match = pattern.exec(command)) != null) { + if (counter++ == index + 1) { + return command.substring(match.index).replace(/^"/, "").replace(/"$/, "").replaceAll(/\\"/g, '"') + } + } +} + +function searchArgument(command, pattern) { + var index = searchArgumentIndex(command, pattern) + if (index == -1) return null + return getArgument(command, index) +} + +function searchArgumentIndex(command, pattern) { + var args = getArguments(command) + if (args.length <= 1) return -1 + args.splice(0, 1) + + const search = (element) => pattern.test(element) + var index = args.findIndex(search) + if (index != -1) return index + return -1 +} + +function arrayToOrPattern(array) { + var pattern = "^" + array.forEach(element => { + pattern += `(${element})|` + }) + pattern += pattern.substring(0, pattern.length - 1) + pattern += "$" + return new RegExp(pattern, "gi") +} + +function statsToOrPattern(stats) { + var array = [] + stats.forEach(element => { + array.push(element.name) + }) + return arrayToOrPattern(array) +} + +function getDice(rolltext) { + var matches = rolltext.match(/\d+(?=d)/) + if (matches != null) { + return matches[0] + } + return 1 +} + +function getSides(rolltext) { + var matches = rolltext.match(/(?<=d)\d+/) + if (matches != null) { + return matches[0] + } + + return 20 +} + +function formatRoll(text) { + var matches = text.match(/(?<=.*)\d*d\d+(?=.*)/) + if (matches != null) { + return matches[0] + } + + matches = text.match(/\d+/) + if (matches != null) { + return "d" + matches[0] + } + + return "d20" +} + +function calculateRoll(rolltext) { + rolltext = rolltext.toLowerCase() + + var dice = getDice(rolltext) + var sides = getSides(rolltext) + + var score = 0; + for (i = 0; i < dice; i++) { + score += getRandomInteger(1, sides) + } + + return score +} + +function getCharacter(characterName) { + if (characterName == null) characterName = state.characterName + return state.characters.find((element) => element.name.toLowerCase() == characterName.toLowerCase()) +} + +function hasCharacter(characterName) { + return getCharacter(characterName) != null +} + +function createCharacter(name) { + var existingCharacter = getCharacter(name) + if (existingCharacter != null) { + existingCharacter.name = name + existingCharacter.className = "adventurer" + existingCharacter.summary = "An auto generated character. Use #create to create this character" + existingCharacter.inventory = [] + existingCharacter.spells = [] + existingCharacter.stats = [] + existingCharacter.spellStat = null + existingCharacter.meleeStat = null + existingCharacter.rangedStat = null + existingCharacter.skills = [] + existingCharacter.experience = 0 + existingCharacter.health = 10 + return existingCharacter + } + + var character = { + name: name, + className: "adventurer", + summary: "An auto generated character. Use #create to create this character", + inventory: [], + spells: [], + stats: [], + spellStat: null, + meleeStat: null, + rangedStat: null, + skills: [], + experience: 0, + health: 10 + } + state.characters.push(character) + return character +} + +function deleteCharacter(name) { + var index = state.characters.findIndex((element) => element.name == name) + state.characters.splice(index, 1) +} + +const levelSplits = [0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000] + +function getLevel(experience) { + if (experience < 0) experience = 0 + + var level + for (level = 0; level < levelSplits.length; level++) { + if (experience < levelSplits[level]) break + } + return level +} + +function getNextLevelXp(experience) { + if (experience < 0) experience = 0 + + var level + for (level = 0; level < levelSplits.length; level++) { + if (experience < levelSplits[level]) return levelSplits[level] + } + return -1 +} + +function addXpToAll(experience) { + if (experience == 0) return "" + var leveledUp = `\n[The party has gained ${experience} experience!]` + state.characters.forEach(x => { + const oldLevel = getLevel(x.experience) + x.experience += experience + const newLevel = getLevel(x.experience) + if (newLevel > oldLevel) leveledUp += `\n[${x.name} has leveled up to ${newLevel}!]` + }) + return leveledUp +} + +function getHealthMax(character) { + if (character == null) character = getCharacter() + + var modifier = 0 + var stat = character.stats.find((element) => element.name.toLowerCase() == "constitution") + if (stat != null) modifier = getModifier(stat.value) + + var level = getLevel(character.experience) + return 10 + level * (6 + modifier) +} + +function getModifier(statValue) { + return Math.floor((statValue - 10) / 2) +} + +function findSpellCardIndex(name) { + return storyCards.findIndex((element) => element.type == "spell" && element.keys == name) +} + +String.prototype.plural = function(revert) { + + var plural = { + '(quiz)$' : "$1zes", + '^(ox)$' : "$1en", + '([m|l])ouse$' : "$1ice", + '(matr|vert|ind)ix|ex$' : "$1ices", + '(x|ch|ss|sh)$' : "$1es", + '([^aeiouy]|qu)y$' : "$1ies", + '(hive)$' : "$1s", + '(?:([^f])fe|([lr])f)$' : "$1$2ves", + '(shea|lea|loa|thie)f$' : "$1ves", + 'sis$' : "ses", + '([ti])um$' : "$1a", + '(tomat|potat|ech|her|vet)o$': "$1oes", + '(bu)s$' : "$1ses", + '(alias)$' : "$1es", + '(octop)us$' : "$1i", + '(ax|test)is$' : "$1es", + '(us)$' : "$1es", + '([^s]+)$' : "$1s" + }; + + var singular = { + '(quiz)zes$' : "$1", + '(matr)ices$' : "$1ix", + '(vert|ind)ices$' : "$1ex", + '^(ox)en$' : "$1", + '(alias)es$' : "$1", + '(octop|vir)i$' : "$1us", + '(cris|ax|test)es$' : "$1is", + '(shoe)s$' : "$1", + '(o)es$' : "$1", + '(bus)es$' : "$1", + '([m|l])ice$' : "$1ouse", + '(x|ch|ss|sh)es$' : "$1", + '(m)ovies$' : "$1ovie", + '(s)eries$' : "$1eries", + '([^aeiouy]|qu)ies$' : "$1y", + '([lr])ves$' : "$1f", + '(tive)s$' : "$1", + '(hive)s$' : "$1", + '(li|wi|kni)ves$' : "$1fe", + '(shea|loa|lea|thie)ves$': "$1f", + '(^analy)ses$' : "$1sis", + '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$': "$1$2sis", + '([ti])a$' : "$1um", + '(n)ews$' : "$1ews", + '(h|bl)ouses$' : "$1ouse", + '(corpse)s$' : "$1", + '(us)es$' : "$1", + 's$' : "" + }; + + var irregular = { + 'move' : 'moves', + 'foot' : 'feet', + 'goose' : 'geese', + 'sex' : 'sexes', + 'child' : 'children', + 'man' : 'men', + 'tooth' : 'teeth', + 'person' : 'people', + 'woman' : 'women', + }; + + var uncountable = [ + 'sheep', + 'fish', + 'deer', + 'moose', + 'series', + 'species', + 'money', + 'rice', + 'information', + 'equipment', + 'gold', + 'bass', + 'milk', + 'food', + 'water', + 'bread', + 'sugar', + 'tea', + 'cheese', + 'coffee', + 'currency', + 'seafood', + 'oil', + 'software' + ]; + + // save some time in the case that singular and plural are the same + if(uncountable.indexOf(this.toLowerCase()) >= 0) + return this; + + // check for irregular forms + for(word in irregular){ + + if(revert){ + var pattern = new RegExp(irregular[word]+'$', 'i'); + var replace = word; + } else{ var pattern = new RegExp(word+'$', 'i'); + var replace = irregular[word]; + } + if(pattern.test(this)) + return this.replace(pattern, replace); + } + + if(revert) var array = singular; + else var array = plural; + + // check for matches using regular expressions + for(reg in array){ + + var pattern = new RegExp(reg, 'i'); + + if(pattern.test(this)) + return this.replace(pattern, array[reg]); + } + + return this; +} + +function clamp(num, min, max) { + return num <= min + ? min + : num >= max + ? max + : num +} + +function toTitleCase(str) { + return str.replace( + /\w\S*/g, + text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase() + ); +} \ No newline at end of file diff --git a/Output.js b/Output.js new file mode 100644 index 0000000..2b61415 --- /dev/null +++ b/Output.js @@ -0,0 +1,300 @@ +const modifier = (text) => { + if (state.show == null) return { text } + + if (state.characterName == null) { + text = " " + return { text } + } + var character = getCharacter() + var possessiveName = character == null ? null : getPossessiveName(character.name) + var type = history[history.length - 1].type + const originalText = text + text = type != "story" ? "" : history[history.length - 1].text.endsWith("\n") ? "" : "\n" + + switch (state.show) { + case "create": + switch (state.createStep) { + case 0: + text += `***CHARACTER CREATION***\nCharacter: ${state.tempCharacter.name}\nWould you like to use a prefab character? (y/n/q to quit)\n` + break + case 1: + text += `What class is your character?\n` + break + case 2: + text += `You rolled the following stat dice: ${state.statDice}\nChoose your abilities in order from highest to lowest\n1. Strength: Physical power and endurance\n2. Dexterity: Agility and coordination\n3. Constitution: Toughness and physique \n4. Intelligence: Reasoning and memory\n5. Wisdom: Judgement and insight\n6. Charisma: Force of personality and persuasiveness\n\nEnter the numbers with spaces between or q to quit.\n` + break + case 3: + text += `What ability is your spell casting ability?\n1. Intelligence\n2. Wisdom\n3. Charisma\n4. Not a spell caster\nq to quit\n` + break + case 4: + text += `Enter a short summary about your character or q to quit\n` + break + case 100: + text += `What character will you choose?\n1. Fighter: A skilled melee warrior specializing in weapons and armor.\n2. Cleric: A follower of a deity that can call on divine power.\n3. Rogue: An expert in stealth, subterfuge, and exploitation.\n4. Ranger: A talented hunter adept in tracking, survival, and animal handling.\n5. Barbarian: Combat expert focused on brute strength and raw fury.\n6. Bard: A musician that can transform song and word into magic.\n7. Druid: Commands the natural world to cast spells and harness its power.\n8. Monk: A martial artist who has mastered melee and unarmed combat.\n9. Paladin: A virtuous holy warrior with expertise in armor and mysticism.\n10. Wizard: An expert in magic ability who found their power through arcane knowledge.\n11. Sorcerer: A masterful spellcaster deriving their power from an innate source.\n12. Warlock: A magic user granted ability by a pact with a powerful patron.\n13. Artificer: An inventor and alchemist capable of imbuing objects with magic.\n\nEnter the number or q to quit.\n` + break + case 500: + text += `${state.tempCharacter.name} the ${state.tempCharacter.className} has been created.\nType #bio to see a summary of your character.\n***********\n` + break; + case null: + text += `[Character creation has been aborted!]\n` + break + } + break + case "bio": + text += `*** ${possessiveName.toUpperCase()} BIO ***\n` + text += `Class: ${character.className}\n` + text += `Health: ${character.health}/${getHealthMax()}\n` + text += `Experience: ${character.experience}\n` + text += `Level: ${getLevel(character.experience)}\n` + var nextLevel = getNextLevelXp(character.experience) + text += `Next level at: ${nextLevel == - 1 ? "(at maximum)": nextLevel + " xp"}\n\n` + text += `-ABILITIES-\n` + + character.stats.forEach(function(x) { + text += `* ${x.name} ${x.value}\n` + }) + + text += `----\n\n` + + text += `-SKILLS-\n` + + character.skills.forEach(function(x) { + const stat = character.stats.find(y => y.name.toLowerCase() == x.stat.toLowerCase()) + var modifier = x.modifier + (stat != null ? getModifier(stat.value): 0) + if (modifier >= 0) modifier = `+${modifier}` + text += `* ${toTitleCase(x.name)} (${x.stat}) ${modifier}\n` + }) + + text += `----\n\n` + + text += `Melee Ability: ${character.meleeStat == null ? "none" : character.meleeStat}\n\n` + text += `Ranged Ability: ${character.rangedStat == null ? "none" : character.rangedStat}\n\n` + text += `Spellcasting Ability: ${character.spellStat == null ? "none" : character.spellStat}\n\n` + + if (character.spellStat != null) { + text += `-SPELLS-\n` + + character.spells.forEach(function(x) { + text += `* ${x}\n` + }) + + text += `----\n\n` + } + + text += `-INVENTORY-\n` + + character.inventory.forEach(function(x) { + text += `* ${x.quantity} ${toTitleCase(x.name.plural(x.quantity == 1))}\n` + }) + + text += `----\n\n` + + text += `Summary: ${character.summary}\n\n` + + text += `**************\n\n` + break + case "showNotes": + text += "*** NOTES ***" + var counter = 1 + state.notes.forEach(function(x) { + text += `\n${counter++}. ${x}` + }) + text += "\n**************\n\n" + break + case "clearNotes": + text += "[Notes cleared successfully]\n" + break + case "inventory": + text += `*** ${possessiveName.toUpperCase()} INVENTORY ***` + if (character.inventory.length > 0) { + character.inventory.forEach(function(x) { + text += `\n* ${x.quantity} ${toTitleCase(x.name.plural(x.quantity == 1))}` + }) + } else { + text += `\n${possessiveName} inventory is empty!` + } + text += "\n******************\n\n" + break + case "characters": + text += `*** CHARACTERS ***` + if (state.characters.length > 0) { + state.characters.forEach(function(x) { + text += `\n* ${x.name} the ${x.className}: ${x.summary}` + }) + } else { + text += `\n${possessiveName} inventory is empty!` + } + text += "\n******************\n\n" + break + case "spellbook": + text += `*** ${possessiveName.toUpperCase()} SPELLBOOK ***` + if (character.spells.length > 0) { + character.spells.forEach(function(x) { + text += "\n* " + x + }) + } else { + text += `\n${possessiveName} spellbook is empty!` + } + text += "\n******************\n\n" + break + case "stats": + text += `*** ${possessiveName.toUpperCase()} ABILITIES ***\n` + if (character.stats.length > 0) { + character.stats.forEach(function(x) { + text += `* ${x.name} ${x.value}\n` + }) + } else { + text += `${character.name} has no abilities!\n` + } + text += "******************\n\n" + break + case "skills": + text += `*** ${possessiveName.toUpperCase()} SKILLS ***\n` + if (character.skills.length > 0) { + character.skills.forEach(function(x) { + const stat = character.stats.find(y => y.name.toLowerCase() == x.stat.toLowerCase()) + var modifier = x.modifier + (stat != null ? getModifier(stat.value): 0) + if (modifier >= 0) modifier = `+${modifier}` + text += `* ${toTitleCase(x.name)} (${x.stat}) ${modifier}\n` + }) + } else { + text += `${character.name} has no skills!\n` + } + text += "******************\n\n" + break + case "none": + text += " " + break + case "prefix": + text = state.prefix + originalText + break + case "clearInventory": + text += `[${possessiveName} inventory has been emptied]\n` + break + case "reset": + text += "[All settings have been reset]\n" + break + case "help": + + text += "--Basic Hashtags--" + text += "\n#roll (advantage|disadvantage) (dice_value)" + text += "\n Rolls a die/dice and shows the result. dice_value can be in the following formats 5d20 or d20 or 20" + text += "\n#shownotes" + text += "\n Shows all the notes." + text += "\n#note message" + text += "\n Adds the specified message as a note." + text += "\n#clearnotes" + text += "\n Removes all notes." + text += "\n#removenote value" + text += "\n Removes the specified note as indicated by the number listed in #shownotes." + + text += "\n\n--Characters--" + text += "\n#setup" + text += "\n Launches the create character setup." + text += "\n#bio" + text += "\n Shows the character's abilities, skills, spells, inventory, and everything else about this character." + text += "\n#setclass" + text += "\n Sets the class of the character for player reference." + text += "\n#setsummary" + text += "\n Sets the summary of the character for player reference." + text += "\n#sethealth value" + text += "\n Sets the character's health to specified value. It's capped at the character's max health." + text += "\n#heal value" + text += "\n Increases the character's health by the specified value. It's capped at the character's max health." + text += "\n#damage value" + text += "\n Decreases the character's health by the specified value. Reaching 0 causes the character to become \"unconcious\"." + text += "\n#rest" + text += "\n Sets all of the characters' health to their maximums. Use #shortrest to only restore half health." + text += "\n#setxp value" + text += "\n Sets the character's experience to the specified value." + text += "\n#addxp value" + text += "\n Increases the character's experience by the specified value. The player is notified if there is a level up." + text += "\n#setautoxp value" + text += "\n Automatically increases the experience of all party members when a #try, #attack, or #cast is called. The amount of experience is scaled based on the difficulty of the check with any check 20 or higher will result in the maximum specified by value. Set to 0 to disable." + text += "\n#showautoxp" + text += "\n Shows the value of the auto xp." + text += "\n#levelup" + text += "\n Increases the character's experience by the exact amount needed to reach the next level." + text += "\n#showcharacters" + text += "\n Lists all current characters and their classes/summaries." + text += "\n#removecharacter name" + text += "\n Removes the character that has the indicated name." + + text += "\n\n--Character Checks--" + text += "\n#check (ability|skill) (advantage|disadvantage) (difficulty_value or effortless|easy|medium|hard|impossible)" + text += "\n Rolls a d20 and compares the result (modified by the character's ability/skill) to the specified difficulty" + text += "\n#try (ability|skill) (advantage|disadvantage) (difficulty_value or effortless|easy|medium|hard|impossible) task" + text += "\n Attempts to do the task based on the character's ability/skill against the specified difficulty." + text += "\n#attack (ranged) (advantage|disadvantage) (ac or effortless|easy|medium|hard|impossible) target" + text += "\n Attacks the specified target with a melee (the default) or ranged attack. The roll is compared against the specified AC which will determine if the attack succeeds or misses." + text += "\n#cast (advantage|disadvantage) (difficulty_value or effortless|easy|medium|hard|impossible) spell(target)" + text += "\n Character will cast the indicated spell if the spell is in their spellbook. It will be a targeted spell if a target is indicated. The roll is modified by the spell casting ability of the character. You may type a phrase without quotes for spell such as \"cast fire bolt at the giant chicken\"" + + text += "\n\n--Abilities--" + text += "\n#setability ability value" + text += "\n Adds the ability to the character if necessary and sets it to the specified value." + text += "\n#showabilities" + text += "\n Shows the character's list of abilities." + text += "\n#removeability ability" + text += "\n Removes the ability from the character's list of abilities." + text += "\n#clearabilities" + text += "\n Removes all abilities from the character." + text += "\n#setspellability ability" + text += "\n Sets the ability that affects the modifier for #cast." + text += "\n#setmeleeability ability" + text += "\n Sets the character's ability modifier that affects melee attacks." + text += "\n#setrangedability ability" + text += "\n Sets the character's ability modifier that affects ranged attacks." + + text += "\n\n--Skills--" + text += "\n#setskill skill ability value" + text += "\n Adds the skill to the character if necessary, and associates it with the specified ability and value." + text += "\n#showskills" + text += "\n Shows the character's list of skills" + text += "\n#removeskill" + text += "\n Removes the skill from the character's list of skills." + text += "\n#clearskills" + text += "\n Removes all skills from the character." + + text += "\n\n--Inventory--" + text += "\n#take (quantity) item" + text += "\n Adds the specified quantity of item to the character's inventory. If a quantity is omitted, it's assumed to be 1. The words the, a, and an are ignored." + text += "\n#buy (buy_quantity) buy_item (sell_quantity) sell_item" + text += "\n Adds the specified buy_quantity of the buy_item to the character's inventory and also removes the sell_quantity of sell_item. If quantities are omitted, they are assumed to be 1. Quotes are necessary for items with spaces. The words for, with, the, a, and an are ignored." + text += "\n#sell (sell_quantity) sell_item (buy_quantity) buy_item" + text += "\n Just like #buy, but with the parameters reversed. Adds the specified buy_quantity of the buy_item to the character's inventory and also removes the sell_quantity of sell_item. If quantities are omitted, they are assumed to be 1. The words for, with, the, a, and an are ignored." + text += "\n#drop (quantity or all|every) item" + text += "\n Removes the specified quantity of item from the character's inventory. If a quantity is omitted, it's assumed to be 1. The words the, a, and an are ignored." + text += "\n#give other_character (quantity or all|every) item" + text += "\n Removes the quantity of item from the character's inventory and adds it to the other_character's inventory. If a quantity is omitted, it's assumed to be 1. The words the, a, and an are ignored." + text += "\n#inventory" + text += "\n Shows the items in the inventory of the character." + text += "\n#clearinventory" + text += "\n Removes all items from the character's inventory." + + text += "\n\n--Spells--" + text += "\n#learnspell spell" + text += "\n Adds the specified spell to the character's spellbook. Creates a story card if necessary." + text += "\n#forgetSpell" + text += "\n Removes the specified spell from the character's spellbook" + text += "\n#clearspells" + text += "\n Removes all spells from the character's spellbook." + text += "\n#spellbook" + text += "\n Shows the list of spells that the character has learned." + + text += "\n\n--Danger Zone--" + text += "\n#reset" + text += "\n Removes all characters and changes all settings to their defaults. Use with caution!" + + text += "\n\n--Other--" + text += "\n#help" + text += "\n This long ass help menu. I am paid by lines of codes." + + break + } + + state.show = null + return { text } +} + +modifier(text) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..97daa38 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Hashtag-DnD + Scenario script for AI Dungeon