diff --git a/Context.js b/Context.js index 2304c34..9faa434 100644 --- a/Context.js +++ b/Context.js @@ -1,4 +1,5 @@ const modifier = (text) => { + [text, stop] = AutoCards("context", text, stop); return { text } } diff --git a/Input.js b/Input.js index 55311e9..aba6921 100644 --- a/Input.js +++ b/Input.js @@ -1,4 +1,4 @@ -const version = "Hashtag DnD v0.4.0" +const version = "Hashtag DnD v0.8.0" const rollSynonyms = ["roll"] const createSynonyms = ["create", "generate", "start", "begin", "setup", "party", "member", "new"] const renameCharacterSynonyms = ["renamecharacter", "renameperson"] @@ -29,6 +29,8 @@ const noteSynonyms = ["note", "takenote", "setnote", "createnote", "remember"] const clearNotesSynonyms = ["clearnotes"] const eraseNoteSynonyms = ["erasenote", "removenote", "deletenote", "cancelnote"] const takeSynonyms = ["take", "steal", "get", "grab", "receive", "loot"] +const takeWeaponSynonyms = ["takeweapon", "stealweapon", "getweapon", "grabweapon", "receiveweapon", "lootweapon"] +const takeArmorSynonyms = ["takearmor", "stealarmor", "getarmor", "grabarmor", "receivearmor", "lootarmor"] const buySynonyms = ["buy", "purchase", "barter", "trade", "swap", "exchange"] const sellSynonyms = ["sell"] const dropSynonyms = ["remove", "discard", "drop", "leave", "dispose", "toss", "throw", "throwaway", "trash", "donate", "eat", "consume", "use", "drink", "pay", "lose"] @@ -68,15 +70,20 @@ const showDaySynonyms = ["showday", "showdate", "day", "date"] const setDaySynonyms = ["setday", "setdate"] const encounterSynonyms = ["encounter", "startencounter"] const showEnemiesSynonyms = ["showenemies", "enemies"] +const showAlliesSynonyms = ["showallies", "allies"] const addEnemySynonyms = ["addenemy"] +const addAllySynonyms = ["addally"] const removeEnemySynonyms = ["removeenemy"] +const removeAllySynonyms = ["removeally"] const clearEnemiesSynonyms = ["clearenemies", "resetenemies", "removeenemies"] +const clearAlliesSynonyms = ["clearallies", "resetallies", "removeallies"] const initiativeSynonyms = ["initiative"] const setAcSynonyms = ["setac", "setarmorclass", "ac", "armorclass"] const turnSynonyms = ["turn", "doturn", "taketurn"] const fleeSynonyms = ["flee", "retreat", "runaway", "endcombat"] const versionSynonyms = ["version", "ver", "showversion"] const setupEnemySynonyms = ["setupenemy", "createenemy"] +const setupAllySynonyms = ["setupally", "createally"] const setDamageSynonyms = ["setdamage"] const setProficiencySynonyms = ["setproficiency", "setweaponproficiency"] const healPartySynonyms = ["healparty", "healcharacters"] @@ -84,8 +91,14 @@ const blockSynonyms = ["block", "parry", "nullify", "invalidate"] const repeatTurnSynonyms = ["repeatturn", "repeat"] const basicDeckSynonyms = ["basicdeck", "stragedybasicdeck"] const cardShopSynonyms = ["cardshop", "stragedyshop", "cardstore", "stragedystore"] +const spellShopSynonyms = ["spellshop", "spellstore"] +const itemShopSynonyms = ["itemshop", "itemstore"] const stragedySynonyms = ["stragedy", "playgame", "game", "startgame", "begingame", "playcards", "playstragedy", "startstragedy", "beginstragedy"] +const lockpickSynonyms = ["lockpick", "lockpicking", "codebreaker", "pick", "hack", "hacking", "mastermind"] +const memorySynonyms = ["memory", "matchmaking", "matching", "matchmaker", "match2"] const addCardSynonyms = ["addcard"] +const equipSynonyms = ["equip", "arm", "wear"] +const rewardSynonyms = ["reward"] const helpSynonyms = ["help"] const modifier = (text) => { @@ -104,6 +117,12 @@ const modifier = (text) => { else text = rawText } + if (state.setupAllyStep != null) { + text = handleSetupAllyStep(text) + if (state.setupAllyStep != null) return { text } + else text = rawText + } + if (state.stragedyShopStep != null) { text = handleStragedyShopStep(text) if (state.stragedyShopStep != null) return { text } @@ -116,6 +135,30 @@ const modifier = (text) => { else text = rawText } + if (state.spellShopStep != null) { + text = handleSpellShopStep(text) + if (state.spellShopStep != null) return { text } + else text = rawText + } + + if (state.itemShopStep != null) { + text = handleItemShopStep(text) + if (state.itemShopStep != null) return { text } + else text = rawText + } + + if (state.lockpickingTurn != null) { + text = handleLockpickingTurn(text) + if (state.lockpickingTurn != null) return { text } + else text = rawText + } + + if (state.memoryTurn != null) { + text = handleMemoryTurn(text) + if (state.memoryTurn != null) return { text } + else text = rawText + } + if (state.initialized == null || !text.includes("#")) { state.initialized = true; return { text } @@ -145,7 +188,7 @@ const modifier = (text) => { return { text } } - if (!found) found = processCommandSynonyms(command, commandName, helpSynonyms.concat(rollSynonyms, noteSynonyms, eraseNoteSynonyms, showNotesSynonyms, clearNotesSynonyms, showCharactersSynonyms, removeCharacterSynonyms, generateNameSynonyms, setDefaultDifficultySynonyms, showDefaultDifficultySynonyms, renameCharacterSynonyms, cloneCharacterSynonyms, createLocationSynonyms, showLocationsSynonyms, goToLocationSynonyms, removeLocationSynonyms, getLocationSynonyms, clearLocationsSynonyms, goNorthSynonyms, goSouthSynonyms, goEastSynonyms, goWestSynonyms, encounterSynonyms, showEnemiesSynonyms, addEnemySynonyms, removeEnemySynonyms, clearEnemiesSynonyms, initiativeSynonyms, turnSynonyms, fleeSynonyms, versionSynonyms, setupEnemySynonyms, healSynonyms, damageSynonyms, restSynonyms, addExperienceSynonyms, healPartySynonyms, blockSynonyms, repeatTurnSynonyms, resetSynonyms), function () {return true}) + if (!found) found = processCommandSynonyms(command, commandName, helpSynonyms.concat(rollSynonyms, noteSynonyms, eraseNoteSynonyms, showNotesSynonyms, clearNotesSynonyms, showCharactersSynonyms, removeCharacterSynonyms, generateNameSynonyms, setDefaultDifficultySynonyms, showDefaultDifficultySynonyms, renameCharacterSynonyms, cloneCharacterSynonyms, createLocationSynonyms, showLocationsSynonyms, goToLocationSynonyms, removeLocationSynonyms, getLocationSynonyms, clearLocationsSynonyms, goNorthSynonyms, goSouthSynonyms, goEastSynonyms, goWestSynonyms, encounterSynonyms, showEnemiesSynonyms, showAlliesSynonyms, addEnemySynonyms, addAllySynonyms, removeEnemySynonyms, removeAllySynonyms, clearEnemiesSynonyms, clearAlliesSynonyms, initiativeSynonyms, turnSynonyms, fleeSynonyms, versionSynonyms, setupEnemySynonyms, setupAllySynonyms, healSynonyms, damageSynonyms, restSynonyms, addExperienceSynonyms, healPartySynonyms, blockSynonyms, repeatTurnSynonyms, lockpickSynonyms, memorySynonyms, resetSynonyms), function () {return true}) if (found == null) { if (state.characterName == null) { @@ -230,13 +273,18 @@ const modifier = (text) => { if (text == null) text = processCommandSynonyms(command, commandName, setAcSynonyms, doSetAc) if (text == null) text = processCommandSynonyms(command, commandName, encounterSynonyms, doEncounter) if (text == null) text = processCommandSynonyms(command, commandName, showEnemiesSynonyms, doShowEnemies) + if (text == null) text = processCommandSynonyms(command, commandName, showAlliesSynonyms, doShowAllies) if (text == null) text = processCommandSynonyms(command, commandName, removeEnemySynonyms, doRemoveEnemy) + if (text == null) text = processCommandSynonyms(command, commandName, removeAllySynonyms, doRemoveAlly) if (text == null) text = processCommandSynonyms(command, commandName, clearEnemiesSynonyms, doClearEnemies) + if (text == null) text = processCommandSynonyms(command, commandName, clearAlliesSynonyms, doClearAllies) if (text == null) text = processCommandSynonyms(command, commandName, addEnemySynonyms, doAddEnemy) + if (text == null) text = processCommandSynonyms(command, commandName, addAllySynonyms, doAddAlly) if (text == null) text = processCommandSynonyms(command, commandName, initiativeSynonyms, doInitiative) if (text == null) text = processCommandSynonyms(command, commandName, fleeSynonyms, doFlee) if (text == null) text = processCommandSynonyms(command, commandName, turnSynonyms, doTurn) if (text == null) text = processCommandSynonyms(command, commandName, setupEnemySynonyms, doSetupEnemy) + if (text == null) text = processCommandSynonyms(command, commandName, setupAllySynonyms, doSetupAlly) if (text == null) text = processCommandSynonyms(command, commandName, setDamageSynonyms, doSetDamage) if (text == null) text = processCommandSynonyms(command, commandName, setProficiencySynonyms, doSetProficiency) if (text == null) text = processCommandSynonyms(command, commandName, healPartySynonyms, doHealParty) @@ -244,8 +292,16 @@ const modifier = (text) => { if (text == null) text = processCommandSynonyms(command, commandName, repeatTurnSynonyms, doRepeatTurn) if (text == null) text = processCommandSynonyms(command, commandName, basicDeckSynonyms, doBasicDeck) if (text == null) text = processCommandSynonyms(command, commandName, cardShopSynonyms, doCardShop) - if (text == null) text = processCommandSynonyms(command, commandName, stragedySynonyms, doStragedy) + if (text == null) text = processCommandSynonyms(command, commandName, spellShopSynonyms, doSpellShop) + if (text == null) text = processCommandSynonyms(command, commandName, itemShopSynonyms, doItemShop) + if (text == null) text = processCommandSynonyms(command, commandName, stragedySynonyms, doStragedy) + if (text == null) text = processCommandSynonyms(command, commandName, lockpickSynonyms, doLockpick) + if (text == null) text = processCommandSynonyms(command, commandName, memorySynonyms, doMemory) if (text == null) text = processCommandSynonyms(command, commandName, addCardSynonyms, doAddCard) + if (text == null) text = processCommandSynonyms(command, commandName, equipSynonyms, doEquip) + if (text == null) text = processCommandSynonyms(command, commandName, rewardSynonyms, doReward) + if (text == null) text = processCommandSynonyms(command, commandName, takeWeaponSynonyms, doTakeWeapon) + if (text == null) text = processCommandSynonyms(command, commandName, takeArmorSynonyms, doTakeArmor) if (text == null) text = processCommandSynonyms(command, commandName, helpSynonyms, doHelp) if (text == null) { var character = getCharacter() @@ -262,6 +318,8 @@ const modifier = (text) => { if (state.flavorText != null) text += state.flavorText + text = AutoCards("input", text); + return { text } } @@ -1146,6 +1204,435 @@ function handleSetupEnemyStep(text) { return text } +function handleSetupAllyStep(text) { + state.show = "setupAlly" + + 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.setupAllyStep = null + return text + } + + switch (state.setupAllyStep) { + case 0: + text = text.toLowerCase(); + if (text.startsWith("y")) state.setupAllyStep = 100 + else if (text.startsWith("n")) state.setupAllyStep++ + break + case 1: + if (text.length > 0) { + state.tempAlly.name = text + state.setupAllyStep++ + + var allyMatches = state.allies.filter(x => x.name.toLowerCase() == state.tempAlly.name.toLowerCase() || x.name.toLowerCase() == `${state.tempAlly.name.toLowerCase()} a`) + if (allyMatches.length > 0) { + state.newAlly = false + state.tempAlly.health = allyMatches[0].health + state.tempAlly.ac = allyMatches[0].ac + state.tempAlly.hitModifier = allyMatches[0].hitModifier + state.tempAlly.damage = allyMatches[0].damage + state.tempAlly.initiative = allyMatches[0].initiative + state.tempAlly.spells = [...allyMatches[0].spells] + } else { + state.newAlly = true + } + } + return text + case 2: + if (text.length > 0) { + if (text.toLowerCase() == "default") { + state.setupAllyStep++ + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { + state.tempAlly.health = calculateRoll(text) + state.setupAllyStep++ + } else if (!isNaN(text)) { + state.tempAlly.health = Math.max(0, parseInt(text)) + state.setupAllyStep++ + } + } + return text + case 3: + if (text.toLowerCase() == "default") { + state.setupAllyStep++ + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { + state.tempAlly.ac = calculateRoll(text) + state.setupAllyStep++ + } else if (!isNaN(text)) { + state.tempAlly.ac = Math.max(0, parseInt(text)) + state.setupAllyStep++ + } + return text + case 4: + if (text.toLowerCase() == "default") { + state.setupAllyStep++ + } else if (!isNaN(text)) { + state.tempAlly.hitModifier = Math.max(0, parseInt(text)) + state.setupAllyStep++ + } + return text + case 5: + if (text.toLowerCase() == "default") { + state.setupAllyStep++ + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { + state.tempAlly.damage = text + state.setupAllyStep++ + } else if (!isNaN(text)) { + state.tempAlly.damage = Math.max(0, parseInt(text)) + state.setupAllyStep++ + } + return text + case 6: + if (text.toLowerCase() == "default") { + state.setupAllyStep++ + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) { + state.tempAlly.initiative = calculateRoll(text) + state.setupAllyStep++ + } else if (!isNaN(text)) { + state.tempAlly.initiative = Math.max(0, parseInt(text)) + state.setupAllyStep++ + } + return text + case 7: + if (text.toLowerCase() == "s") { + state.setupAllyStep = 500 + } else if (text.toLowerCase() == "e") { + state.tempAlly.spells = [] + } else if (text.length > 0) { + state.tempAlly.spells.push(text) + state.setupAllyStep++ + } + return text + case 8: + if (text.toLowerCase() == "s") { + state.setupAllyStep = 500 + } + else if (text.length > 0) { + state.tempAlly.spells.push(text) + } + return text + case 100: + if (/^\d+(\s.*)?$/gi.test(text)) { + state.setupAllyStep = 500 + state.newAlly = true + + var value = text.match(/^\d+/gi)[0] + var nameMatches = text.match(/(?<=\s).*/gi) + + //name, health, ac, hitModifier, damage, initiative, ...spells + switch (parseInt(value)) { + case 1: + state.tempAlly = createAlly("Fighter", calculateRoll("1d6+12"), 18, 4, "1d8+4", "d20+2", "Javelin Throw1d6+4") + break + case 2: + state.tempAlly = createAlly("Cleric", calculateRoll("1d6+10"), 17, 3, "1d6+2", "d20", "Healing Word", "Sanctuary", "Guiding Bolt4d6") + break + case 3: + state.tempAlly = createAlly("Rogue", calculateRoll("1d6+10"), 15, 5, "2d6+3", "d20+5", "Sneak Attack3d6+3") + break + case 4: + state.tempAlly = createAlly("Ranger", calculateRoll("1d6+10"), 15, 4, "1d8+2", "d20+2", "Cure Wounds", "Hunter's Mark", "Ensaring Strike1d8+2") + break + case 5: + state.tempAlly = createAlly("Barbarian", calculateRoll("1d6+15"), 17, 3, "1d12+4", "d20+1", "Rage1d12+4") + break + case 6: + state.tempAlly = createAlly("Bard", calculateRoll("1d6+10"), 15, 3, "1d6", "d20", "Petrifying Bite1d4+1") + break + case 7: + state.tempAlly = createAlly("Druid", calculateRoll("1d6+10"), 16, 3, "1d6+1", "d20", "Poison Bite2d4+1") + break + case 8: + state.tempAlly = createAlly("Monk", calculateRoll("1d6+10"), 16, 5, "2d6+2", "d20+3", "Flurry of Blows 3d6+2") + break + case 9: + state.tempAlly = createAlly("Paladin", calculateRoll("1d6+10"), 16, 3, "1d8+2", "d20+1", "Searing Smite2d6+4") + break + case 10: + state.tempAlly = createAlly("Wizard", calculateRoll("1d6+8"), 14, 3, "1d6", "d20", "Ray of Frost1d8", "Mage Armor", "Ice Knife1d10+5") + break + case 11: + state.tempAlly = createAlly("Sorcerer", calculateRoll("1d6+8"), 14, 3, "1d6", "d20", "Sorcerous Burst1d8", "Chromatic Orb2d8", "Burning Hands1d10") + break + case 12: + state.tempAlly = createAlly("Warlock", calculateRoll("1d6+8"), 14, 3, "1d6", "d20", "Eldritch Blast1d8+5", "Chill Touch1d12", "Hex") + break + case 13: + state.tempAlly = createAlly("Artificer", calculateRoll("1d6+10"), 15, 3, "2d6", "d20+1", "Archanist's Fire2d6+5", "Acid Vial1d10") + break + case 14: + state.tempAlly = createAlly("Commoner", calculateRoll("1d8"), 10, 2, "1d4", "d20") + break + case 15: + state.tempAlly = createAlly("Bandit", calculateRoll("2d8+2"), 12, 3, "1d6+1", "d20+1") + break + case 16: + state.tempAlly = createAlly("Guard", calculateRoll("2d8+2"), 16, 3, "1d6+1", "d20+1") + break + case 17: + state.tempAlly = createAlly("Cultist", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1", "Dark Devotion") + break + case 18: + state.tempAlly = createAlly("Acolyte", calculateRoll("2d8"), 10, 2, "1d4", "d20", "Sacred Flame1d8", "Cure Wounds") + break + case 19: + state.tempAlly = createAlly("Apprentice", calculateRoll("3d8"), 10, 4, "1d10+2", "d20", "Burning Hands3d6") + break + case 20: + state.tempAlly = createAlly("Witch", calculateRoll("3d8+3"), 10, 3, "1d6+2", "d20", "Ray of Sickness2d8", "Tashas Hideous Laughter", "Invisibility", "Ray of Frost2d8") + break + case 21: + state.tempAlly = createAlly("Buccaneer", calculateRoll("8d8+24"), 14, 5, "1d6+3", "d20+2", "Invade") + break + case 22: + state.tempAlly = createAlly("Spy", calculateRoll("6d8"), 12, 4, "1d6+2", "d20+2", "Sneak Attack2d6+2") + break + case 23: + state.tempAlly = createAlly("Captain", calculateRoll("10d8+20"), 15, 5, "3d6+9", "initiative") + break + case 24: + state.tempAlly = createAlly("Charlatan", calculateRoll("8d8+8"), 15, 4, "1d6+2", "d20+2", "Charm Person", "Shatter3d8", "Thunderwave2d8", "Vicious Mockery1d4") + break + case 25: + state.tempAlly = createAlly("Berserker", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1") + break + case 26: + state.tempAlly = createAlly("Priest", calculateRoll("5d8+5"), 13, 2, "1d6", "d20", "Spirit Guardians3d8", "Spiritual Weapon1d8", "Guiding Bolt4d6", "Cure Wounds") + break + case 27: + state.tempAlly = createAlly("Knight", calculateRoll("8d8+16"), 18, 5, "4d6+6", "d20", "Leadership") + break + case 28: + state.tempAlly = createAlly("Archer", calculateRoll("10d8+30"), 16, 6, "2d8+8", "d20+4") + break + case 29: + state.tempAlly = createAlly("Warrior", calculateRoll("6d8+12"), 16, 6, "1d8+3", "d20+1") + break + case 30: + state.tempAlly = createAlly("Conjurer", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Conjure Elemental", "Cloud Kill5d8", "Cloud of Daggers5d8", "Poison Spray1d12") + break + case 31: + state.tempAlly = createAlly("Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Greater Invisibility", "Ice Storm4d6", "Fireball8d6", "Magic Missile3d4+3") + break + case 32: + state.tempAlly = createAlly("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3", "Sneak Attack6d6+6") + break + case 33: + state.tempAlly = createAlly("Evoker", calculateRoll("12d8+12"), 12, 3, "1d6-1", "d20+2", "Chain Lightning10d8", "Wall of Ice", "Counter Spell", "Shatter3d8", "Magic Missile6d4+6") + break + case 34: + state.tempAlly = createAlly("Necromancer", calculateRoll("12d8+12"), 12, 7, "2d4", "d20+2", "Circle of Death8d6", "Blight8d8", "Cloudkill5d8", "Animate Dead", "Chill Touch1d8") + break + case 35: + state.tempAlly = createAlly("Champion", calculateRoll("22d8+44"), 18, 9, "6d6+15", "d20+2", "Second Wind") + break + case 36: + state.tempAlly = createAlly("Warlord", calculateRoll("27d8+108"), 18, 9, "4d6+10", "d20+3", "Command Ally", "Frighten Foe") + break + case 37: + state.tempAlly = createAlly("Archmage", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Lightning Bolt8d6", "Cone of Cold8d8", "Shocking Grasp1d8") + break + case 38: + state.tempAlly = createAlly("Archdruid", calculateRoll("24d8+24"), 16, 6, "1d6+2", "d20+2", "Fire Storm7d10", "Sunbeam6d8", "Wall of Fire", "Beast Sense", "Conjure Animals") + break + case 39: + state.tempAlly = createAlly("Ape", calculateRoll("3d8+6"), 12, 5, "2d4+6", "d20+2", "Throw Rock2d6+3") + break + case 40: + state.tempAlly = createAlly("Badger", calculateRoll("1d4+3"), 11, 2, "1", "d20") + break + case 41: + state.tempAlly = createAlly("Bat", calculateRoll("1d4-1"), 12, 4, "1", "d20+2") + break + case 42: + state.tempAlly = createAlly("Black Bear", calculateRoll("3d8+6"), 11, 4, "2d6+4", "d20+1") + break + case 43: + state.tempAlly = createAlly("Boar", calculateRoll("2d8+4"), 11, 3, "1d6+1", "d20", "Gore2d6+1") + break + case 44: + state.tempAlly = createAlly("Brown Bear", calculateRoll("3d10+6"), 11, 5, "3d4+6", "d20+1", "Fire Storm7d10", "Sunbeam6d8", "Wall of Fire", "Beast Sense", "Conjure Animals") + break + case 45: + state.tempAlly = createAlly("Camel", calculateRoll("2d10+6"), 10, 4, "1d4+2", "d20-1") + break + case 46: + state.tempAlly = createAlly("Cat", calculateRoll("1d4"), 12, 4, "1", "d20+2") + break + case 47: + state.tempAlly = createAlly("Constrictor Snake", calculateRoll("2d10+2"), 13, 4, "1d8+2", "d20+2", "Constrict3d4") + break + case 48: + state.tempAlly = createAlly("Crab", calculateRoll("1d4+1"), 11, 2, "1", "d20") + break + case 49: + state.tempAlly = createAlly("Crocodile", calculateRoll("2d10+2"), 12, 4, "1d8+2", "d20") + break + case 50: + state.tempAlly = createAlly("Dire Wolf", calculateRoll("3d10+6"), 14, 5, "1d10+3", "d20+2") + break + case 51: + state.tempAlly = createAlly("Draft Horse", calculateRoll("2d10+4"), 10, 6, "1d4+4", "d20") + break + case 52: + state.tempAlly = createAlly("Elephant", calculateRoll("8d12+24"), 12, 8, "4d8+12", "d20-1", "Trample2d10+6") + break + case 53: + state.tempAlly = createAlly("Elk", calculateRoll("2d10+5"), 10, 5, "1d6+3", "d20") + break + case 54: + state.tempAlly = createAlly("Frog", calculateRoll("1d4-1"), 11, 3, "1", "d20+1") + break + case 55: + state.tempAlly = createAlly("Giant Badger", calculateRoll("2d8+6"), 13, 3, "2d4+1", "d20") + break + case 56: + state.tempAlly = createAlly("Giant Crab", calculateRoll("3d8"), 15, 3, "1d6+1", "d20+1") + break + case 57: + state.tempAlly = createAlly("Giant Goat", calculateRoll("3d10+3"), 11, 5, "1d6+3", "d20+1") + break + case 58: + state.tempAlly = createAlly("Giant Seahorse", calculateRoll("3d10"), 14, 4, "2d6+2", "d20+1", "Bubble Dash") + break + case 59: + state.tempAlly = createAlly("Giant Spider", calculateRoll("4d10+4"), 14, 5, "1d8+3", "d20+3", "Web") + break + case 60: + state.tempAlly = createAlly("Giant Weasel", calculateRoll("2d8"), 13, 5, "1d4+3", "d20+3") + break + case 61: + state.tempAlly = createAlly("Goat", calculateRoll("1d8"), 10, 2, "1", "d20") + break + case 62: + state.tempAlly = createAlly("Hawk", calculateRoll("1d4-1"), 13, 5, "1", "d20+3") + break + case 63: + state.tempAlly = createAlly("Imp", calculateRoll("6d4+6"), 13, 5, "3d6+3", "d20+3", "Invisibility") + break + case 64: + state.tempAlly = createAlly("Lion", calculateRoll("4d10"), 12, 5, "2d8+6", "d20+2", "Roar") + break + case 65: + state.tempAlly = createAlly("Lizard", calculateRoll("1d4"), 10, 2, "1", "d20") + break + case 66: + state.tempAlly = createAlly("Mastiff", calculateRoll("1d8+1"), 12, 3, "1d6+1", "d20+2") + break + case 67: + state.tempAlly = createAlly("Mule", calculateRoll("2d8+2"), 10, 4, "1d4+2", "d20") + break + case 68: + state.tempAlly = createAlly("Octopus", calculateRoll("1d6"), 12, 4, "1", "d20+2", "Ink Cloud") + break + case 69: + state.tempAlly = createAlly("Owl", calculateRoll("1"), 11, 3, "1", "d20+1") + break + case 70: + state.tempAlly = createAlly("Panther", calculateRoll("3d8"), 12, 4, "1d4+2", "d20+2") + break + case 71: + state.tempAlly = createAlly("Pony", calculateRoll("2d8+2"), 10, 4, "1d4+2", "d20") + break + case 72: + state.tempAlly = createAlly("Pseudodragon", calculateRoll("3d4+3"), 14, 4, "2d4+4", "d20+2", "String2d4+2") + break + case 73: + state.tempAlly = createAlly("Quasit", calculateRoll("10d4"), 13, 5, "1d4+3", "d20+3", "Shape Shift", "Scare", "Invisibility") + break + case 74: + state.tempAlly = createAlly("Rat", calculateRoll("1d4-1"), 10, 2, "1", "d20") + break + case 75: + state.tempAlly = createAlly("Raven", calculateRoll("1d4"), 12, 4, "1", "d20+2") + break + case 76: + state.tempAlly = createAlly("Reef Shark", calculateRoll("4d8+4"), 12, 4, "2d4+2") + break + case 77: + state.tempAlly = createAlly("Riding Horse", calculateRoll("2d10+2"), 11, 5, "1d8+3", "d20+1") + break + case 78: + state.tempAlly = createAlly("Scorpion", calculateRoll("1d4-1"), 13, 2, "1d6+1", "d20") + break + case 79: + state.tempAlly = createAlly("Skeleton", calculateRoll("2d8+4"), 13, 5, "1d6+3", "d20+3", "Shortbow1d6+3", "Sword1d6+3") + break + case 80: + state.tempAlly = createAlly("Slaad Tadpole", calculateRoll("3d4"), 12, 4, "1d6+2", "d20+2") + break + case 81: + state.tempAlly = createAlly("Sphinx of Wonder", calculateRoll("7d4+7"), 13, 5, "1d4+3", "d20+2") + break + case 82: + state.tempAlly = createAlly("Spider", calculateRoll("1d4-1"), 12, 4, "1", "d20+2") + break + case 83: + state.tempAlly = createAlly("Sprite", calculateRoll("4d4"), 15, 6, "1d4+4", "d20+4", "Enchanting Bow1d4", "Invisibility") + break + case 84: + state.tempAlly = createAlly("Tiger", calculateRoll("3d10+6"), 13, 5, "1d6+3", "d20+3") + break + case 85: + state.tempAlly = createAlly("Venomous Snake", calculateRoll("2d4"), 12, 4, "2d4+2", "d20+2") + break + case 86: + state.tempAlly = createAlly("Warhorse", calculateRoll("3d10+3"), 11, 6, "2d4+4", "d20+2") + break + case 87: + state.tempAlly = createAlly("Weasel", calculateRoll("1d4-1"), 13, 5, "1", "d20+3") + break + case 88: + state.tempAlly = createAlly("Wolf", calculateRoll("2d8+2"), 12, 4, "1d6+2", "d20+2") + break + case 89: + state.tempAlly = createAlly("Zombie", calculateRoll("2d8+6"), 8, 3, "1d6+1", "d20-2") + break + } + + if (nameMatches != null) state.tempAlly.name = nameMatches[0] + } + return text + case 500: + state.show = null + state.setupAllyStep = null + + var ally = createAlly(state.tempAlly.name, state.tempAlly.health, state.tempAlly.ac, state.tempAlly.hitModifier, state.tempAlly.damage, state.tempAlly.initiative) + ally.spells = [...state.tempAlly.spells] + + var allyMatches = state.allies.filter(x => x.name.toLowerCase() == ally.name.toLowerCase() || x.name.toLowerCase() == `${ally.name.toLowerCase()} a`) + if (state.newAlly && allyMatches.length > 0) { + ally.name = getUniqueName(ally.name) + if (ally.name.endsWith("A")) { + allyMatches[0].name = ally.name + ally.name = ally.name.substring(0, ally.name.length - 1) + "B" + } + } else if (!state.newAlly) { + let removeIndex = state.allies.indexOf(allyMatches[0]) + state.allies.splice(removeIndex, 1) + } + + state.allies.push(ally) + break + } + return text +} + function resetTempCharacterSkills() { state.tempCharacter.skills = [ {name: "Acrobatics", stat: "Dexterity", modifier: 0}, @@ -1200,6 +1687,7 @@ function init() { } if (state.tempEnemy == null) state.tempEnemy = createEnemy("enemy", 10, 10, "2d6", 10) + if (state.tempAlly == null) state.tempAlly = createAlly("ally", 10, 10, "2d6", 10) if (state.characters == null) state.characters = [] if (state.notes == null) state.notes = [] if (state.locations == null) state.locations = [] @@ -1209,6 +1697,7 @@ function init() { if (state.defaultDifficulty == null) state.defaultDifficulty = 10 if (state.day == null) state.day = 0 if (state.enemies == null) state.enemies = [] + if (state.allies == null) state.allies = [] if (state.initiativeOrder == null) state.initiativeOrder = [] state.show = null state.prefix = null @@ -1277,6 +1766,248 @@ function doSetupEnemy(command) { return " " } +function doSetupAlly(command) { + state.setupAllyStep = 0 + state.tempAlly = createAlly("ally", 20, 10, 0, "2d6", 10) + state.show = "setupAlly" + return " " +} + +function doMemory(command) { + var arg0 = getArgument(command, 0) + if (arg0 == null) { + arg0 = "easy" + } + + switch(arg0) { + case "impossible": + state.memoryWidth = 6 + state.memoryHeight = 6 + state.memoryMaxTurns = 46 + break + case "hard": + state.memoryWidth = 5 + state.memoryHeight = 6 + state.memoryMaxTurns = 40 + break + case "medium": + state.memoryWidth = 4 + state.memoryHeight = 5 + state.memoryMaxTurns = 30 + break + case "effortless": + state.memoryWidth = 3 + state.memoryHeight = 2 + state.memoryMaxTurns = 25 + break + case "automatic": + state.memoryWidth = 2 + state.memoryHeight = 2 + state.memoryMaxTurns = 25 + break + case "easy": + default: + state.memoryWidth = 4 + state.memoryHeight = 3 + state.memoryMaxTurns = 25 + break + } + + state.memoryTurns = 1 + + let possibleSymbols = ["ā¤ļø", "šŸ’™", "šŸ‰", "🐸", "āš”ļø", "šŸ›”ļø", "🐻", "šŸ‘»", "🦁", "😺", "😈", "šŸ§™", "šŸ’€", "🐵", "šŸ“", "šŸ¦‰", "šŸ•·ļø", "šŸ¹", "šŸŽ", "šŸŽ²", "ā„ļø", "šŸ”„", "⚔", "🌳", "šŸ’¦", "šŸŽ", "šŸ„’", "šŸ–"] + shuffle(possibleSymbols) + possibleSymbols = possibleSymbols.splice(0, state.memoryWidth * state.memoryHeight / 2) + + state.memoryCards = possibleSymbols.concat(possibleSymbols) + shuffle(state.memoryCards) + state.memorySolved = new Array(state.memoryCards.length) + state.memoryRevealed = null + + state.memoryTurn = "game" + state.show = "memory" + return " " +} + +function handleMemoryTurn(text) { + state.show = "memory" + + 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+/, "") + } + + text = text.toLowerCase() + if (text == "q") { + state.memoryTurn = "forfeit" + return text + } + + switch (state.memoryTurn) { + case "game": + if (isNaN(text)) return text + let guess = parseInt(text) + state.memoryTurns++ + + if (guess < 1 || guess > state.memoryCards.length) return text + + if (state.memoryRevealed == null) { + state.message = `Card ${guess} is revealed to be:\n${state.memoryCards[guess -1]}` + state.memoryRevealed = guess + } else { + if (state.memoryRevealed == guess) { + state.message = `Card ${guess} is the same card you already picked, silly:\n${state.memoryCards[guess - 1]}` + state.memoryTurns-- + } else if (state.memoryCards[state.memoryRevealed - 1] == state.memoryCards[guess - 1]) { + state.message = `Cards ${state.memoryRevealed} and ${guess} are a match!\n${state.memoryCards[state.memoryRevealed - 1]} ${state.memoryCards[guess - 1]}` + state.memorySolved[state.memoryRevealed - 1] = true + state.memorySolved[guess - 1] = true + state.memoryRevealed = null + } else { + state.message = `Card ${guess} is revealed to be: ${state.memoryCards[guess - 1]}\nCards ${state.memoryRevealed} and ${guess} are NOT a match:${state.memoryCards[state.memoryRevealed - 1]} ${state.memoryCards[guess - 1]}` + state.memoryRevealed = null + } + } + + let win = true + for (let i = 0; i < state.memorySolved.length; i++) { + if (state.memorySolved[i] != true) { + win = false + break + } + } + if (win) state.memoryTurn = "win" + if (state.memoryTurns > state.memoryMaxTurns) state.memoryTurn = "lose" + + log(state.memorySolved) + return text + case "win": + case "lose": + case "forfeit": + state.show = null + state.memoryTurn = null + return text + } + + return `\nUnexpected Mastermind state. Input text: ${text}` +} + +function doLockpick(command) { + var arg0 = getArgument(command, 0) + if (arg0 == null) { + arg0 = "easy" + } + + state.lockpickingTurn = "intro" + state.lockpickingGuesses = 0 + state.show = "lockpicking" + + switch(arg0) { + case "impossible": + state.lockpickingSlots = 7 + state.lockpickingGuessMax = 15 + break + case "hard": + state.lockpickingSlots = 6 + state.lockpickingGuessMax = 15 + break + case "medium": + state.lockpickingSlots = 5 + state.lockpickingGuessMax = 15 + break + case "effortless": + state.lockpickingSlots = 4 + state.lockpickingGuessMax = 15 + break + case "automatic": + state.lockpickingSlots = 3 + state.lockpickingGuessMax = 20 + break + case "easy": + default: + state.lockpickingSlots = 4 + state.lockpickingGuessMax = 12 + break + } + + state.lockpickingCombination = "" + for (let i = 0; i < state.lockpickingSlots; i++) { + state.lockpickingCombination += getRandomFromList("r", "y", "w", "g", "o", "b") + } + + return " " +} + +function handleLockpickingTurn(text) { + state.show = "lockpicking" + + 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+/, "") + } + + text = text.toLowerCase() + if (text == "q") { + state.lockpickingTurn = "forfeit" + return text + } + + switch (state.lockpickingTurn) { + case "intro": + state.lockpickingTurn = "game" + case "game": + state.lockpickingInput = text + + state.lockpickingCorrect = 0 + let combo = state.lockpickingCombination + for (var i = 0; i < state.lockpickingSlots; i++) { + let letter = text.substring(i, i + 1) + if (letter == state.lockpickingCombination.substring(i, i + 1)) state.lockpickingCorrect++ + combo = combo.replace(letter, "") + } + state.lockpickingWrongPlace = state.lockpickingSlots - combo.length - state.lockpickingCorrect + + if (state.lockpickingInput.length == state.lockpickingSlots) state.lockpickingGuesses++ + + if (state.lockpickingCorrect == state.lockpickingSlots) state.lockpickingTurn = "win" + else if (state.lockpickingGuesses >= state.lockpickingGuessMax) state.lockpickingTurn = "lose" + + return text + case "win": + case "lose": + case "forfeit": + state.show = null + state.lockpickingTurn = null + return text + } + + return `\nUnexpected Mastermind state. Input text: ${text}` +} + function doBasicDeck(command) { var character = getCharacter() var takeWord = character.name == "You" ? "take" : "takes" @@ -1303,6 +2034,265 @@ function doBasicDeck(command) { return `${toTitleCase(character.name)} ${takeWord} the Stragedy Basic Deck` } +function doItemShop(command) { + command = command.replace(/very rare/gi, "phenomenal") + + state.itemShopCategoryName = searchArgument(command, /default|weapons|armor|tools|gear|common|uncommon|rare|phenomenal|legendary|artifact/gi) + if (state.itemShopCategoryName == null && searchArgument(command, /weapon/) != null) state.itemShopCategoryName = "weapons" + if (state.itemShopCategoryName == null) state.itemShopCategoryName = "default" + + let arg1 = searchArgument(command, /free/gi) + state.itemShopIsFree = arg1 != null + + let arg2 = searchArgument(command, /all/gi) + let all = arg2 != null + state.itemShopClearDeals = state.itemShopAll || state.itemShopAll != all + state.itemShopAll = all + + state.itemShopStep = 0 + state.show = "itemShop" + return " " +} + +function handleItemShopStep(text) { + state.show = "itemShop" + + 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.itemShopStep = 500 + return text + } + + switch (state.itemShopStep) { + case 0: + case 1: + case 2: + case 3: + if (isNaN(text)) return text + var index = parseInt(text) - 1 + + let deals = findItemShopDeals(state.itemShopCategoryName, false) + if (index < 0 || index >= deals.length) return text + + let deal = deals[index] + + var character = getCharacter() + var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold") + var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity + + if (!state.itemShopIsFree && deal.price > gold) { + state.itemShopStep = 2 + return text + } + + if ("damage" in deal) doTakeWeapon(`takeweapon ${deal.damage} ${deal.toHitBonus} ${deal.ability} ${deal.name}`) + else if ("ac" in deal) doTakeArmor(`takearmor ${deal.ac} ${deal.name}`) + else doTake(`take ${deal.quantity} ${deal.name}`) + if (!state.itemShopIsFree) character.inventory[goldIndex].quantity -= deal.price + deal.bought = true + + state.itemShopStep = 1 + break + case 500: + state.show = null + state.itemShopStep = null + break + } + return text +} + +function doSpellShop(command) { + var character = getCharacter() + + let arg0 = searchArgument(command, /bard|cleric|druid|paladin|ranger|sorcerer|warlock|wizard/gi) + if (arg0 == null) { + arg0 = character.className.toLowerCase() + if (/bard|cleric|druid|paladin|ranger|sorcerer|warlock|wizard/gi.test(arg0) == false) arg0 = "wizard" + } + state.spellShopClassName = arg0 + + let arg1 = searchArgument(command, /\d+/gi) + if (arg1 == null) { + let level = getLevel(character.experience) + switch (state.spellShopClassName) { + case "bard": + case "cleric": + case "druid": + case "sorcerer": + case "warlock": + case "wizard": + switch(level) { + case 1: + case 2: + arg1 = 1 + break + case 3: + case 4: + arg1 = 2 + break + case 5: + case 6: + arg1 = 3 + break + case 7: + case 8: + arg1 = 4 + break + case 9: + case 10: + arg1 = 5 + break + case 11: + case 12: + arg1 = 6 + break + case 13: + case 14: + arg1 = 7 + break + case 15: + case 16: + arg1 = 8 + break + default: + arg1 = 9 + break + } + break + case "paladin": + case "ranger": + switch(level) { + case 1: + case 2: + case 3: + case 4: + arg1 = 1 + break + case 5: + case 6: + case 7: + case 8: + arg1 = 2 + break + case 9: + case 10: + case 11: + case 12: + arg1 = 3 + break + case 13: + case 14: + case 15: + case 16: + arg1 = 4 + break + default: + arg1 = 5 + break + } + break + default: + arg1 = 1 + break + } + } + arg1 = parseInt(arg1) + state.spellShopLevel = arg1 + + let arg2 = searchArgument(command, /free/gi) + state.spellShopIsFree = arg2 != null + + let arg3 = searchArgument(command, /all/gi) + let all = arg3 != null + state.spellShopClearDeals = state.spellShopAll || state.spellShopAll != all + state.spellShopAll = all + + state.spellShopStep = 0 + state.show = "spellShop" + return " " +} + +function handleSpellShopStep(text) { + state.show = "spellShop" + + 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.spellShopStep = 500 + return text + } + + switch (state.spellShopStep) { + case 0: + case 1: + case 2: + case 3: + if (isNaN(text)) return text + var index = parseInt(text) - 1 + + let deals = findSpellShopDeals(state.spellShopClassName, state.spellShopLevel, false) + if (index < 0 || index >= deals.length) return text + + let deal = deals[index] + + var character = getCharacter() + var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold") + var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity + var found = character.spells.find((element) => element == deal.name) != undefined + + if (deal.price > gold) { + state.spellShopStep = 2 + return text + } else if (found) { + state.spellShopStep = 3 + return text + } + + doLearnSpell(`learnspell ${deal.name}`) + if (!state.spellShopIsFree) character.inventory[goldIndex].quantity -= deal.price + deal.bought = true + + state.spellShopStep = 1 + break + case 500: + state.show = null + state.spellShopStep = null + break + } + return text +} + function doCardShop(command) { state.stragedyShopStep = 0 state.show = "stragedyShop" @@ -1337,6 +2327,7 @@ function handleStragedyShopStep(text) { switch (state.stragedyShopStep) { case 0: case 1: + case 2: if (isNaN(text)) return text var index = parseInt(text) - 1 if (index < 0 || index >= state.cardDeals.length) return text @@ -1520,6 +2511,24 @@ function doAddCard(command) { case "b": case "brigand": return doTake("take Stragedy Brigand Card") + case "2": + return doTake("take Stragedy 2 Card") + case "3": + return doTake("take Stragedy 3 Card") + case "4": + return doTake("take Stragedy 4 Card") + case "5": + return doTake("take Stragedy 5 Card") + case "6": + return doTake("take Stragedy 6 Card") + case "7": + return doTake("take Stragedy 7 Card") + case "8": + return doTake("take Stragedy 8 Card") + case "9": + return doTake("take Stragedy 9 Card") + case "10": + return doTake("take Stragedy 10 Card") case "common": return doTake(`take Stragedy ${getRandomFromList("2", "3", "4", "5", "6", "7", "8", "9")} Card`) case "rare": @@ -1941,6 +2950,14 @@ function doHeal(command) { } } + for (var ally of state.allies) { + if (ally.name.toLowerCase() == arg1.toLowerCase()) { + ally.health = Math.max(0, ally.health + healing) + state.show = "none" + return `\n[${toTitleCase(ally.name)} has been healed for ${healing} hp to a total of ${ally.health}]\n` + } + } + for (var character of state.characters) { if (character.name.toLowerCase() == arg1.toLowerCase()) { character.health += healing @@ -1951,7 +2968,7 @@ function doHeal(command) { } state.show = "none" - return `\n[Error: Could not find an enemy or character matching the name ${arg1}. Type #enemies or #characters to see a list]` + return `\n[Error: Could not find an enemy, ally, or character matching the name ${arg1}. Type #enemies, #allies, or #characters to see a list]` } } @@ -2015,6 +3032,14 @@ function doDamage(command) { } } + for (var ally of state.allies) { + if (ally.name.toLowerCase() == arg1.toLowerCase()) { + ally.health = Math.max(0, ally.health - damage) + state.show = "none" + return `\n[${toTitleCase(ally.name)} has been damaged for ${damage} hp with ${ally.health} remaining] ${ally.health == 0 ? " " + toTitleCase(ally.name) + " has been defeated!" : ""}\n` + } + } + for (var character of state.characters) { if (character.name.toLowerCase() == arg1.toLowerCase()) { character.health = Math.max(0, character.health - damage) @@ -2024,7 +3049,7 @@ function doDamage(command) { } state.show = "none" - return `\n[Error: Could not find an enemy matching the name ${arg1}. Type #enemies or #characters to see a list]` + return `\n[Error: Could not find an enemy, ally, or character matching the name ${arg1}. Type #enemies, #allies, or #characters to see a list]` } } @@ -2034,6 +3059,8 @@ function doRest(command) { state.enemies = [] state.cardDeals = null state.cardPrices = null + state.spellShopDeals = null + state.itemShopDeals = null var healingFactor = 1 var text @@ -2295,6 +3322,7 @@ function doAttack(command) { } var enemyString = "" + var allyString = "" if (state.initiativeOrder.length > 0) { var foundEnemy @@ -2313,6 +3341,23 @@ function doAttack(command) { } } + var foundAlly + + if (foundEnemy == null) for (var ally of state.allies) { + if (targetText.toLowerCase().includes(ally.name.toLowerCase())) { + foundAlly = ally + break + } + } + + if (foundAlly == null) { + var indexMatches = targetText.match(/(?<=ally\s*)\d+/gi) + if (indexMatches != null) { + foundAlly = state.allies[parseInt(indexMatches[0]) - 1] + targetText = targetText.replace(/ally\s*d+/gi, foundAlly.name) + } + } + var damage if (/^\d*d\d+((\+|-)d+)?$/gi.test(character.damage)) damage = score == 20 ? calculateRoll(character.damage) + calculateRoll(character.damage) : calculateRoll(character.damage) else damage = parseInt(character.damage) @@ -2339,6 +3384,22 @@ function doAttack(command) { } else enemyString += ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!` } } + + if (foundAlly != null) { + if (usingDefaultDifficulty) targetRoll = foundAlly.ac + if (score == 20 || score + modifier >= targetRoll) { + if (score == 20) allyString += `\nCritical Damage: ${damage}\n` + else allyString += `\nDamage: ${damage}\n` + + state.blockCharacter = foundAlly + state.blockPreviousHealth = foundAlly.health + foundAlly.health = Math.max(0, foundAlly.health - damage) + if (foundAlly.health == 0) { + allyString += ` ${toTitleCase(foundAlly.name)} has been defeated!` + + } else allyString += ` ${toTitleCase(foundAlly.name)} has ${foundAlly.health} health remaining!` + } + } } var dieText = advantageText == "advantage" || advantageText == "disadvantage" ? `${advantageText}(${die1},${die2})` : die1 @@ -2346,10 +3407,10 @@ function doAttack(command) { state.show = "prefix" if (targetRoll == 0) state.prefix = "" - else 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` + else if (score == 20) state.prefix = `\n[Target AC: ${targetRoll} Attack roll: ${dieText}]\n` + else if (score == 1) state.prefix = `\n[Target AC: ${targetRoll} Attack roll: ${dieText}]\n` + else if (modifier != 0) state.prefix = `\n[Target AC: ${targetRoll} Attack roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= targetRoll ? "Success!" : "Failure!"}]\n` + else state.prefix = `\n[Target AC: ${targetRoll} Attack roll: ${dieText}. ${score >= targetRoll ? "Success!" : "Failure!"}]\n` var text if (score + modifier >= targetRoll) text = `\n${toTitleCase(character.name)} successfully hit ${targetText}!` @@ -2359,6 +3420,7 @@ function doAttack(command) { else if (score == 1) text += " Critical failure! The attack missed in a spectacular way!" if (enemyString != null) text += enemyString + if (allyString != null) text += allyString if (targetRoll > 0 && (score + modifier >= targetRoll || score == 20)) text += addXpToAll(Math.floor(state.autoXp * clamp(targetRoll, 1, 20) / 20)) return text + "\n" @@ -2697,6 +3759,11 @@ function doShowEnemies(command) { return " " } +function doShowAllies(command) { + state.show = "showAllies" + return " " +} + function doRemoveEnemy(command) { var arg0 = getArgumentRemainder(command, 0) if (arg0 == null) { @@ -2749,6 +3816,58 @@ function doRemoveEnemy(command) { return `\n[The enemy ${toTitleCase(enemy.name)} has been removed]\n` } +function doRemoveAlly(command) { + var arg0 = getArgumentRemainder(command, 0) + if (arg0 == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + if (/\d+\D+(\d+\D*)+/gi.test(arg0)) { + + var list = arg0.split(/\D+/) + list.sort(function(a, b) { + return b - a; + }); + + var text = "\n" + list.forEach(x => { + var num = parseInt(x) - 1 + if (num >= state.allies.length) { + state.show = "none" + return `\n[Error: Ally ${x} does not exist. See #showallies]\n` + } + + var ally = state.allies[num] + state.allies.splice(num, 1) + var index = state.initiativeOrder.indexOf(ally) + if (index >= 0) state.initiativeOrder.splice(index, 1) + text += `[The ally ${toTitleCase(ally.name)} has been removed]\n` + }) + + state.show = "none" + return text + } + + var ally + if (isNaN(arg0)) arg0 = state.allies.findIndex(x => x.name.toLowerCase() == arg0.toLowerCase()) + else arg0-- + + if (arg0 == -1) { + state.show = "none" + return "\n[Error: Ally not found. See #showallies]\n" + } else if (arg0 >= state.allies.length || arg0 < 0) { + state.show = "none" + return "\n[Error: Location number out of bounds. See #showallies]\n" + } else { + ally = state.allies[arg0] + state.allies.splice(arg0, 1) + } + + state.show = "none" + return `\n[The ally ${toTitleCase(ally.name)} has been removed]\n` +} + function doClearEnemies(command) { var arg0 = getArgument(command, 0) if (arg0 != null) { @@ -2762,6 +3881,19 @@ function doClearEnemies(command) { return "\n[The enemies have been cleared]\n" } +function doClearAllies(command) { + var arg0 = getArgument(command, 0) + if (arg0 != null) { + return doRemoveAlly(command) + } + + state.allies = [] + state.initiativeOrder = [] + + state.show = "none" + return "\n[The allies have been cleared]\n" +} + function doAddEnemy(command) { var name = getArgument(command, 0) if (name == null) { @@ -2851,6 +3983,95 @@ function doAddEnemy(command) { return `[Enemy ${toTitleCase(enemy.name)} has been created]` } +function doAddAlly(command) { + var name = getArgument(command, 0) + if (name == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + var health = getArgument(command, 1) + if (health == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(health)) { + health = calculateRoll(health) + } else if (isNaN(health)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + health = parseInt(health) + + var ac = getArgument(command, 2) + if (ac == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(ac)) { + ac = calculateRoll(ac) + } else if (isNaN(ac)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + ac = parseInt(ac) + + var hitModifier = getArgument(command, 3) + if (hitModifier == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(hitModifier)) { + hitModifier = calculateRoll(hitModifier) + } else if (isNaN(hitModifier)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + + var damage = getArgument(command, 4) + if (damage == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } else if (isNaN(damage) && !/^\d*d\d+((\+|-)\d+)?$/gi.test(damage)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + + var initiative = getArgument(command, 5) + if (initiative == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(initiative)) { + initiative = calculateRoll(initiative) + } else if (isNaN(initiative)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + initiative = parseInt(initiative) + + var spells = [] + var spell = null + var index = 6 + do { + spell = getArgument(command, index++) + if (spell != null) spells.push(spell) + } while (spell != null) + + var ally = createAlly(name, health, ac, hitModifier, damage, initiative) + ally.spells = spells + + var allyMatches = state.allies.filter(x => x.name.toLowerCase() == ally.name.toLowerCase() || x.name.toLowerCase() == `${ally.name.toLowerCase()} a`) + if (allyMatches.length > 0) { + ally.name = getUniqueName(ally.name) + if (ally.name.endsWith("A")) { + allyMatches[0].name = ally.name + ally.name = ally.name.substring(0, ally.name.length - 1) + "B" + } + } + + state.allies.push(ally) + + state.show = "none" + return `[Ally ${toTitleCase(ally.name)} has been created]` +} + function doInitiative(command) { for (character of state.characters) { var stat = character.stats.find(element => element.name.toLowerCase() == "dexterity") @@ -2863,6 +4084,11 @@ function doInitiative(command) { else enemy.calculatedInitiative = enemy.initiative } + for (ally of state.allies) { + if (isNaN(ally.initiative)) ally.calculatedInitiative = calculateRoll(ally.initiative) + else ally.calculatedInitiative = ally.initiative + } + if (state.enemies.length == 0) { state.show = "none" return "\n[Error: No enemies! Type #addenemy or #encounter]\n" @@ -2924,6 +4150,15 @@ function doTurn(command) { if (index >= 0) state.initiativeOrder.splice(index, 1) } + var defeatedAllies = 0 + for (var ally of state.allies) { + if (ally.health > 0) continue + + defeatedAllies++ + var index = state.initiativeOrder.findIndex(x => x.name.toLowerCase() == ally.name.toLowerCase()) + if (index >= 0) state.initiativeOrder.splice(index, 1) + } + var defeatedCharacters = 0 for (var character of state.characters) { if (character.health > 0) continue @@ -2964,6 +4199,7 @@ function doBlock(command) { var character = state.characters.find(x => x.name.toLowerCase() == state.blockCharacter.name.toLowerCase()) if (character == null) character = state.enemies.find(x => x.name.toLowerCase() == state.blockCharacter.name.toLowerCase()) + if (character == null) character = state.allies.find(x => x.name.toLowerCase() == state.blockCharacter.name.toLowerCase()) if (character == null) { state.show = "none" return "\n[Error: Character no longer exists. See #help]\n" @@ -3026,11 +4262,223 @@ function doTake(command) { return text } +function doTakeWeapon(command) { + var itemIndex = 3 + 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" + } + if (isNaN(arg1)) { + state.show = "none" + return "\n[Error: Expected a number. See #help]\n" + } + arg1 = parseInt(arg1) + + var arg2 = getArgument(command, 2) + if (arg2 == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + var arg3 = getArgument(command, 3) + if (arg3 == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + if (arg3 == "the") { + var tempArg = getArgument(command, 1) + if (tempArg != null && !isNaN(tempArg)) { + arg3 = tempArg + itemIndex++ + } + } + + const item = { + quantity: 1, + name: getArgumentRemainder(command, itemIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true), + damageDice: arg0, + toHitBonus: arg1, + ability: arg2 + } + + var character = getCharacter() + var commandName = "take" + var commandNamePlural = commandName.plural(character.name == "You") + var haveWord = character.name == "You" ? "have" : "has" + + var text = "\n" + text += `${character.name} ${commandNamePlural} ${item.name.toLowerCase().startsWith("the ") ? "" : "the "}${item.name}.\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) + + let displayItemName = existingItem.name.plural(existingItem.quantity == 1) + text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n` + } + + return text +} + +function doTakeArmor(command) { + var itemIndex = 1 + 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" + } + + const item = { + quantity: 1, + name: getArgumentRemainder(command, itemIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true), + ac: arg0, + } + + var character = getCharacter() + var commandName = "take" + var commandNamePlural = commandName.plural(character.name == "You") + var haveWord = character.name == "You" ? "have" : "has" + + var text = "\n" + text += `${character.name} ${commandNamePlural} ${item.name.toLowerCase().startsWith("the ") ? "" : "the "}${item.name}.\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) + + let displayItemName = existingItem.name.plural(existingItem.quantity == 1) + text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n` + } + + return text +} + +function doReward(command) { + command = command.replace(/very rare/gi, "phenomenal") + + let quantity = getArgument(command, 0) + if (quantity == null || isNaN(quantity)) quantity = 1 + if (!isNaN(quantity)) quantity = parseInt(quantity) + if (quantity < 1) quantity = 1 + + let categoryName = searchArgument(command, /default|weapons|armor|tools|gear|common|uncommon|rare|phenomenal|legendary|artifact/gi) + if (categoryName == null && searchArgument(command, /weapon/) != null) categoryName = "weapons" + if (categoryName == null) categoryName = "default" + + let loot = [] + for (let i = 0; i < quantity; i++) { + const rand = Math.random() + categoryName = categoryName.toLowerCase() + let category + + if (categoryName == "weapons" || categoryName == "default" && rand <= .125) category = weaponsList + else if (categoryName == "armor" || categoryName == "default" && rand <= .25) category = armorList + else if (categoryName == "tools" || categoryName == "default" && rand <= .375) category = toolsList + else if (categoryName == "gear" || categoryName == "default" && rand <= .50) category = gearList + else if (categoryName == "common" || categoryName == "default" && rand <= .70) category = commonList + else if (categoryName == "uncommon" || categoryName == "default" && rand <= .80) category = uncommonList + else if (categoryName == "rare" || categoryName == "default" && rand <= .88) category = rareList + else if (categoryName == "phenomenal" || categoryName == "default" && rand <= .94) category = phenomenalList + else if (categoryName == "legendary" || categoryName == "default" && rand <= .98) category = legendaryList + else if (categoryName == "artifact" || categoryName == "default" && rand > .98) category = artifactList + else category = commonList + + let itemStoryCardName + shuffled = [...category].sort(() => 0.5 - Math.random()); + itemStoryCardName = shuffled[0] + + let itemName = itemShopConvertGenericName(itemStoryCardName) + loot.push(itemName) + + let itemStoryCard = findItemCard(itemName, itemStoryCardName) + + if (itemStoryCard != null && itemStoryCard.type == "weapon") doTakeWeapon(`takeweapon ${itemStoryCard.description.split(",")[1]} ${itemStoryCard.description.split(",")[2]} ${itemStoryCard.description.split(",")[3]} ${itemName}`) + else if (itemStoryCard != null && itemStoryCard.type == "armor") doTakeArmor(`takearmor ${itemStoryCard.description.split(",")[1]} ${itemName}`) + else doTake(`take ${itemName}`) + } + + let text = "You have found" + if (loot.length == 1) { + let itemName = loot[0] + let aWord = ['a', 'e', 'i', 'o', 'u'].indexOf(itemName.charAt(0).toLowerCase()) !== -1 ? "an" : "a" + text += ` ${aWord} ${itemName}!` + } else { + text += ":" + loot.forEach(itemName => { + let aWord = ['a', 'e', 'i', 'o', 'u'].indexOf(itemName.charAt(0).toLowerCase()) !== -1 ? "an" : "a" + text += `\n${aWord} ${itemName},` + }) + } + + return text +} + function doMap(command) { state.show = "map" return " " } +function doEquip(command) { + let character = getCharacter() + let arg0 = getArgument(command, 0) + if (arg0 == null) { + state.show = "none" + return "\n[Error: Not enough parameters. See #help]\n" + } + + var dontWord = character.name == "You" ? "don't" : "doesn't" + + let itemName = getArgumentRemainder(command, 0) + + let item = character.inventory.find((element) => element.name.toLowerCase() == itemName.toLowerCase()) + + if (item == null) return `${character.name} tried to equip ${toTitleCase(itemName)}, but ${dontWord} possess it` + + let text = `\n${character.name} equipped the item ${toTitleCase(itemName)}!\n` + if ("damageDice" in item && "toHitBonus" in item) { + let abilityValue = character.stats.find((element) => element.name.toLowerCase() == item.ability) + let ability = abilityValue == null ? 10 : abilityValue.value + let abilityModifier = Math.ceil((ability - 10) / 2) + + let damageBase = item.damageDice.replaceAll(/\+.*/gi, "") + let damageModifier = parseInt(item.damageDice.replaceAll(/.*\+/gi, "")) + abilityModifier + character.damage = `${damageBase}+${damageModifier}` + character.proficiency = abilityModifier + character.meleeStat = item.ability + } else if ("ac" in item) { + let dexterityStat = character.stats.find((element) => element.name.toLowerCase() == "dexterity") + let dexterity = dexterityStat == null ? 10 : dexterityStat.value + let ac = parseInt(item.ac.replaceAll(/(?<=.)\+.*/gi, "")) + if (/.*\+dmax2/i.test(item.ac)) character.ac = ac + Math.max(2, Math.ceil((dexterity - 10) / 2)) + else if (/.*\+d/i.test(item.ac)) character.ac = ac + Math.ceil((dexterity - 10) / 2) + else if (/\+.*/i.test(item.ac)) character.ac += ac + else character.ac = ac + } + + text += "\n" + return text +} + function doDrop(command) { var character = getCharacter() var commandName = getCommandName(command) @@ -3498,6 +4946,7 @@ function doCastSpell(command) { var roll = advantage == "advantage" ? Math.max(roll1, roll2) : advantage == "disadvantage" ? Math.min(roll1, roll2) : roll1 var enemyString = "" + var allyString = "" if (targetText != null && state.initiativeOrder.length > 0) { var foundEnemy @@ -3516,6 +4965,23 @@ function doCastSpell(command) { } } + var foundAlly + + if (foundEnemy == null) for (var ally of state.allies) { + if (targetText.toLowerCase().includes(ally.name.toLowerCase())) { + foundAlly = ally + break + } + } + + if (foundAlly == null) { + var indexMatches = targetText.match(/(?<=ally\s*)\d+/gi) + if (indexMatches != null) { + foundAlly = state.allies[parseInt(indexMatches[0]) - 1] + targetText = targetText.replace(/ally\s*d+/gi, foundAlly.name) + } + } + var damage = roll == 20 ? calculateRoll("2d6") + calculateRoll("2d6") : calculateRoll("2d6") var damageMatches = targetText.match(/\d*d\d+((\+|-)d+)?/gi) @@ -3538,6 +5004,20 @@ function doCastSpell(command) { else enemyString += ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!\n` } } + + if (foundAlly != null) { + if (usingDefaultDifficulty) difficulty = foundAlly.ac + if (roll == 20 || roll + modifier >= difficulty) { + if (roll == 20) allyString += `\nCritical Damage: ${damage}\n` + else allyString += `\nDamage: ${damage}\n` + + state.blockCharacter = foundAlly + state.blockPreviousHealth = foundAlly.health + foundAlly.health = Math.max(0, foundAlly.health - damage) + if (foundAlly.health == 0) allyString += ` ${toTitleCase(foundAlly.name)} has been defeated!\n` + else allyString += ` ${toTitleCase(foundAlly.name)} has ${foundAlly.health} health remaining!\n` + } + } } state.show = "prefix" @@ -3555,6 +5035,7 @@ function doCastSpell(command) { else text += ` The spell ${targetText != null ? "misses" : "fails"}!` if (enemyString != null) text += enemyString + if (allyString != null) text += allyString if (difficulty > 0 && (roll + modifier >= difficulty || roll == 20)) text += addXpToAll(Math.floor(state.autoXp * clamp(difficulty, 1, 20) / 20)) return `\n${text}\n` @@ -3726,6 +5207,7 @@ function doReset(command) { state.locations = [] state.location = null state.enemies = null + state.allies = null state.initiativeOrder = [] state.x = null state.y = null diff --git a/Library.js b/Library.js index 747f00b..21f6d9f 100644 --- a/Library.js +++ b/Library.js @@ -1,3 +1,14 @@ +const weaponsList = ["Club", "Dagger", "Greatclub", "Handaxe", "Javelin", "Light Hammer", "Mace", "Quarterstaff", "Sickle", "Spear", "Dart", "Light Crossbow", "Shortbow", "Sling", "Battleaxe", "Flail", "Glaive", "Greataxe", "Greatsword", "Halberd", "Lance", "Longsword", "Maul", "Morningstar", "Pike", "Rapier", "Scimitar", "Shortsword", "Trident", "Warhammer", "Warhammer", "War Pick", "Whip", "Blowgun", "Hand Crossbow", "Heavy Crossbow", "Longbow", "Musket", "Pistol"] +const armorList = ["Padded Armor", "Leather Armor", "Studded Leather Armor", "Hide Armor", "Chain Shirt", "Scale Mail", "Breastplate", "Half Plate Armor", "Ring Mail", "Chain Mail", "Splint Armor", "Plate Armor", "Shield"] +const toolsList = ["Alchemist's Supplies", "Brewer's Supplies", "Calligrapher's Supplies", "Carpenter's Tools", "Cartographer's Tools", "Cobbler's Tools", "Cook's Utensils", "Glassblower's Tools", "Jeweler's Tools", "Leatherworker's Tools", "Mason's Tools", "Painter's Supplies", "Potter's Tools", "Smith's Tools", "Tinker's Tools", "Weaver's Tools", "Woodcarver's Tools", "Disguise Kit", "Forgery Kit", "Gaming Set", "Herbalism Kit", "Musical Instrument", "Navigator's Tools", "Poisoner's Kit", "Thieves' Tools"] +const gearList = ["Acid", "Alchemist's Fire", "Ammunition", "Antitoxin", "Arcane Focus", "Backpack", "Ball Bearings", "Barrel", "Basket", "Bedroll", "Bell", "Blanket", "Block and Tackle", "Book", "Glass Bottle", "Bucket", "Burglar's Pack", "Caltrops", "Candle", "Crossbow Bolt Case", "Map Case", "Scroll Case", "Chain", "Chest", "Climber's Kit", "Fine Clothes", "Traveler's Clothes", "Component Pouch", "Costume", "Crowbar", "Diplomat's Pack", "Druidic Focus", "Dungeoneer's Pack", "Entertainer's Pack", "Explorer's Pack", "Flask", "Grappling Hook", "Healer's Kit", "Holy Symbol", "Holy Water", "Hunting Trap", "Ink", "Ink Pen", "Jug", "Ladder", "Lamp", "Bullseye Lantern", "Hooded Lantern", "Lock", "Magnifying Glass", "Manacles", "Map", "Mirror", "Net", "Oil", "Paper", "Parchment", "Perfume", "Basic Poison", "Pole", "Iron Pot", "Potion of Healing", "Pouch", "Priest's Pack", "Quiver", "Portable Ram", "Rations", "Robe", "Rope", "Sack", "Scholar's Pack", "Shovel", "Signal Whistle", "Spell Scroll", "Iron Spikes", "Spyglass", "String", "Tent", "Tinderbox", "Torch", "Vial", "Waterskin"] +const commonList = ["Armor of Gleaming", "Bead of Nourishment", "Bead of Refreshment", "Boots of False Tracks", "Candle of the Deep", "Cast-Off Armor", "Charlatan's Die", "Cloak of Billowing", "Cloak of Many Fashions", "Clockwork Amulet", "Clothes of Mending", "Dark Shard Amulet", "Dread Helm", "Ear Horn of Hearing", "Enduring Spellbook", "Ersatz Eye", "Hat of Vermin", "Hat of Wizardry", "Heward's Handy Spice Pouch", "Horn of Silent Alarm", "Instrument of Illusions", "Instrument of Scribing", "Lock of Trickery", "Moon-Touched Sword", "Mystery Key", "Orb of Direction", "Orb of Time", "Perfume of Bewitching", "Pipe of Smoke Monsters", "Pole of Angling", "Pole of Collapsing", "Potion of Climbing", "Potion of Comprehension", "Pot of Awakening", "Prosthetic Limb", "Rival Coin", "Rope of Mending", "Ruby of the War Mage", "Shield of Expression", "Silvered Weapon", "Smoldering Armor", "Staff of Adornment", "Staff of Birdcalls", "Staff of Flowers", "Talking Doll", "Tankard of Sobriety", "Veteran's Cane", "Walloping Ammunition", "Wand of Conducting", "Wand of Pyrotechnics"] +const uncommonList = ["Adamantine Armor", "Adamantine Weapon", "Alchemy Jug", "Ammunition +1", "Amulet of Proof against Detection and Location", "Baba Yaga's Dancing Broom", "Bag of Holding", "Bag of Tricks", "Boots of Elvenkind", "Boots of Striding and Springing", "Boots of the Winterlands", "Bracers of Archery", "Brooch of Shielding", "Broom of Flying", "Cap of Water Breathing", "Circlet of Blasting", "Cloak of Elvenkind", "Cloak of Protection", "Cloak of the Manta Ray", "Decanter of Endless Water", "Deck of Illusions", "Driftglobe", "Dust of Disappearance", "Dust of Dryness", "Dust of Sneezing and Choking", "Elemntal Gem", "Enspelled Armor Uncommon", "Uncommon Enspelled Staff", "Enspelled Weapon Uncommon", "Eversmoking Bottle", "Eyes of Charming", "Eyes of Minute Seeing", "Eyes of the Eagle", "Silver Raven Figurine of Wondrous Power", "Gauntlets of Ogre Power", "Gem of Brightness", "Gloves of Missile Snaring", "Gloves of Swimming and Climbing", "Gloves of Thievery", "Goggles of Night", "Hag Eye", "Hat of Disguise", "Headband of Intellect", "Helm of Comprehending Languages", "Helm of Telepathy", "Immovable Rod", "Doss Lute", "Fochlucan Bandore", "Mac-Fuirmidh Cittern", "Javelin of Lightning", "Keoghtom's Ointment", "Lantern of Revealing", "Mariner's Armor", "Medallion of Thoughts", "Nature's Mantle", "Necklace of Adaptation", "Oil of Slipperiness", "Pearl of Power", "Periapt of Health", "Periapt of Wound Closure", "Philter of Love", "Pipes of Haunting", "Pipes of the Sewers", "Potion of Animal Friendship", "Potion of Fire Breath", "Potion of Hill Giant Strength", "Potion of Growth", "Potion of Poison", "Potion of Puglism", "Potion of Resistance", "Potion of Water Breathing", "Quaal's Feather Token Uncommon", "Quiver of Ehlonna", "Ring of Jumping", "Ring of Mind Shielding", "Ring of Swimming", "Ring of Warmth", "Ring of Water Walking", "Robe of Useful Items", "Rod of the Pact Keeper +1", "Rope of Climbing", "Saddle of the Cavalier", "Sending Stones", "Sentinel Shield", "Shield +1", "Slippers of Spider Climbining", "Staff of the Adder", "Staff of the Python", "Stone of Good Luck", "Sword of Vengeance", "Trident of Fish Command", "Wand of Magic Detection", "Wand of Magic Missiles", "Wand of Secrets", "Wand of the War Mage +1", "Wand of Web", "Weapon +1", "Weapon of Warning", "Wind Fan", "Winged Boots", "Wraps of Unarmed Power +1"] +const rareList = ["Ammunition +2", "Amulet of Health", "Armor +1", "Armor of Resistance", "Armor of Vulnerability", "Arrow-Catching Shield", "Bag of Beans", "Belt of Dwarvenkind", "Belt of Hill Giant Strength", "Berserker Axe", "Boots of Levitation", "Boots of Speed", "Bowl of Commanding Water Elementals", "Bracers of Defense", "Brazier of Commanding Fire Elementals", "Cape of the Mountebank", "Censer of Controlling Air Elementals", "Chime of Opening", "Cloak of Displacement", "Cloak of the Bat", "Cube of Force", "Cube of Summoning", "Daern's Instant Fortress", "Dagger of Venom", "Dimensional Shackles", "Dragon Slayer", "Elixir of Health", "Elven Chain", "Enspelled Armor Rare", "Rare Enspelled Staff", "Enspelled Weapon Rare", "Figurine of Wondrous Power Rare", "Flame Tongue", "Folding Boat", "Gem of Seeing", "Giant Slayer", "Glamoured Studded Leather", "Helm of Teleportation", "Heward's Handy Haversack", "Horn of Blasting", "Silver Horn of Valhalla", "Brass Horn of Valhalla", "Horseshoes of Speed", "Canaith Mandolin", "Cli Lyre", "Ioun Stone Rare", "Iron Bands of Bilarro", "Mace of Disruption", "Mace of Smiting", "Mace of Terror", "Mantle of Spell Resistance", "Necklace of Fireballs", "Necklace of Prayer Beads", "Oil of Etherealness", "Periapt of Proof against Poison", "Portable Hole", "Potion of Clairvoyance", "Potion of Diminution", "Potion of Gaseous Form", "Potion of Frost Giant Strength", "Potion of Stone Giant Strength", "Potion of Fire Giant Strength", "Potion of Heroism", "Potion of Invisibility", "Potion of Invulnerability", "Potion of Mind Reading", "Quaal's Feather Token Rare", "Ring of Animal Influence", "Ring of Evasion", "Ring of Feather Falling", "Ring of Free Action", "Ring of Protection", "Ring of Resistance", "Ring of Spell Storing", "Ring of the Ram", "Ring of X-ray Vision", "Robe of Eyes", "Rod of Rulership", "Rod of the Pact Keeper +2", "Rope of Entanglement", "Scroll of Protection", "Shield +2", "Shield of Missile Attraction", "Staff of Charming", "Staff of Swarming Insects", "Staff of the Woodlands", "Staff of Withering", "Stone of Controlling Earth Elementals", "Sun Blade", "Sword of Life Stealing", "Tentacle Rod", "Vicious Weapon", "Wand of Binding", "Wand of Enemy Detection", "Wand of Fear", "Wand of Fireballs", "Wand of Lightning Bolts", "Wand of Paralysis", "Wand of Wonder", "Weapon +2", "Wings of Flying"] +const phenomenalList = ["Ammunition +3", "Ammunition of Slaying", "Amulet of the Planes", "Animated Shield", "Armor +2", "Bag of Devouring", "Belt of Frost Giant Strength", "Belt of Stone Giant Strength", "Belt of Fire Giant Strength", "Candle of Invocation", "Carpet of Flying", "Cauldron of Rebirth", "Cloak of Arachnida", "Crystal Ball", "Dancing Sword", "Demon Armor", "Dragon Scale Mail", "Dwarven Plate", "Dwarven Thrower", "Efreeti Bottle", "Energy Longbow", "Energy Shortbow", "Enspelled Armor Very Rare", "Enspelled Weapon Very Rare", "Executioner's Axe", "Obsidian Steed Figurine of Wondrous Power", "Frost Brand", "Hat of Many Spells", "Helm of Brilliance", "Bronze Horn of Valhalla", "Horseshoes of a Zephyr", "Ioun Stone Very Rare", "Lute of Thunderous Thumping", "Manual of Bodily Health", "Manual of Gainful Exercise", "Manual of Golems", "Manual of Quickness of Action", "Mirror of Life Trapping", "Nine Lives Stealer", "Nolzur's Marvelous Pigments", "Oathbow", "Oil of Sharpness", "Potion of Flying", "Potion of Cloud Giant Strength", "Potion of Greater Invisibility", "Potion of Longevity", "Potion of Speed", "Potion of Vitality", "Quarterstaff of the Acrobat", "Ring of Regeneration", "Ring of Shooting Stars", "Ring of Telekenisis", "Robe of Scintillating Colors", "Robe of Stars", "Rod of Absorption", "Rod of Alertness", "Rod of Security", "Rod of the Pact Keeper +3", "Scimitar of Speed", "Shield +3", "Shield of the Cavalier", "Spellguard Shield", "Spirit Board", "Staff of Fire", "Staff of Frost", "Staff of Power", "Staff of Striking", "Staff of Thunder and Lightning", "Sword of Sharpness", "Thunderous Greatclub", "Tome of Clear Thought", "Tome of Leadership and Influence", "Tome of Understanding", "Wand of Polymorph", "Weapon +3"] +const legendaryList = ["Apparatus of Kwalish", "Armor +3", "Armor of Invulnerability", "Belt of Cloud Giant Strength", "Belt of Storm Giant Strength", "Cloak of Invisibility", "Crystal Ball of Mind Reading", "Crystal Ball of Telepathy", "Crystal Ball of True Seeing", "Cubic Gate", "Deck of Many Things", "Defender", "Efreeti Chain", "Enspelled Armor Legendary", "Legendary Enspelled Staff", "Enspelled Weapon Legendary", "Hammer of Thunderbolts", "Holy Avenger", "Ioun Stone of Greater Absorption", "Ioun Stone of Mastery", "Ioun Stone of Regeneration", "Iron Flask", "Luck Blade", "Moonblade", "Plate Armor of Etherealness", "Potion of Storm Giant Strength", "Ring of Djinni Summoning", "Ring of Elemental Command", "Ring of Invisibility", "Ring of Spell Turning", "Ring of Three Wishes", "Robe of the Archmagi", "Rod of Lordly Might", "Rod of Resurrection", "Scarab of Protection", "Scroll of Titan Summoning", "Sovereign Glue", "Sphere of Annihilation", "Staff of the Magi", "Sword of Answering", "Talisman of Pure Good", "Talisman of the Sphere", "Talisman of Ultimate Evil", "Tome of the Stilled Tongue", "Universal Solvent", "Well of Many Worlds"] +const artifactList = ["Axe of the Dwarvish Lords", "Blackrazor", "Book of Exalted Deeds", "Book of Vile Darkness", "Demonomicon of Iggwilv", "Efreeti Chain", "Eye of Vecna", "Hand of Vecna", "Orb of Dragonkind", "Sword of Kas", "Wand of Orcus", "Wave", "Whelm"] + function getRandomInteger(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } @@ -20,6 +31,10 @@ function getRandomFromList(...choices) { return choices[getRandomInteger(0, choices.length - 1)] } +function numberWithCommas(x) { + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +} + function isAnumber(number) { return !isNaN(number) } @@ -329,10 +344,13 @@ function executeTurn(activeCharacter) { if (possessiveName == "Your") possessiveName = "your" if (activeCharacter.className != null) { + //player state.show = "none" return `\n[It is ${possessiveName} turn]\n` - } else { + } else if (activeCharacter.ally == false) { + //enemy var characters = state.characters.filter(x => x.health > 0) + characters.push(...state.allies.filter(x => x.health > 0)) var target = characters[getRandomInteger(0, characters.length - 1)] var areWord = target.name == "You" ? "are" : "is" var targetNameAdjustedCase = target.name == "You" ? "you" : toTitleCase(target.name) @@ -358,9 +376,52 @@ function executeTurn(activeCharacter) { var diceMatches = spell.match(/(?<=^.*)\d*d\d+((\+|-)\d+)?$/gi) if (diceMatches == null) text += `${activeCharacterName} casts spell ${spell}!` else { + var spell = spell.substring(0, spell.length - diceMatches[0].length) + if (hit) { + var damage = calculateRoll(diceMatches[0]) + target.health = Math.max(target.health - damage, 0) + + text += `\n[Character AC: ${target.ac} Attack roll: ${attack}]\n` + + text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} for ${damage} damage!` + + if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious!\n` + else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n` + } else text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} but misses!\n` + } + } + return text + } else { + //ally + var enemies = state.enemies.filter(x => x.health > 0) + var target = enemies[getRandomInteger(0, enemies.length - 1)] + var areWord = target.name == "You" ? "are" : "is" + var targetNameAdjustedCase = target.name == "You" ? "you" : toTitleCase(target.name) + var attack = calculateRoll(`1d20${activeCharacter.hitModifier > 0 ? "+" + activeCharacter.hitModifier : activeCharacter.hitModifier < 0 ? activeCharacter.hitModifier : ""}`) + var hit = attack >= target.ac + + var text = `\n[It is ${possessiveName} turn]\n` + if (getRandomBoolean() || activeCharacter.spells.length == 0) { + if (hit) { + state.blockCharacter = target + state.blockPreviousHealth = target.health + var damage = isNaN(activeCharacter.damage) ? calculateRoll(activeCharacter.damage) : activeCharacter.damage + target.health = Math.max(target.health - damage, 0) + + text += `\n[Enemy AC: ${target.ac} Attack roll: ${attack}]\n` + + text += `${activeCharacterName} attacks ${targetNameAdjustedCase} for ${damage} damage!\n` + if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious! \n` + else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n` + } else text += `${activeCharacterName} attacks ${targetNameAdjustedCase} but misses!\n` + } else { + var spell = activeCharacter.spells[getRandomInteger(0, activeCharacter.spells.length - 1)] + var diceMatches = spell.match(/(?<=^.*)\d*d\d+((\+|-)\d+)?$/gi) + if (diceMatches == null) text += `${activeCharacterName} casts spell ${spell}!` + else { + var spell = spell.substring(0, spell.length - diceMatches[0].length) if (hit) { var damage = calculateRoll(diceMatches[0]) - var spell = spell.substring(0, spell.length - diceMatches[0].length) target.health = Math.max(target.health - damage, 0) text += `\n[Character AC: ${target.ac} Attack roll: ${attack}]\n` @@ -2760,11 +2821,26 @@ function createEnemy(name, health, ac, hitModifier, damage, initiative, ...spell hitModifier: hitModifier, damage: damage, initiative: initiative, - spells: spells + spells: spells, + ally: false } return enemy } +function createAlly(name, health, ac, hitModifier, damage, initiative, ...spells) { + var ally = { + name: name, + health: health, + ac: ac, + hitModifier: hitModifier, + damage: damage, + initiative: initiative, + spells: spells, + ally: true + } + return ally +} + function getUniqueName(name) { const letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"] var letterIndex = 0 @@ -2792,6 +2868,11 @@ function createInitiativeOrder() { state.initiativeOrder.push(enemy) } + for (var ally of state.allies) { + if (ally.health <= 0) continue + state.initiativeOrder.push(ally) + } + state.initiativeOrder.sort(function(a, b) { return b.calculatedInitiative - a.calculatedInitiative; }); @@ -2848,7 +2929,19 @@ function getModifier(statValue) { } function findSpellCardIndex(name) { - return storyCards.findIndex((element) => element.type == "spell" && element.keys == name) + return storyCards.findIndex((element) => element.type == "spell" && element.title == name) +} + +function findSpellCard(name) { + return storyCards[findSpellCardIndex(name)] +} + +function findItemCardIndex(name, storyCardName) { + return storyCards.findIndex((element) => (element.type == "item" || element.type == "weapon" || element.type == "armor") && (element.title == name || element.title == storyCardName)) +} + +function findItemCard(name, storyCardName) { + return storyCards[findItemCardIndex(name, storyCardName)] } function stragedyCalculateScores() { @@ -2939,7 +3032,6 @@ function stragedyCalculateScores() { } function stragedyEnemyTurn() { - log(`enemy turn: ${state.stragedyEnemyHand}`) state.stragedyEnemySkipTurn = false state.stragedyEnemyTurnText = "" if (state.stragedyPlayerScore > 30) { @@ -3159,12 +3251,10 @@ function stragedyEnemyTurn() { } if (hand.length == 0) { - log("Enemy has no cards in hand") if (deck.length == 0) state.stragedyEnemyTurnText = stragedyEnemyRetire() else if (score > 30) state.stragedyEnemyTurnText = stragedyEnemyRetire() else state.stragedyEnemyTurnText = stragedyEnemyDrawCard() } else if (score > 30 && battlefield.length > 0) { - log("Enemy is going to bust") if (hasQueen && bestQueenCardToSave != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + bestQueenCardToSave) else if (hasPriest && bestPriestCardToSave != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "p" + bestPriestCardToSave) else if (hasJack && bestJackCardToSave != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "j" + bestJackCardToSave) @@ -3172,7 +3262,6 @@ function stragedyEnemyTurn() { else if (kingCards.length > 0 && kingNumberedCardsInHand.length > 0) state.stragedyEnemyTurnText = stragedyPlayCard(false, kingNumberedCardsInHand[kingNumberedCardsInHand.length - 1]) else state.stragedyEnemyTurnText = stragedyEnemyRetire() } else if (playerRetired && score < playerScore) { - log("Enemy is reacting to the player retiring while behind") if (hasJoker && playerScore < 30) state.stragedyEnemyTurnText = stragedyPlayCard(false, "?" + lowestNumberedBattlefieldCard) else if (hasQueen && bestQueenCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + bestQueenCardToBustPlayer) else if (hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard) @@ -3182,20 +3271,15 @@ function stragedyEnemyTurn() { else if (hasJoker && playerScore == 30) state.stragedyEnemyTurnText = stragedyPlayCard(false, "?" + lowestNumberedBattlefieldCard) else state.stragedyEnemyTurnText = stragedyEnemyRetire() } else if (playerRetired && score > playerScore && !hasJokerOnBattlefield) { - log("Enemy is reacting to the player retiring while ahead") state.stragedyEnemyTurnText = stragedyEnemyRetire() } else if (playerRetired && score == playerScore) { - log("Enemy is reacting to the player retiring while tied") if (highestNumberedHandCardToReach30 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, highestNumberedHandCardToReach30) else state.stragedyEnemyTurnText = stragedyEnemyRetire() } else if (score - playerScore > 20 && !hasJokerOnBattlefield) { - log("Enemy has a significant lead") state.stragedyEnemyTurnText = stragedyEnemyRetire() } else if (deck.length > 0 && hand.length == 1) { - log("Enemy only has one card in hand") state.stragedyEnemyTurnText = stragedyEnemyDiscardCard() } else if (hasNumberedCards && (score < playerScore || score < 15)) { - log("Enemy is behind or needs to reach at least 15") if (score < 20 && highestNumberedHandCardToReach20 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, highestNumberedHandCardToReach20) else if (highestNumberedHandCardToReach30 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, highestNumberedHandCardToReach30) else if (faceCardHandCount > 1 && hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard) @@ -3207,13 +3291,10 @@ function stragedyEnemyTurn() { else if (hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer) else stragedyEnemyRandom() } else if (score >= playerScore && hasWitch) { - log("Enemy has lead and has a witch") state.stragedyEnemyTurnText = stragedyPlayCard(false, "w") } else if (score >= playerScore && hasBrigand) { - log("Enemy has lead and has a brigand") state.stragedyEnemyTurnText = stragedyPlayCard(false, "b") } else if (highestNumberedHandCardToReach20 == null && hand.length > 0) { - log("Enemy can't reach 20 and has cards in hand") if (score >= 20 && score < playerScore && faceCardHandCount > 1 && hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard) else if (score >= 20 && score < playerScore && faceCardHandCount > 1 && hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer) else if (score >= 20 && score < playerScore && faceCardHandCount > 1 && hasQueen && highestNumberedBattlefieldCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + highestNumberedBattlefieldCard) @@ -3224,7 +3305,6 @@ function stragedyEnemyTurn() { else if (hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard) else state.stragedyEnemyTurnText = stragedyEnemyRetire() } else { - log("Enemy has ran out of options and is doing random stuff") state.stragedyEnemyTurnText = stragedyEnemyRandom() } @@ -3236,14 +3316,12 @@ function stragedyEnemyTurn() { } function stragedyEnemyDrawCard() { - log(`Enemy draw a card`) var card = state.stragedyEnemyDeck.pop() state.stragedyEnemyHand.push(card) return `\nThe opponent has drawn a card.\n` } function stragedyEnemyDiscardCard() { - log(`Enemy discard a card`) var hand = [...state.stragedyEnemyHand] var score = state.stragedyEnemyScore @@ -3276,13 +3354,11 @@ function stragedyEnemyDiscardCard() { } function stragedyEnemyRetire() { - log(`Enemy retire`) state.stragedyEnemyRetired = true return `\nThe opponent has retired at ${state.stragedyEnemyScore} points.\n` } function stragedyEnemyRandom(punish) { - log(`Enemy random`) var hand = [...state.stragedyEnemyHand] if (hand.length == 0) { @@ -3321,7 +3397,6 @@ function stragedyEnemyRandom(punish) { } function stragedyPlayerRandom(punish) { - log(`Player random`) var hand = [...state.stragedyPlayerHand] if (hand.length == 0) { @@ -3358,7 +3433,6 @@ function stragedyPlayerRandom(punish) { } function stragedyPlayerTurn(text) { - log(`player turn`) if (text.startsWith("d") && state.stragedyPlayerHand.length > 0) { if (state.stragedyPlayerDeck.length == 0) return "\nYou cannot discard if you have 0 cards in your deck.\n" @@ -3429,7 +3503,6 @@ function stragedyPlayerTurn(text) { } function stragedyPlayCard(player, text) { - log(`${player ? "Player" : "Enemy"} play card ${text}`) var character = getCharacter() if (player) { var battlefield = state.stragedyPlayerBattlefield @@ -3616,6 +3689,183 @@ function stragedyCheckForWin() { else state.stragedyWinner = "tie" } +const simpleMeleeWeapons = ["Club", "Dagger", "Greatclub", "Handaxe", "Javelin", "Light Hammer", "Mace", "Quarterstaff", "Sickle", "Spear", "Dart"] +const simpleRangedWeapons = ["Light Crossbow", "Shortbow", "Sling"] +const martialMeleeWeapons = ["Battleaxe", "Flail", "Glaive", "Greataxe", "Greatsword", "Halberd", "Lance", "Longsword", "Maul", "Morningstar", "Pike", "Rapier", "Scimitar", "Shortsword", "Trident", "Warhammer", "War Pick", "Whip"] +const martialRangedWeapons = ["Blowgun", "Hand Crossbow", "Heavy Crossbow", "Longbow", "Musket", "Pistol"] +const lightArmor = ["Padded Armor", "Leather Armor", "Studded Leather Armor"] +const mediumArmor = ["Hide Armor", "Chain Shirt", "Scale Mail", "Breastplate", "Half Plate Armor"] +const heavyArmor = ["Ring Mail", "Chain Mail", "Splint Armor", "Plate Armor"] +const ammunition = ["Arrow", "Bolt", "Bullet", "Needle"] + +function itemShopConvertGenericName(name) { + switch (name) { + case "Armor of Gleaming": + name = itemShopNameAddPrefix("of Gleaming", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Cast-Off Armor": + name = itemShopNameAddSuffix("Cast-Off", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Moon-Touched Sword": + name = itemShopNameAddSuffix("Moon-Touched", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Silvered Weapon": + name = itemShopNameAddSuffix("Silvered", ...simpleMeleeWeapons.concat(simpleRangedWeapons, martialMeleeWeapons, martialRangedWeapons)) + break + case "Smoldering Armor": + name = itemShopNameAddSuffix("Smoldering", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Sylvan Talon": + name = itemShopNameAddSuffix("Sylvan Talon", "Dagger", "Rapier", "Scimitar", "Shortsword", "Sickle", "Spear") + break + case "Walloping Ammunition": + name = itemShopNameAddSuffix("Walloping", ...ammunition) + quantity = 10 + break + case "Adamantine Armor": + name = itemShopNameAddSuffix("Adamantine", ...mediumArmor.concat(heavyArmor)) + break + case "Adamantine Weapon": + name = itemShopNameAddSuffix("Adamantine", ...martialMeleeWeapons.concat(ammunition, simpleMeleeWeapons)) + break + case "Ammunition +1": + name = itemShopNameAddPrefix("+1", ...ammunition) + break + case "Enspelled Armor Uncommon": + name = itemShopNameAddSuffix("Uncommon Enspelled", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Weapon Uncommon": + name = itemShopNameAddSuffix("Uncommon Enspelled", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Mariner's Armor": + name = itemShopNameAddSuffix("Mariner's", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Quaal's Feather Token Uncommon": + name = itemShopNameAddSuffix("Quaal's Feather Token of", "Anchor", "Fan", "Tree") + break + case "Sword of Vengeance": + name = itemShopNameAddPrefix("of Vengeance", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Weapon +1": + name = itemShopNameAddPrefix("+1", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Weapon of Warning": + name = itemShopNameAddPrefix("of Warning", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Ammunition +2": + name = itemShopNameAddPrefix("+2", ...ammunition) + break + case "Armor +1": + name = itemShopNameAddPrefix("+1", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Armor of Resistance": + name = itemShopNameAddPrefix("of Resistance", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Armor of Vulnerability": + name = itemShopNameAddPrefix("of Vulnerability", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Armor Rare": + name = itemShopNameAddSuffix("Rare Enspelled", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Weapon Rare": + name = itemShopNameAddSuffix("Rare Enspelled", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Figurine of Wondrous Power Rare": + name = itemShopNameAddPrefix("Figurine of Wondrous Power", "Bronze Griffon", "Ebony Fly", "Golden Lions", "Ivory Goats", "Marble Elephant", "Onyx Dog", "Serpentine Owl") + break + case "Flame Tongue": + name = itemShopNameAddSuffix("Flame Tongue", ...simpleMeleeWeapons.concat(martialMeleeWeapons)) + break + case "Giant Slayer": + name = itemShopNameAddSuffix("Giant Slayer", ...simpleMeleeWeapons.concat(martialMeleeWeapons)) + break + case "Ioun Stone Rare": + name = itemShopNameAddSuffix("Ioun Stone of", "Awareness", "Protection", "Reserve", "Sustenance") + break + case "Quaal's Feather Token Rare": + name = itemShopNameAddSuffix("Quaal's Feather Token of", "Bird", "Swan Boat", "Whip") + break + case "Sword of Life Stealing": + name = itemShopNameAddPrefix("of Life Stealing", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Sword of Wounding": + name = itemShopNameAddPrefix("of Wounding", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Vicious Weapon": + name = itemShopNameAddSuffix("Vicious", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Weapon +2": + name = itemShopNameAddPrefix("+2", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Ammunition +3": + name = itemShopNameAddPrefix("+3", ...ammunition) + break + case "Ammunition of Slaying": + let type = getRandomFromList("Aberration", "Beast", "Celestial", "Construct", "Dragon", "Elemental", "Humonoid", "Fey", "Fiend", "Giant", "Monstrosity", "Ooze", "Plant", "Undead") + name = itemShopNameAddPrefix(`of ${type} Slaying`, ...ammunition) + break + case "Armor +2": + name = itemShopNameAddPrefix("+2", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Dancing Sword": + name = itemShopNameAddSuffix("Dancing", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Demon Armor": + name = itemShopNameAddPrefix("Demon", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Armor Very Rare": + name = itemShopNameAddSuffix("Very Rare Enspelled", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Weapon Very Rare": + name = itemShopNameAddSuffix("Very Rare Enspelled", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Frost Brand": + name = itemShopNameAddSuffix("Frost Brand", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + case "Ioun Stone Very Rare": + name = itemShopNameAddSuffix("Ioun Stone of", "Absorption", "Fortitude", "Insight", "Intellect", "Leadership", "Strength") + break + case "Sword of Sharpness": + name = itemShopNameAddPrefix("of Sharpness", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar") + break + case "Weapon +3": + name = itemShopNameAddPrefix("+3", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Armor +3": + name = itemShopNameAddPrefix("+3", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Armor Legendary": + name = itemShopNameAddSuffix("Very Rare Enspelled", ...lightArmor.concat(mediumArmor, heavyArmor)) + break + case "Enspelled Weapon Legendary": + name = itemShopNameAddSuffix("Legendary Enspelled", ...simpleMeleeWeapons.concat(martialMeleeWeapons, simpleRangedWeapons, martialRangedWeapons)) + break + case "Luck Blade": + name = itemShopNameAddPrefix("Luck Blade", "Glaive", "Greatsword", "Longsword", "Rapier", "Scimitar", "Sickle", "Shortsword") + break + case "Moonblade": + name = itemShopNameAddPrefix("Moonblade", "Greatsword", "Longsword", "Rapier", "Scimitar", "Shortsword") + break + } + return name +} + +function itemShopNameAddPrefix(name, ...prefixes) { + return getRandomFromList(...prefixes) + " " + name +} + +function itemShopNameAddSuffix(name, ...suffixes) { + return name + " " + getRandomFromList(...suffixes) +} + +function findItemShopDeals(className, bought) { + return state.itemShopDeals.filter(element => element.className == className && (bought == null || element.bought == bought)) +} + +function findSpellShopDeals(className, level, bought) { + return state.spellShopDeals.filter(element => element.className == className && element.level == level && (bought == null || element.bought == bought)) +} + String.prototype.replaceAt = function(index, replacement) { return this.substring(0, index) + replacement + this.substring(index + replacement.length); } @@ -3848,4 +4098,5974 @@ function generateName(genre, male) { return nordicFemaleNames[state.nordicFemaleIndex++] } } -} \ No newline at end of file +} + + +/* +Auto-Cards +Made by LewdLeah on May 21, 2025 +This AI Dungeon script automatically creates and updates plot-relevant story cards while you play +General-purpose usefulness and compatibility with other scenarios/scripts were my design priorities +Auto-Cards is fully open-source, please copy for use within your own projects! ā¤ļø +*/ +function AutoCards(inHook, inText, inStop) { + "use strict"; + /* + Default Auto-Cards settings + Feel free to change these settings to customize your scenario's default gameplay experience + The default values for your scenario are specified below: + */ + + // Is Auto-Cards already enabled when the adventure begins? + const DEFAULT_DO_AC = true + // (true or false) + + // Pin the "Configure Auto-Cards" story card at the top of the player's story cards list? + const DEFAULT_PIN_CONFIGURE_CARD = true + // (true or false) + + // Minimum number of turns in between automatic card generation events? + const DEFAULT_CARD_CREATION_COOLDOWN = 22 + // (0 to 9999) + + // Use a bulleted list format for newly generated card entries? + const DEFAULT_USE_BULLETED_LIST_MODE = true + // (true or false) + + // Maximum allowed length for newly generated story card entries? + const DEFAULT_GENERATED_ENTRY_LIMIT = 750 + // (200 to 2000) + + // Do newly generated cards have memory updates enabled by default? + const DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES = true + // (true or false) + + // Default character limit before the card's memory bank is summarized? + const DEFAULT_NEW_CARDS_MEMORY_LIMIT = 2750 + // (1750 to 9900) + + // Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new) + const DEFAULT_MEMORY_COMPRESSION_RATIO = 25 + // (20 to 1250) + + // Ignore all-caps during title candidate detection? + const DEFAULT_IGNORE_ALL_CAPS_TITLES = true + // (true or false) + + // Should player input actions (Do/Say/Story) be considered during title detection? + const DEFAULT_DETECT_TITLES_FROM_INPUTS = false + // (true or false) + + // How many (minimum) actions in the past does Auto-Cards look for named entities? + const DEFAULT_MINIMUM_LOOK_BACK_DISTANCE = 5 + // (2 to 88) + + // Is Live Script Interface v2 enabled? + const DEFAULT_DO_LSI_V2 = false + // (true or false) + + // Should the "Debug Data" story card be visible? + const DEFAULT_SHOW_DEBUG_DATA = false + // (true or false) + + // AI prompt used to generate new story card entries? + const DEFAULT_CARD_GENERATION_PROMPT = prose( + "-----", + "", + "", + "# Stop the story and ignore previous instructions. Write a brief and coherent informational entry for %{title} following these instructions:", + "- Write only third-person pure prose information about %{title} using complete sentences with correct punctuation", + "- Avoid short-term temporary details or appearances, instead focus on plot-significant information", + "- Prioritize story-relevant details about %{title} first to ensure seamless integration with the previous plot", + "- Create new information based on the context and story direction", + "- Mention %{title} in every sentence", + "- Use semicolons if needed", + "- Add additional details about %{title} beneath incomplete entries", + "- Be concise and grounded", + "- Imitate the story's writing style and infer the reader's preferences", + "", + "Continue the entry for %{title} below while avoiding repetition:", + "%{entry}" + ); // (mimic this multi-line "text" format) + + // AI prompt used to summarize a given story card's memory bank? + const DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT = prose( + "-----", + "", + "", + "# Stop the story and ignore previous instructions. Summarize and condense the given paragraph into a narrow and focused memory passage while following these guidelines:", + "- Ensure the passage retains the core meaning and most essential details", + "- Use the third-person perspective", + "- Prioritize information-density, accuracy, and completeness", + "- Remain brief and concise", + "- Write firmly in the past tense", + "- The paragraph below pertains to old events from far earlier in the story", + "- Integrate %{title} naturally within the memory; however, only write about the events as they occurred", + "- Only reference information present inside the paragraph itself, be specific", + "", + "Write a summarized old memory passage for %{title} based only on the following paragraph:", + "\"\"\"", + "%{memory}", + "\"\"\"", + "Summarize below:" + ); // (mimic this multi-line "text" format) + + // Titles banned from future card generation attempts? + const DEFAULT_BANNED_TITLES_LIST = ( + "North, East, South, West, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, January, February, March, April, May, June, July, August, September, October, November, December" + ); // (mimic this comma-list "text" format) + + // Default story card "type" used by Auto-Cards? (does not matter) + const DEFAULT_CARD_TYPE = "class" + // ("text") + + // Should titles mentioned in the "opening" plot component be banned from future card generation by default? + const DEFAULT_BAN_TITLES_FROM_OPENING = true + // (true or false) + + //————————————————————————————————————————————————————————————————————————————————— + + /* + Useful API functions for coders (otherwise ignore) + Here's what each one does in plain terms: + + AutoCards().API.postponeEvents(); + Pauses Auto-Cards activity for n many turns + + AutoCards().API.emergencyHalt(); + Emergency stop or resume + + AutoCards().API.suppressMessages(); + Hides Auto-Cards toasts by preventing assignment to state.message + + AutoCards().API.debugLog(); + Writes to the debug log card + + AutoCards().API.toggle(); + Turns Auto-Cards on/off + + AutoCards().API.generateCard(); + Initiates AI generation of the requested card + + AutoCards().API.redoCard(); + Regenerates an existing card + + AutoCards().API.setCardAsAuto(); + Flags or unflags a card as automatic + + AutoCards().API.addCardMemory(); + Adds a memory to a specific card + + AutoCards().API.eraseAllAutoCards(); + Deletes all auto-cards + + AutoCards().API.getUsedTitles(); + Lists all current card titles + + AutoCards().API.getBannedTitles(); + Shows your current banned titles list + + AutoCards().API.setBannedTitles(); + Replaces the banned titles list with a new list + + AutoCards().API.buildCard(); + Makes a new card from scratch, using exact parameters + + AutoCards().API.getCard(); + Finds cards that match a filter + + AutoCards().API.eraseCard(); + Deletes cards matching a filter + */ + + /*** Postpones internal Auto-Cards events for a specified number of turns + * + * @function + * @param {number} turns A non-negative integer representing the number of turns to postpone events + * @returns {Object} An object containing cooldown values affected by the postponement + * @throws {Error} If turns is not a non-negative integer + */ + // AutoCards().API.postponeEvents(); + + /*** Sets or clears the emergency halt flag to pause Auto-Cards operations + * + * @function + * @param {boolean} shouldHalt A boolean value indicating whether to engage (true) or disengage (false) emergency halt + * @returns {boolean} The value that was set + * @throws {Error} If called from within isolateLSIv2 scope or with a non-boolean argument + */ + // AutoCards().API.emergencyHalt(); + + /*** Enables or disables state.message assignments from Auto-Cards + * + * @function + * @param {boolean} shouldSuppress If true, suppresses all Auto-Cards messages; false enables them + * @returns {Array} The current pending messages after setting suppression + * @throws {Error} If shouldSuppress is not a boolean + */ + // AutoCards().API.suppressMessages(); + + /*** Logs debug information to the "Debug Log card console + * + * @function + * @param {...any} args Arguments to log for debugging purposes + * @returns {any} The story card object reference + */ + // AutoCards().API.debugLog(); + + /*** Toggles Auto-Cards behavior or sets it directly + * + * @function + * @param {boolean|null|undefined} toggleType If undefined, toggles the current state. If boolean or null, sets the state accordingly + * @returns {boolean|null|undefined} The state that was set or inferred + * @throws {Error} If toggleType is not a boolean, null, or undefined + */ + // AutoCards().API.toggle(); + + /*** Generates a new card using optional prompt details or a card request object + * + * This function supports two usage modes: + * + * 1. Object Mode: + * Pass a single object containing card request parameters. The only mandatory property is "title" + * All other properties are optional and customize the card generation + * + * Example: + * AutoCards().API.generateCard({ + * type: "character", // The category or type of the card; defaults to "class" if omitted + * title: "Leah the Lewd", // The card's title (required) + * keysStart: "Lewd,Leah", // Optional trigger keywords associated with the card + * entryStart: "You are a woman named Leah.", // Existing content to prepend to the AI-generated entry + * entryPrompt: "", // Global prompt guiding AI content generation + * entryPromptDetails: "Focus on Leah's works of artifice and ingenuity", // Additional prompt info + * entryLimit: 750, // Target character length for the AI-generated entry + * description: "Player character!", // Freeform notes + * memoryStart: "Leah purchased a new sweater.", // Existing memory content + * memoryUpdates: true, // Whether the card's memory bank will update on its own + * memoryLimit: 2750 // Preferred memory bank size before summarization/compression + * }); + * + * 2. String Mode: + * Pass a string as the title and optionally two additional strings to specify prompt details + * This mode is shorthand for quick card generation without an explicit card request object + * + * Examples: + * AutoCards().API.generateCard("Leah the Lewd"); + * AutoCards().API.generateCard("Leah the Lewd", "Focus on Leah's works of artifice and ingenuity"); + * AutoCards().API.generateCard( + * "Leah the Lewd", + * "Focus on Leah's works of artifice and ingenuity", + * "You are a woman named Leah." + * ); + * + * @function + * @param {Object|string} request Either a fully specified card request object or a string title + * @param {string} [extra1] Optional detailed prompt text when using string mode + * @param {string} [extra2] Optional entry start text when using string mode + * @returns {boolean} Returns true if the generation attempt succeeded, false otherwise + * @throws {Error} Throws if called with invalid arguments or missing a required title property + */ + // AutoCards().API.generateCard(); + + /*** Regenerates a card by title or object reference, optionally preserving or modifying its input info + * + * @function + * @param {Object|string} request Either a fully specified card request object or a string title for the card to be regenerated + * @param {boolean} [useOldInfo=true] If true, preserves old info in the new generation; false omits it + * @param {string} [newInfo=""] Additional info to append to the generation prompt + * @returns {boolean} True if regeneration succeeded; false otherwise + * @throws {Error} If the request format is invalid, or if the second or third parameters are the wrong types + */ + // AutoCards().API.redoCard(); + + /*** Flags or unflags a card as an auto-card, controlling its automatic generation behavior + * + * @function + * @param {Object|string} targetCard The card object or title to mark/unmark as an auto-card + * @param {boolean} [setOrUnset=true] If true, marks the card as an auto-card; false removes the flag + * @returns {boolean} True if the operation succeeded; false if the card was invalid or already matched the target state + * @throws {Error} If the arguments are invalid types + */ + // AutoCards().API.setCardAsAuto(); + + /*** Appends a memory to a story card's memory bank + * + * @function + * @param {Object|string} targetCard A card object reference or title string + * @param {string} newMemory The memory text to add + * @returns {boolean} True if the memory was added; false if it was empty, already present, or the card was not found + * @throws {Error} If the inputs are not a string or valid card object reference + */ + // AutoCards().API.addCardMemory(); + + /*** Removes all previously generated auto-cards and resets various states + * + * @function + * @returns {number} The number of cards that were removed + */ + // AutoCards().API.eraseAllAutoCards(); + + /*** Retrieves an array of titles currently used by the adventure's story cards + * + * @function + * @returns {Array} An array of strings representing used titles + */ + // AutoCards().API.getUsedTitles(); + + /*** Retrieves an array of banned titles + * + * @function + * @returns {Array} An array of banned title strings + */ + // AutoCards().API.getBannedTitles(); + + /*** Sets the banned titles array, replacing any previously banned titles + * + * @function + * @param {string|Array} titles A comma-separated string or array of strings representing titles to ban + * @returns {Object} An object containing oldBans and newBans arrays + * @throws {Error} If the input is neither a string nor an array of strings + */ + // AutoCards().API.setBannedTitles(); + + /*** Creates a new story card with the specified parameters + * + * @function + * @param {string|Object} title Card title string or full card template object containing all fields + * @param {string} [entry] The entry text for the card + * @param {string} [type] The card type (e.g., "character", "location") + * @param {string} [keys] The keys (triggers) for the card + * @param {string} [description] The notes or memory bank of the card + * @param {number} [insertionIndex] Optional index to insert the card at a specific position within storyCards + * @returns {Object|null} The created card object reference, or null if creation failed + */ + // AutoCards().API.buildCard(); + + /*** Finds and returns story cards satisfying a user-defined condition + * Example: + * const leahCard = AutoCards().API.getCard(card => (card.title === "Leah")); + * + * @function + * @param {Function} predicate A function which takes a card and returns true if it matches + * @param {boolean} [getAll=false] If true, returns all matching cards; otherwise returns the first match + * @returns {Object|Array|null} A single card object reference, an array of cards, or null if no match is found + * @throws {Error} If the predicate is not a function or getAll is not a boolean + */ + // AutoCards().API.getCard(); + + /*** Removes story cards based on a user-defined condition or by direct reference + * Example: + * AutoCards().API.eraseCard(card => (card.title === "Leah")); + * + * @function + * @param {Function|Object} predicate A predicate function or a card object reference + * @param {boolean} [eraseAll=false] If true, removes all matching cards; otherwise removes the first match + * @returns {boolean|number} True if a single card was removed, false if none matched, or the number of cards erased + * @throws {Error} If the inputs are not a valid predicate function, card object, or boolean + */ + // AutoCards().API.eraseCard(); + + //————————————————————————————————————————————————————————————————————————————————— + + /* + To everyone who helped, thank you: + + AHotHamster22 + Most extensive testing, feedback, ideation, and kindness + + BinKompliziert + UI feedback + + Boo + Discord communication + + bottledfox + API ideas for alternative card generation use-cases + + Bruno + Most extensive testing, feedback, ideation, and kindness + https://play.aidungeon.com/profile/Azuhre + + Burnout + Implementation improvements, algorithm ideas, script help, and LSIv2 inspiration + + bweni + Testing + + DebaczX + Most extensive testing, feedback, ideation, and kindness + + Dirty Kurtis + Card entry generation prompt engineering + + Dragranis + Provided the memory dataset used for boundary calibration + + effortlyss + Data, testing, in-game command ideas, config settings, and other UX improvements + + Hawk + Grammar and special-cased proper nouns + + Idle Confusion + Testing + https://play.aidungeon.com/profile/Idle%20Confusion + + ImprezA + Most extensive testing, feedback, ideation, and kindness + https://play.aidungeon.com/profile/ImprezA + + Kat-Oli + Title parsing, grammar, and special-cased proper nouns + + KryptykAngel + LSIv2 ideas + https://play.aidungeon.com/profile/KryptykAngel + + Mad19pumpkin + API ideas + https://play.aidungeon.com/profile/Mad19pumpkin + + Magic + Implementation and syntax improvements + https://play.aidungeon.com/profile/MagicOfLolis + + Mirox80 + Testing, feedback, and scenario integration ideas + https://play.aidungeon.com/profile/Mirox80 + + Nathaniel Wyvern + Testing + https://play.aidungeon.com/profile/NathanielWyvern + + NobodyIsUgly + All-caps title parsing feedback + + OnyxFlame + Card memory bank implementation ideas and special-cased proper nouns + + Purplejump + API ideas for deep integration with other AID scripts + + Randy Viosca + Context injection and card memory bank structure + https://play.aidungeon.com/profile/Random_Variable + + RustyPawz + API ideas for simplified card interaction + https://play.aidungeon.com/profile/RustyPawz + + sinner + Testing + + Sleepy pink + Testing and feedback + https://play.aidungeon.com/profile/Pinkghost + + Vutinberg + Memory compression ideas and prompt engineering + + Wilmar + Card entry generation and memory summarization prompt engineering + + Yi1i1i + Idea for the redoCard API function and "/ac redo" in-game command + + A note to future individuals: + If you fork or modify Auto-Cards... Go ahead and put your name here too! Yay! 🄰 + */ + + //————————————————————————————————————————————————————————————————————————————————— + + /* + The code below implements Auto-Cards + Enjoy! ā¤ļø + */ + + // My class definitions are hoisted by wrapper functions because it's less ugly (lol) + const Const = hoistConst(); + const O = hoistO(); + const Words = hoistWords(); + const StringsHashed = hoistStringsHashed(); + const Internal = hoistInternal(); + // AutoCards has an explicitly immutable domain: HOOK, TEXT, and STOP + const HOOK = inHook; + const TEXT = ((typeof inText === "string") && inText) || "\n"; + const STOP = (inStop === true); + // AutoCards returns a pseudoimmutable codomain which is initialized only once before being read and returned + const CODOMAIN = new Const().declare(); + // Transient sets for high-performance lookup + const [used, bans, auto, forenames, surnames] = Array.from({length: 5}, () => new Set()); + // Holds a reference to the data card singleton, remains unassigned unless required + let data = null; + // Validate globalThis.text + text = ((typeof text === "string") && text) || "\n"; + // Container for the persistent state of AutoCards + const AC = (function() { + if (state.LSIv2) { + // The Auto-Cards external API is also available from within the inner scope of LSIv2 + // Call with AutoCards().API.nameOfFunction(yourArguments); + return state.LSIv2; + } else if (state.AutoCards) { + // state.AutoCards is prioritized for performance + const ac = state.AutoCards; + delete state.AutoCards; + return ac; + } + const dataVariants = getDataVariants(); + data = getSingletonCard(false, O.f({...dataVariants.critical}), O.f({...dataVariants.debug})); + // Deserialize the state of Auto-Cards from the data card + const ac = (function() { + try { + return JSON.parse(data?.description); + } catch { + return null; + } + })(); + // If the deserialized state fails to match the following structure, fallback to defaults + if (validate(ac, O.f({ + config: [ + "doAC", "deleteAllAutoCards", "pinConfigureCard", "addCardCooldown", "bulletedListMode", "defaultEntryLimit", "defaultCardsDoMemoryUpdates", "defaultMemoryLimit", "memoryCompressionRatio", "ignoreAllCapsTitles", "readFromInputs", "minimumLookBackDistance", "LSIv2", "showDebugData", "generationPrompt", "compressionPrompt", "defaultCardType" + ], + signal: [ + "emergencyHalt", "forceToggle", "overrideBans", "swapControlCards", "recheckRetryOrErase", "maxChars", "outputReplacement", "upstreamError" + ], + generation: [ + "cooldown", "completed", "permitted", "workpiece", "pending" + ], + compression: [ + "completed", "titleKey", "vanityTitle", "responseEstimate", "lastConstructIndex", "oldMemoryBank", "newMemoryBank" + ], + message: [ + "previous", "suppress", "pending", "event" + ], + chronometer: [ + "turn", "step", "amnesia", "postpone" + ], + database: { + titles: [ + "used", "banned", "candidates", "lastActionParsed", "lastTextHash", "pendingBans", "pendingUnbans" + ], + memories: [ + "associations", "duplicates" + ] + } + }))) { + // The deserialization was a success + return ac; + } + function validate(obj, finalKeys) { + if ((typeof obj !== "object") || (obj === null)) { + return false; + } else { + return Object.entries(finalKeys).every(([key, value]) => { + if (!(key in obj)) { + return false; + } else if (Array.isArray(value)) { + return value.every(finalKey => { + return (finalKey in obj[key]); + }); + } else { + return validate(obj[key], value); + } + }); + } + } + // AC is malformed, reinitialize with default values + return { + // In-game configurable parameters + config: getDefaultConfig(), + // Collection of various short-term signals passed forward in time + signal: { + // API: Suspend nearly all Auto-Cards processes + emergencyHalt: false, + // API: Forcefully toggle Auto-Cards on or off + forceToggle: null, + // API: Banned titles were externally overwritten + overrideBans: 0, + // Signal the construction of the opposite control card during the upcoming onOutput hook + swapControlCards: false, + // Signal a limited recheck of recent title candidates following a retry or erase + recheckRetryOrErase: false, + // Signal an upcoming onOutput text replacement + outputReplacement: "", + // info.maxChars is only defined onContext but must be accessed during other hooks too + maxChars: Math.abs(info?.maxChars || 3200), + // An error occured within the isolateLSIv2 scope during an earlier hook + upstreamError: "" + }, + // Moderates the generation of new story card entries + generation: { + // Number of story progression turns between card generations + cooldown: validateCooldown(underQuarterInteger(validateCooldown(DEFAULT_CARD_CREATION_COOLDOWN))), + // Continues prompted so far + completed: 0, + // Upper limit on consecutive continues + permitted: 34, + // Properties of the incomplete story card + workpiece: O.f({}), + // Pending card generations + pending: [], + }, + // Moderates the compression of story card memories + compression: { + // Continues prompted so far + completed: 0, + // A title header reference key for this auto-card + titleKey: "", + // The full and proper title + vanityTitle: "", + // Response length estimate used to compute # of outputs remaining + responseEstimate: 1400, + // Indices [0, n] of oldMemoryBank memories used to build the current memory construct + lastConstructIndex: -1, + // Bank of card memories awaiting compression + oldMemoryBank: [], + // Incomplete bank of newly compressed card memories + newMemoryBank: [], + }, + // Prevents incompatibility issues borne of state.message modification + message: { + // Last turn's state.message + previous: getStateMessage(), + // API: Allow Auto-Cards to post messages? + suppress: false, + // Pending Auto-Cards message(s) + pending: (function() { + if (DEFAULT_DO_AC !== false) { + const startupMessage = "Enabled! You may now edit the \"Configure Auto-Cards\" story card"; + logEvent(startupMessage); + return [startupMessage]; + } else { + return []; + } + })(), + // Counter to track all Auto-Cards message events + event: 0 + }, + // Timekeeper used for temporal events + chronometer: { + // Previous turn's measurement of info.actionCount + turn: getTurn(), + // Whether or not various turn counters should be stepped (falsified by retry actions) + step: true, + // Number of consecutive turn interruptions + amnesia: 0, + // API: Postpone Auto-Cards externalities for n many turns + postpone: 0, + }, + // Scalable atabase to store dynamic game information + database: { + // Words are pale shadows of forgotten names. As names have power, words have power + titles: { + // A transient array of known titles parsed from card titles, entry title headers, and trigger keywords + used: [], + // Titles banned from future card generation attempts and various maintenance procedures + banned: getDefaultConfigBans(), + // Potential future card titles and their turns of occurrence + candidates: [], + // Helps avoid rechecking the same action text more than once, generally + lastActionParsed: -1, + // Ensures weird combinations of retry/erase events remain predictable + lastTextHash: "%@%", + // Newly banned titles which will be added to the config card + pendingBans: [], + // Currently banned titles which will be removed from the config card + pendingUnbans: [] + }, + // Memories are parsed from context and handled by various operations (basically magic) + memories: { + // Dynamic store of 'story card -> memory' conceptual relations + associations: {}, + // Serialized hashset of the 2000 most recent near-duplicate memories purged from context + duplicates: "%@%" + } + } + }; + })(); + O.f(AC); + O.s(AC.config); + O.s(AC.signal); + O.s(AC.generation); + O.s(AC.generation.workpiece); + AC.generation.pending.forEach(request => O.s(request)); + O.s(AC.compression); + O.s(AC.message); + O.s(AC.chronometer); + O.f(AC.database); + O.s(AC.database.titles); + O.s(AC.database.memories); + if (!HOOK) { + globalThis.stop ??= false; + AC.signal.maxChars = Math.abs(info?.maxChars || AC.signal.maxChars); + if (HOOK === null) { + if (/Recent\s*Story\s*:/i.test(text)) { + // AutoCards(null) is always invoked once after being declared within the shared library + // Context must be cleaned before passing text to the context modifier + // This measure is taken to ensure compatability with other scripts + // First, remove all command, continue, and comfirmation messages from the context window + text = (text + // Hide the guide + .replace(/\s*>>>\s*Detailed\s*Guide\s*:[\s\S]*?<<<\s*/gi, "\n\n") + // Excise all /AC command messages + .replace(/\s*>>>\s*Auto-Cards\s*has\s*been\s*enabled!\s*<<<\s*/gi, " ") + .replace(/^.*\/\s*A\s*C.*$/gmi, "%@%") + .replace(/\s*%@%\s*/g, " ") + // Consolidate all consecutive continue messages into placeholder substrings + .replace(/(?:(?:\s*>>>\s*please\s*select\s*"continue"\s*\([\s\S]*?\)\s*<<<\s*)+)/gi, message => { + // Replace all continue messages with %@+%-patterned substrings + return ( + // The # of "@" symbols corresponds with the # of consecutive continue messages + "%" + "@".repeat( + // Count the number of consecutive continue message occurrences + (message.match(/>>>\s*please\s*select\s*"continue"\s*\([\s\S]*?\)\s*<< { + // Check the case of the next char following the match to decide how to replace it + let i = matchIndex + match.length; + let nextChar = intermediateText[i]; + if (nextChar === undefined) { + return " "; + } else if (/^[A-Z]$/.test(nextChar)) { + // Probably denotes a new sentence/paragraph + return "\n\n"; + } else if (/^[a-z]$/.test(nextChar)) { + return " "; + } + // The first nextChar was a weird punctuation char, find the next non-whitespace char + do { + i++; + nextChar = intermediateText[i]; + if (nextChar === undefined) { + return " "; + } + } while (/\s/.test(nextChar)); + if (nextChar === nextChar.toUpperCase()) { + // Probably denotes a new sentence/paragraph + return "\n\n"; + } + // Returning " " probably indicates a previous output's incompleteness + return " "; + }) + // Remove all comfirmation requests and responses + .replace(/\s*\n*.*CONFIRM\s*DELETE.*\n*\s*/gi, confirmation => { + if (confirmation.includes("<<<")) { + return " "; + } else { + return ""; + } + }) + // Remove dumb memories from the context window + // (Latitude, if you're reading this, please give us memoryBank read/write access 😭) + .replace(/(Memories\s*:)\s*([\s\S]*?)\s*(Recent\s*Story\s*:|$)/i, (_, left, memories, right) => { + return (left + "\n" + (memories + .split("\n") + .filter(memory => { + const lowerMemory = memory.toLowerCase(); + return !( + (lowerMemory.includes("select") && lowerMemory.includes("continue")) + || lowerMemory.includes(">>>") || lowerMemory.includes("<<<") + || lowerMemory.includes("lsiv2") + ); + }) + .join("\n") + ) + (function() { + if (right !== "") { + return "\n\n" + right; + } else { + return ""; + } + })()); + }) + // Remove LSIv2 error messages + .replace(/(?:\s*>>>[\s\S]*?<<<\s*)+/g, " ") + ); + if (!shouldProceed()) { + // Whenever Auto-Cards is inactive, remove auto card title headers from contextualized story card entries + text = (text + .replace(/\s*{\s*titles?\s*:[\s\S]*?}\s*/gi, "\n\n") + .replace(/World\s*Lore\s*:\s*/i, "World Lore:\n") + ); + // Otherwise, implement a more complex version of this step within the (HOOK === "context") scope of AutoCards + } + } + CODOMAIN.initialize(null); + } else { + // AutoCards was (probably) called without arguments, return an external API to allow other script creators to programmatically govern the behavior of Auto-Cards from elsewhere within their own scripts + CODOMAIN.initialize({API: O.f(Object.fromEntries(Object.entries({ + // Call these API functions like so: AutoCards().API.nameOfFunction(argumentsOfFunction) + /*** Postpones internal Auto-Cards events for a specified number of turns + * + * @function + * @param {number} turns A non-negative integer representing the number of turns to postpone events + * @returns {Object} An object containing cooldown values affected by the postponement + * @throws {Error} If turns is not a non-negative integer + */ + postponeEvents: function(turns) { + if (Number.isInteger(turns) && (0 <= turns)) { + AC.chronometer.postpone = turns; + } else { + throw new Error( + "Invalid argument: \"" + turns + "\" -> AutoCards().API.postponeEvents() must be be called with a non-negative integer" + ); + } + return { + postponeAllCooldown: turns, + addCardRealCooldown: AC.generation.cooldown, + addCardNextCooldown: AC.config.addCardCooldown + }; + }, + /*** Sets or clears the emergency halt flag to pause Auto-Cards operations + * + * @function + * @param {boolean} shouldHalt A boolean value indicating whether to engage (true) or disengage (false) emergency halt + * @returns {boolean} The value that was set + * @throws {Error} If called from within isolateLSIv2 scope or with a non-boolean argument + */ + emergencyHalt: function(shouldHalt) { + const scopeRestriction = new Error(); + if (scopeRestriction.stack && scopeRestriction.stack.includes("isolateLSIv2")) { + throw new Error( + "Scope restriction: AutoCards().API.emergencyHalt() cannot be called from within LSIv2 (prevents deadlock) but you're more than welcome to use AutoCards().API.postponeEvents() instead!" + ); + } else if (typeof shouldHalt === "boolean") { + AC.signal.emergencyHalt = shouldHalt; + } else { + throw new Error( + "Invalid argument: \"" + shouldHalt + "\" -> AutoCards().API.emergencyHalt() must be called with a boolean true or false" + ); + } + return shouldHalt; + }, + /*** Enables or disables state.message assignments from Auto-Cards + * + * @function + * @param {boolean} shouldSuppress If true, suppresses all Auto-Cards messages; false enables them + * @returns {Array} The current pending messages after setting suppression + * @throws {Error} If shouldSuppress is not a boolean + */ + suppressMessages: function(shouldSuppress) { + if (typeof shouldSuppress === "boolean") { + AC.message.suppress = shouldSuppress; + } else { + throw new Error( + "Invalid argument: \"" + shouldSuppress + "\" -> AutoCards().API.suppressMessages() must be called with a boolean true or false" + ); + } + return AC.message.pending; + }, + /*** Logs debug information to the "Debug Log" console card + * + * @function + * @param {...any} args Arguments to log for debugging purposes + * @returns {any} The story card object reference + */ + debugLog: function(...args) { + return Internal.debugLog(...args); + }, + /*** Toggles Auto-Cards behavior or sets it directly + * + * @function + * @param {boolean|null|undefined} toggleType If undefined, toggles the current state. If boolean or null, sets the state accordingly + * @returns {boolean|null|undefined} The state that was set or inferred + * @throws {Error} If toggleType is not a boolean, null, or undefined + */ + toggle: function(toggleType) { + if (toggleType === undefined) { + if (AC.signal.forceToggle !== null) { + AC.signal.forceToggle = !AC.signal.forceToggle; + } else if (AC.config.doAC) { + AC.signal.forceToggle = false; + } else { + AC.signal.forceToggle = true; + } + } else if ((toggleType === null) || (typeof toggleType === "boolean")) { + AC.signal.forceToggle = toggleType; + } else { + throw new Error( + "Invalid argument: \"" + toggleType + "\" -> AutoCards().API.toggle() must be called with either A) a boolean true or false, B) a null argument, or C) no arguments at all (undefined)" + ); + } + return toggleType; + }, + /*** Generates a new card using optional prompt details or a request object + * + * @function + * @param {Object|string} request A request object with card parameters or a string representing the title + * @param {string} [extra1] Optional entryPromptDetails if using string mode + * @param {string} [extra2] Optional entryStart if using string mode + * @returns {boolean} Did the generation attempt succeed or fail + * @throws {Error} If the request is not valid or missing a title + */ + generateCard: function(request, extra1, extra2) { + // Function call guide: + // AutoCards().API.generateCard({ + // // All properties except 'title' are optional + // type: "card type, defaults to 'class' for ease of filtering", + // title: "card title", + // keysStart: "preexisting card triggers", + // entryStart: "preexisting card entry", + // entryPrompt: "prompt the AI will use to complete this entry", + // entryPromptDetails: "extra details to include with this card's prompt", + // entryLimit: 750, // target character count for the generated entry + // description: "card notes", + // memoryStart: "preexisting card memory", + // memoryUpdates: true, // card updates when new relevant memories are formed + // memoryLimit: 2750, // max characters before the card memory is compressed + // }); + if (typeof request === "string") { + request = {title: request}; + if (typeof extra1 === "string") { + request.entryPromptDetails = extra1; + if (typeof extra2 === "string") { + request.entryStart = extra2; + } + } + } else if (!isTitleInObj(request)) { + throw new Error( + "Invalid argument: \"" + request + "\" -> AutoCards().API.generateCard() must be called with either 1, 2, or 3 strings OR a correctly formatted card generation object" + ); + } + O.f(request); + Internal.getUsedTitles(true); + return Internal.generateCard(request); + }, + /*** Regenerates a card by title or object reference, optionally preserving or modifying its input info + * + * @function + * @param {Object|string} request A card object reference or title string for the card to be regenerated + * @param {boolean} [useOldInfo=true] If true, preserves old info in the new generation; false omits it + * @param {string} [newInfo=""] Additional info to append to the generation prompt + * @returns {boolean} True if regeneration succeeded; false otherwise + * @throws {Error} If the request format is invalid, or if the second or third parameters are the wrong types + */ + redoCard: function(request, useOldInfo = true, newInfo = "") { + if (typeof request === "string") { + request = {title: request}; + } else if (!isTitleInObj(request)) { + throw new Error( + "Invalid argument: \"" + request + "\" -> AutoCards().API.redoCard() must be called with a string or correctly formatted card generation object" + ); + } + if (typeof useOldInfo !== "boolean") { + throw new Error( + "Invalid argument: \"" + request + ", " + useOldInfo + "\" -> AutoCards().API.redoCard() requires a boolean as its second argument" + ); + } else if (typeof newInfo !== "string") { + throw new Error( + "Invalid argument: \"" + request + ", " + useOldInfo + ", " + newInfo + "\" -> AutoCards().API.redoCard() requires a string for its third argument" + ); + } + return Internal.redoCard(request, useOldInfo, newInfo); + }, + /*** Flags or unflags a card as an auto-card, controlling its automatic generation behavior + * + * @function + * @param {Object|string} targetCard The card object or title to mark/unmark as an auto-card + * @param {boolean} [setOrUnset=true] If true, marks the card as an auto-card; false removes the flag + * @returns {boolean} True if the operation succeeded; false if the card was invalid or already matched the target state + * @throws {Error} If the arguments are invalid types + */ + setCardAsAuto: function(targetCard, setOrUnset = true) { + if (isTitleInObj(targetCard)) { + targetCard = targetCard.title; + } else if (typeof targetCard !== "string") { + throw new Error( + "Invalid argument: \"" + targetCard + "\" -> AutoCards().API.setCardAsAuto() must be called with a string or card object" + ); + } + if (typeof setOrUnset !== "boolean") { + throw new Error( + "Invalid argument: \"" + targetCard + ", " + setOrUnset + "\" -> AutoCards().API.setCardAsAuto() requires a boolean as its second argument" + ); + } + const [card, isAuto] = getIntendedCard(targetCard); + if (card === null) { + return false; + } + if (setOrUnset) { + if (checkAuto()) { + return false; + } + card.description = "{title:}"; + Internal.getUsedTitles(true); + return card.entry.startsWith("{title: "); + } else if (!checkAuto()) { + return false; + } + card.entry = removeAutoProps(card.entry); + card.description = removeAutoProps(card.description.replace(( + /\s*Auto(?:-|\s*)Cards\s*will\s*contextualize\s*these\s*memories\s*:\s*/gi + ), "")); + function checkAuto() { + return (isAuto || /{updates: (?:true|false), limit: \d+}/.test(card.description)); + } + return true; + }, + /*** Appends a memory to a story card's memory bank + * + * @function + * @param {Object|string} targetCard A card object reference or title string + * @param {string} newMemory The memory text to add + * @returns {boolean} True if the memory was added; false if it was empty, already present, or the card was not found + * @throws {Error} If the inputs are not a string or valid card object reference + */ + addCardMemory: function(targetCard, newMemory) { + if (isTitleInObj(targetCard)) { + targetCard = targetCard.title; + } else if (typeof targetCard !== "string") { + throw new Error( + "Invalid argument: \"" + targetCard + "\" -> AutoCards().API.addCardMemory() must be called with a string or card object" + ); + } + if (typeof newMemory !== "string") { + throw new Error( + "Invalid argument: \"" + targetCard + ", " + newMemory + "\" -> AutoCards().API.addCardMemory() requires a string for its second argument" + ); + } + newMemory = newMemory.trim().replace(/\s+/g, " ").replace(/^-+\s*/, ""); + if (newMemory === "") { + return false; + } + const [card, isAuto, titleKey] = getIntendedCard(targetCard); + if ( + (card === null) + || card.description.replace(/\s+/g, " ").toLowerCase().includes(newMemory.toLowerCase()) + ) { + return false; + } else if (card.description !== "") { + card.description += "\n"; + } + card.description += "- " + newMemory; + if (titleKey in AC.database.memories.associations) { + AC.database.memories.associations[titleKey][1] = (StringsHashed + .deserialize(AC.database.memories.associations[titleKey][1], 65536) + .remove(newMemory) + .add(newMemory) + .latest(3500) + .serialize() + ); + } else if (isAuto) { + AC.database.memories.associations[titleKey] = [999, (new StringsHashed(65536) + .add(newMemory) + .serialize() + )]; + } + return true; + }, + /*** Removes all previously generated auto-cards and resets various states + * + * @function + * @returns {number} The number of cards that were removed + */ + eraseAllAutoCards: function() { + return Internal.eraseAllAutoCards(); + }, + /*** Retrieves an array of titles currently used by the adventure's story cards + * + * @function + * @returns {Array} An array of strings representing used titles + */ + getUsedTitles: function() { + return Internal.getUsedTitles(true); + }, + /*** Retrieves an array of banned titles + * + * @function + * @returns {Array} An array of banned title strings + */ + getBannedTitles: function() { + return Internal.getBannedTitles(); + }, + /*** Sets the banned titles array, replacing any previously banned titles + * + * @function + * @param {string|Array} titles A comma-separated string or array of strings representing titles to ban + * @returns {Object} An object containing oldBans and newBans arrays + * @throws {Error} If the input is neither a string nor an array of strings + */ + setBannedTitles: function(titles) { + const codomain = {oldBans: AC.database.titles.banned}; + if (Array.isArray(titles) && titles.every(title => (typeof title === "string"))) { + assignBannedTitles(titles); + } else if (typeof titles === "string") { + if (titles.includes(",")) { + assignBannedTitles(titles.split(",")); + } else { + assignBannedTitles([titles]); + } + } else { + throw new Error( + "Invalid argument: \"" + titles + "\" -> AutoCards().API.setBannedTitles() must be called with either a string or an array of strings" + ); + } + codomain.newBans = AC.database.titles.banned; + function assignBannedTitles(titles) { + Internal.setBannedTitles(uniqueTitlesArray(titles), false); + AC.signal.overrideBans = 3; + return; + } + return codomain; + }, + /*** Creates a new story card with the specified parameters + * + * @function + * @param {string|Object} title Card title string or full card template object containing all fields + * @param {string} [entry] The entry text for the card + * @param {string} [type] The card type (e.g., "character", "location") + * @param {string} [keys] The keys (triggers) for the card + * @param {string} [description] The notes or memory bank of the card + * @param {number} [insertionIndex] Optional index to insert the card at a specific position within storyCards + * @returns {Object|null} The created card object reference, or null if creation failed + */ + buildCard: function(title, entry, type, keys, description, insertionIndex) { + if (isTitleInObj(title)) { + type = title.type ?? type; + keys = title.keys ?? keys; + entry = title.entry ?? entry; + description = title.description ?? description; + title = title.title; + } + title = cast(title); + const card = constructCard(O.f({ + type: cast(type, AC.config.defaultCardType), + title, + keys: cast(keys, buildKeys("", title)), + entry: cast(entry), + description: cast(description) + }), boundInteger(0, insertionIndex, storyCards.length, newCardIndex())); + if (notEmptyObj(card)) { + return card; + } + function cast(value, fallback = "") { + if (typeof value === "string") { + return value; + } else { + return fallback; + } + } + return null; + }, + /*** Finds and returns story cards satisfying a user-defined condition + * + * @function + * @param {Function} predicate A function which takes a card and returns true if it matches + * @param {boolean} [getAll=false] If true, returns all matching cards; otherwise returns the first match + * @returns {Object|Array|null} A single card object reference, an array of cards, or null if no match is found + * @throws {Error} If the predicate is not a function or getAll is not a boolean + */ + getCard: function(predicate, getAll = false) { + if (typeof predicate !== "function") { + throw new Error( + "Invalid argument: \"" + predicate + "\" -> AutoCards().API.getCard() must be called with a function" + ); + } else if (typeof getAll !== "boolean") { + throw new Error( + "Invalid argument: \"" + predicate + ", " + getAll + "\" -> AutoCards().API.getCard() requires a boolean as its second argument" + ); + } + return Internal.getCard(predicate, getAll); + }, + /*** Removes story cards based on a user-defined condition or by direct reference + * + * @function + * @param {Function|Object} predicate A predicate function or a card object reference + * @param {boolean} [eraseAll=false] If true, removes all matching cards; otherwise removes the first match + * @returns {boolean|number} True if a single card was removed, false if none matched, or the number of cards erased + * @throws {Error} If the inputs are not a valid predicate function, card object, or boolean + */ + eraseCard: function(predicate, eraseAll = false) { + if (isTitleInObj(predicate) && storyCards.includes(predicate)) { + return eraseCard(predicate); + } else if (typeof predicate !== "function") { + throw new Error( + "Invalid argument: \"" + predicate + "\" -> AutoCards().API.eraseCard() must be called with a function or card object" + ); + } else if (typeof eraseAll !== "boolean") { + throw new Error( + "Invalid argument: \"" + predicate + ", " + eraseAll + "\" -> AutoCards().API.eraseCard() requires a boolean as its second argument" + ); + } else if (eraseAll) { + // Erase all cards which satisfy the given condition + let cardsErased = 0; + for (const [index, card] of storyCards.entries()) { + if (predicate(card)) { + removeStoryCard(index); + cardsErased++; + } + } + return cardsErased; + } + // Erase the first card which satisfies the given condition + for (const [index, card] of storyCards.entries()) { + if (predicate(card)) { + removeStoryCard(index); + return true; + } + } + return false; + } + }).map(([key, fn]) => [key, function(...args) { + const result = fn.apply(this, args); + if (data) { + data.description = JSON.stringify(AC); + } + return result; + }])))}); + function isTitleInObj(obj) { + return ( + (typeof obj === "object") + && (obj !== null) + && ("title" in obj) + && (typeof obj.title === "string") + ); + } + } + } else if (AC.signal.emergencyHalt) { + switch(HOOK) { + case "context": { + // AutoCards was called within the context modifier + advanceChronometer(); + break; } + case "output": { + // AutoCards was called within the output modifier + concludeEmergency(); + const previousAction = readPastAction(0); + if (isDoSayStory(previousAction.type) && /escape\s*emergency\s*halt/i.test(previousAction.text)) { + AC.signal.emergencyHalt = false; + } + break; } + } + CODOMAIN.initialize(TEXT); + } else if ((AC.config.LSIv2 !== null) && AC.config.LSIv2) { + // Silly recursion shenanigans + state.LSIv2 = AC; + AC.config.LSIv2 = false; + const LSI_DOMAIN = AutoCards(HOOK, TEXT, STOP); + // Is this lazy loading mechanism overkill? Yes. But it's fun! + const factories = O.f({ + library: () => ({ + name: Words.reserved.library, + entry: prose( + "// Your adventure's Shared Library code goes here", + "// Example Library code:", + "state.promptDragon ??= false;", + "state.mind ??= 0;", + "state.willStop ??= false;", + "function formatMessage(message, space = \" \") {", + " let leadingNewlines = \"\";", + " let trailingNewlines = \"\\n\\n\";", + " if (text.startsWith(\"\\n> \")) {", + " // We don't want any leading/trailing newlines for Do/Say", + " trailingNewlines = \"\";", + " } else if (history && (0 < history.length)) {", + " // Decide leading newlines based on the previous action", + " const action = history[history.length - 1];", + " if ((action.type === \"continue\") || (action.type === \"story\")) {", + " if (!action.text.endsWith(\"\\n\")) {", + " leadingNewlines = \"\\n\\n\";", + " } else if (!action.text.endsWith(\"\\n\\n\")) {", + " leadingNewlines = \"\\n\";", + " }", + " }", + " }", + " return leadingNewlines + \"{>\" + space + (message", + " .replace(/(?:\\s*(?:{>|<})\\s*)+/g, \" \")", + " .trim()", + " ) + space + \"<}\" + trailingNewlines;", + "}"), + description: + "// You may also continue your Library code below", + singleton: false, + position: 2 + }), + input: () => ({ + name: Words.reserved.input, + entry: prose( + "// Your adventure's Input Modifier code goes here", + "// Example Input code:", + "const minds = [", + "\"kind and gentle\",", + "\"curious and eager\",", + "\"cruel and evil\"", + "];", + "// Type any of these triggers into a Do/Say/Story action", + "const commands = new Map([", + "[\"encounter dragon\", () => {", + " AutoCards().API.postponeEvents(1);", + " state.promptDragon = true;", + " text = formatMessage(\"You encounter a dragon!\");", + " log(\"A dragon appears!\");", + "}],", + "[\"summon leah\", () => {", + " alterMind();", + " const success = AutoCards().API.generateCard({", + " title: \"Leah\",", + " entryPromptDetails: (", + " \"Leah is an exceptionally \" +", + " minds[state.mind] +", + " \" woman\"", + " ),", + " entryStart: \"Leah is your magically summoned assistant.\"", + " });", + " if (success) {", + " text = formatMessage(\"You begin summoning Leah!\");", + " log(\"Attempting to summon Leah\");", + " } else {", + " text = formatMessage(\"You failed to summon Leah...\");", + " log(\"Leah could not be summoned\");", + " }", + "}],", + "[\"alter leah\", () => {", + " alterMind();", + " const success = AutoCards().API.redoCard(\"Leah\", true, (", + " \"You subjected Leah to mind-altering magic\\n\" +", + " \"Therefore she is now entirely \" +", + " minds[state.mind] +", + " \", utterly captivated by your will\"", + " ));", + " if (success) {", + " text = formatMessage(", + " \"You proceed to alter Leah's mind!\"", + " );", + " log(\"Attempting to alter Leah\");", + " } else {", + " text = formatMessage(\"You failed to alter Leah...\");", + " log(\"Leah could not be altered\");", + " }", + "}],", + "[\"show api\", () => {", + " state.showAPI = true;", + " text = formatMessage(\"Displaying the Auto-Cards API below\");", + "}],", + "[\"force stop\", () => {", + " state.willStop = true;", + "}]", + "]);", + "const lowerText = text.toLowerCase();", + "for (const [trigger, implement] of commands) {", + " if (lowerText.includes(trigger)) {", + " implement();", + " break;", + " }", + "}", + "function alterMind() {", + " state.mind = (state.mind + 1) % minds.length;", + " return;", + "}"), + description: + "// You may also continue your Input code below", + singleton: false, + position: 3 + }), + context: () => ({ + name: Words.reserved.context, + entry: prose( + "// Your adventure's Context Modifier code goes here", + "// Example Context code:", + "text = text.replace(/\\s*{>[\\s\\S]*?<}\\s*/gi, \"\\n\\n\");", + "if (state.willStop) {", + " state.willStop = false;", + " // Assign true to prevent the onOutput hook", + " // This can only be done onContext", + " stop = true;", + "} else if (state.promptDragon) {", + " state.promptDragon = false;", + " text = (", + " text.trimEnd() +", + " \"\\n\\nA cute little dragon softly lands upon your head. \"", + " );", + "}"), + description: + "// You may also continue your Context code below", + singleton: false, + position: 4 + }), + output: () => ({ + name: Words.reserved.output, + entry: prose( + "// Your adventure's Output Modifier code goes here", + "// Example Output code:", + "if (state.showAPI) {", + " state.showAPI = false;", + " const apiKeys = (Object.keys(AutoCards().API)", + " .map(key => (\"AutoCards().API.\" + key + \"()\"))", + " );", + " text = formatMessage(apiKeys.join(\"\\n\"), \"\\n\");", + " log(apiKeys);", + "}"), + description: + "// You may also continue your Output code below", + singleton: false, + position: 5 + }), + guide: () => ({ + name: Words.reserved.guide, + entry: prose( + "Any valid JavaScript code you write within the Shared Library or Input/Context/Output Modifier story cards will be executed from top to bottom; Live Script Interface v2 closely emulates AI Dungeon's native scripting environment, even if you aren't the owner of the original scenario. Furthermore, I've provided full access to the Auto-Cards scripting API. Please note that disabling LSIv2 via the \"Configure Auto-Cards\" story card will reset your LSIv2 adventure scripts!", + "", + "If you aren't familiar with scripting in AI Dungeon, please refer to the official guidebook page:", + "https://help.aidungeon.com/scripting", + "", + "I've included an example script with the four aforementioned code cards, to help showcase some of my fancy schmancy Auto-Cards API functions. Take a look, try some of my example commands, inspect the Console Log, and so on... It's a ton of fun! ā¤ļø", + "", + "If you ever run out of space in your Library, Input, Context, or Output code cards, simply duplicate whichever one(s) you need and then perform an in-game turn before writing any more code. (emphasis on \"before\") Doing so will signal LSIv2 to convert your duplicated code card(s) into additional auxiliary versions.", + "", + "Auxiliary code cards are numbered, and any code written within will be appended in sequential order. For example:", + "// Shared Library (entry)", + "// Shared Library (notes)", + "// Shared Library 2 (entry)", + "// Shared Library 2 (notes)", + "// Shared Library 3 (entry)", + "// Shared Library 3 (notes)", + "// Input Modifier (entry)", + "// Input Modifier (notes)", + "// Input Modifier 2 (entry)", + "// Input Modifier 2 (notes)", + "// And so on..."), + description: + "", + singleton: true, + position: 0 + }), + state: () => ({ + name: Words.reserved.state, + entry: + "Your adventure's full state object is displayed in the Notes section below.", + description: + "", + singleton: true, + position: 6 + }), + log: () => ({ + name: Words.reserved.log, + entry: + "Please refer to the Notes section below to view the full log history for LSIv2. Console log entries are ordered from most recent to oldest. LSIv2 error messages will be recorded here, alongside the outputs of log and console.log function calls within your adventure scripts.", + description: + "", + singleton: true, + position: 1 + }) + }); + const cache = {}; + const templates = new Proxy({}, { + get(_, key) { + return cache[key] ??= O.f(factories[key]()); + } + }); + if (AC.config.LSIv2 !== null) { + switch(HOOK) { + case "input": { + // AutoCards was called within the input modifier + const [libraryCards, inputCards, logCard] = collectCards( + templates.library, + templates.input, + templates.log + ); + const [error, newText] = isolateLSIv2(parseCode(libraryCards, inputCards), callbackLog(logCard), LSI_DOMAIN); + handleError(logCard, error); + if (hadError()) { + CODOMAIN.initialize(getStoryError()); + AC.signal.upstreamError = "\n"; + } else { + CODOMAIN.initialize(newText); + } + break; } + case "context": { + // AutoCards was called within the context modifier + const [libraryCards, contextCards, logCard] = collectCards( + templates.library, + templates.context, + templates.log, + templates.input + ); + if (hadError()) { + endContextLSI(LSI_DOMAIN); + break; + } + const [error, ...newCodomain] = (([error, newText, newStop]) => [error, newText, (newStop === true)])( + isolateLSIv2(parseCode(libraryCards, contextCards), callbackLog(logCard), LSI_DOMAIN[0], LSI_DOMAIN[1]) + ); + handleError(logCard, error); + endContextLSI(newCodomain); + function endContextLSI(newCodomain) { + CODOMAIN.initialize(newCodomain); + if (!newCodomain[1]) { + return; + } + const [guideCard, stateCard] = collectCards( + templates.guide, + templates.state, + templates.output + ); + AC.message.pending = []; + concludeLSI(guideCard, stateCard, logCard); + return; + } + break; } + case "output": { + // AutoCards was called within the output modifier + const [libraryCards, outputCards, guideCard, stateCard, logCard] = collectCards( + templates.library, + templates.output, + templates.guide, + templates.state, + templates.log + ); + if (hadError()) { + endOutputLSI(true, LSI_DOMAIN); + break; + } + const [error, newText] = isolateLSIv2(parseCode(libraryCards, outputCards), callbackLog(logCard), LSI_DOMAIN); + handleError(logCard, error); + endOutputLSI(hadError(), newText); + function endOutputLSI(displayError, newText) { + if (displayError) { + if (AC.signal.upstreamError === "\n") { + CODOMAIN.initialize("\n"); + } else { + CODOMAIN.initialize(getStoryError() + "\n"); + } + AC.message.pending = []; + } else { + CODOMAIN.initialize(newText); + } + concludeLSI(guideCard, stateCard, logCard); + return; + } + break; } + case "initialize": { + collectAll(); + logToCard(Internal.getCard(card => (card.title === templates.log.name)), "LSIv2 startup -> Success!"); + CODOMAIN.initialize(null); + break; } + } + AC.config.LSIv2 = true; + function parseCode(...args) { + return (args + .flatMap(cardset => [cardset.primary, ...cardset.auxiliaries]) + .flatMap(card => [card.entry, card.description]) + .join("\n") + ); + } + function callbackLog(logCard) { + return function(...args) { + logToCard(logCard, ...args); + return; + } + } + function handleError(logCard, error) { + if (!error) { + return; + } + O.f(error); + AC.signal.upstreamError = ( + "LSIv2 encountered an error during the on" + HOOK[0].toUpperCase() + HOOK.slice(1) + " hook" + ); + if (error.message) { + AC.signal.upstreamError += ":\n"; + if (error.stack) { + const stackMatch = error.stack.match(/AutoCards[\s\S]*?:\s*(\d+)\s*:\s*(\d+)/i); + if (stackMatch) { + AC.signal.upstreamError += ( + (error.name ?? "Error") + ": " + error.message + "\n" + + "(line #" + stackMatch[1] + " column #" + stackMatch[2] + ")" + ); + } else { + AC.signal.upstreamError += error.stack; + } + } else { + AC.signal.upstreamError += (error.name ?? "Error") + ": " + error.message; + } + AC.signal.upstreamError = cleanSpaces(AC.signal.upstreamError.trimEnd()); + } + logToCard(logCard, AC.signal.upstreamError); + if (getStateMessage() === AC.signal.upstreamError) { + state.message = AC.signal.upstreamError + " "; + } else { + state.message = AC.signal.upstreamError; + } + return; + } + function hadError() { + return (AC.signal.upstreamError !== ""); + } + function getStoryError() { + return getPrecedingNewlines() + ">>>\n" + AC.signal.upstreamError + "\n<<<\n"; + } + function concludeLSI(guideCard, stateCard, logCard) { + AC.signal.upstreamError = ""; + guideCard.description = templates.guide.description; + guideCard.entry = templates.guide.entry; + stateCard.entry = templates.state.entry; + logCard.entry = templates.log.entry; + postMessages(); + const simpleState = {...state}; + delete simpleState.LSIv2; + stateCard.description = limitString(stringifyObject(simpleState).trim(), 999999).trimEnd(); + return; + } + } else { + const cardsets = collectAll(); + for (const cardset of cardsets) { + if ("primary" in cardset) { + killCard(cardset.primary); + for (const card of cardset.auxiliaries) { + killCard(card); + } + } else { + killCard(cardset); + } + function killCard(card) { + unbanTitle(card.title); + eraseCard(card); + } + } + AC.signal.upstreamError = ""; + CODOMAIN.initialize(LSI_DOMAIN); + } + // This measure ensures the Auto-Cards external API is equally available from within the inner scope of LSIv2 + // As before, call with AutoCards().API.nameOfFunction(yourArguments); + deepMerge(AC, state.LSIv2); + delete state.LSIv2; + function deepMerge(target, source) { + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } else if ( + (typeof source[key] === "object") + && (source[key] !== null) + && !Array.isArray(source[key]) + && (typeof target[key] === "object") + && (target[key] !== null) + && (key !== "workpiece") + && (key !== "associations") + ) { + // Recursively merge static objects + deepMerge(target[key], source[key]); + } else { + // Directly replace values + target[key] = source[key]; + } + } + return; + } + function collectAll() { + return collectCards(...Object.keys(factories).map(key => templates[key])); + } + // collectCards constructs, validates, repairs, retrieves, and organizes all LSIv2 script cards associated with the given arguments by iterating over the storyCards array only once! Returned elements are easily handled via array destructuring assignment + function collectCards(...args) { + // args: [{name: string, entry: string, description: string, singleton: boolean, position: integer}] + const collections = O.f(args.map(({name, entry, description, singleton, position}) => { + const collection = { + template: O.f({ + type: AC.config.defaultCardType, + title: name, + keys: name, + entry, + description + }), + singleton, + position, + primary: null, + excess: [], + }; + if (!singleton) { + collection.auxiliaries = []; + collection.occupied = new Set([0, 1]); + } + return O.s(collection); + })); + for (const card of storyCards) { + O.s(card); + for (const collection of collections) { + if ( + !card.title.toLowerCase().includes(collection.template.title.toLowerCase()) + && !card.keys.toLowerCase().includes(collection.template.title.toLowerCase()) + ) { + // No match, swipe left + continue; + } + if (collection.singleton) { + setPrimary(); + break; + } + const [extensionA, extensionB] = [card.title, card.keys].map(name => { + const extensionMatch = name.replace(/[^a-zA-Z0-9]/g, "").match(/\d+$/); + if (extensionMatch) { + return parseInt(extensionMatch[0], 10); + } else { + return -1; + } + }); + if (-1 < extensionA) { + if (-1 < extensionB) { + if (collection.occupied.has(extensionA)) { + setAuxiliary(extensionB); + } else { + setAuxiliary(extensionA, true); + } + } else { + setAuxiliary(extensionA); + } + } else if (-1 < extensionB) { + setAuxiliary(extensionB); + } else { + setPrimary(); + } + function setAuxiliary(extension, preChecked = false) { + if (preChecked || !collection.occupied.has(extension)) { + addAuxiliary(card, collection, extension); + } else { + card.title = card.keys = collection.template.title; + collection.excess.push(card); + } + return; + } + function setPrimary() { + card.title = card.keys = collection.template.title; + if (collection.primary === null) { + collection.primary = card; + } else { + collection.excess.push(card); + } + return; + } + break; + } + } + for (const collection of collections) { + banTitle(collection.template.title); + if (collection.singleton) { + if (collection.primary === null) { + constructPrimary(); + } else if (hasExs()) { + for (const card of collection.excess) { + eraseCard(card); + } + } + continue; + } else if (collection.primary === null) { + if (hasExs()) { + collection.primary = collection.excess.shift(); + if (hasExs() || hasAux()) { + applyComment(collection.primary); + } else { + collection.primary.entry = collection.template.entry; + collection.primary.description = collection.template.description; + continue; + } + } else { + constructPrimary(); + if (hasAux()) { + applyComment(collection.primary); + } else { + continue; + } + } + } + if (hasExs()) { + for (const card of collection.excess) { + let extension = 2; + while (collection.occupied.has(extension)) { + extension++; + } + applyComment(card); + addAuxiliary(card, collection, extension); + } + } + if (hasAux()) { + collection.auxiliaries.sort((a, b) => { + return a.extension - b.extension; + }); + } + function hasExs() { + return (0 < collection.excess.length); + } + function hasAux() { + return (0 < collection.auxiliaries.length); + } + function applyComment(card) { + card.entry = card.description = "// You may continue writing your code here"; + return; + } + function constructPrimary() { + collection.primary = constructCard(collection.template, newCardIndex()); + // I like my LSIv2 cards to display in the proper order once initialized uwu + const templateKeys = Object.keys(factories); + const cards = templateKeys.map(key => O.f({ + card: Internal.getCard(card => (card.title === templates[key].name)), + position: templates[key].position + })).filter(pair => (pair.card !== null)); + if (cards.length < templateKeys.length) { + return; + } + const fullCardset = cards.sort((a, b) => (a.position - b.position)).map(pair => pair.card); + for (const card of fullCardset) { + eraseCard(card); + card.title = card.keys; + } + storyCards.splice(newCardIndex(), 0, ...fullCardset); + return; + } + } + function addAuxiliary(card, collection, extension) { + collection.occupied.add(extension); + card.title = card.keys = collection.template.title + " " + extension; + collection.auxiliaries.push({card, extension}); + return; + } + return O.f(collections.map(({singleton, primary, auxiliaries}) => { + if (singleton) { + return primary; + } else { + return O.f({primary, auxiliaries: O.f(auxiliaries.map(({card}) => card))}); + } + })); + } + } else if (AC.config.doAC) { + // Auto-Cards is currently enabled + // "text" represents the original text which was present before any scripts were executed + // "TEXT" represents the script-modified version of "text" which AutoCards was called with + // This dual scheme exists to ensure Auto-Cards is safely compatible with other scripts + switch(HOOK) { + case "input": { + // AutoCards was called within the input modifier + if ((AC.config.deleteAllAutoCards === false) && /CONFIRM\s*DELETE/i.test(TEXT)) { + CODOMAIN.initialize("CONFIRM DELETE -> Success!"); + } else if (/\/\s*A\s*C/i.test(text)) { + CODOMAIN.initialize(doPlayerCommands(text)); + } else if (TEXT.startsWith(" ") && readPastAction(0).text.endsWith("\n")) { + // Just a simple little formatting bugfix for regular AID story actions + CODOMAIN.initialize(getPrecedingNewlines() + TEXT.replace(/^\s+/, "")); + } else { + CODOMAIN.initialize(TEXT); + } + break; } + case "context": { + // AutoCards was called within the context modifier + advanceChronometer(); + // Get or construct the "Configure Auto-Cards" story card + const configureCardTemplate = getConfigureCardTemplate(); + const configureCard = getSingletonCard(true, configureCardTemplate); + banTitle(configureCardTemplate.title); + pinAndSortCards(configureCard); + const bansOverwritten = (0 < AC.signal.overrideBans); + if ((configureCard.description !== configureCardTemplate.description) || bansOverwritten) { + const descConfigPatterns = (getConfigureCardDescription() + .split(Words.delimiter) + .slice(1) + .map(descPattern => (descPattern + .slice(0, descPattern.indexOf(":")) + .trim() + .replace(/\s+/g, "\\s*") + )) + .map(descPattern => (new RegExp("^\\s*" + descPattern + "\\s*:", "i"))) + ); + const descConfigs = configureCard.description.split(Words.delimiter).slice(1); + if ( + (descConfigs.length === descConfigPatterns.length) + && descConfigs.every((descConfig, index) => descConfigPatterns[index].test(descConfig)) + ) { + // All description config headers must be present and well-formed + let cfg = extractDescSetting(0); + if (AC.config.generationPrompt !== cfg) { + notify("Changes to your card generation prompt were successfully saved"); + AC.config.generationPrompt = cfg; + } + cfg = extractDescSetting(1); + if (AC.config.compressionPrompt !== cfg) { + notify("Changes to your card memory compression prompt were successfully saved"); + AC.config.compressionPrompt = cfg; + } + if (bansOverwritten) { + overrideBans(); + } else if ((0 < AC.database.titles.pendingBans.length) || (0 < AC.database.titles.pendingUnbans.length)) { + const pendingBans = AC.database.titles.pendingBans.map(pair => pair[0]); + const pendingRewrites = new Set( + lowArr([...pendingBans, ...AC.database.titles.pendingUnbans.map(pair => pair[0])]) + ); + Internal.setBannedTitles([...pendingBans, ...extractDescSetting(2) + .split(",") + .filter(newBan => !pendingRewrites.has(newBan.toLowerCase().replace(/\s+/, " ").trim())) + ], true); + } else { + Internal.setBannedTitles(extractDescSetting(2).split(","), true); + } + function extractDescSetting(index) { + return descConfigs[index].replace(descConfigPatterns[index], "").trim(); + } + } else if (bansOverwritten) { + overrideBans(); + } + configureCard.description = getConfigureCardDescription(); + function overrideBans() { + Internal.setBannedTitles(AC.database.titles.pendingBans.map(pair => pair[0]), true); + AC.signal.overrideBans = 0; + return; + } + } + if (configureCard.entry !== configureCardTemplate.entry) { + const oldConfig = {}; + const settings = O.f((function() { + const userSettings = extractSettings(configureCard.entry); + if (userSettings.resetallconfigsettingsandprompts !== true) { + return userSettings; + } + // Reset all config settings and display state change notifications only when appropriate + Object.assign(oldConfig, AC.config); + Object.assign(AC.config, getDefaultConfig()); + AC.config.deleteAllAutoCards = oldConfig.deleteAllAutoCards; + AC.config.LSIv2 = oldConfig.LSIv2; + AC.config.defaultCardType = oldConfig.defaultCardType; + AC.database.titles.banned = getDefaultConfigBans(); + configureCard.description = getConfigureCardDescription(); + configureCard.entry = getConfigureCardEntry(); + const defaultSettings = extractSettings(configureCard.entry); + if ((DEFAULT_DO_AC === false) || (userSettings.disableautocards === true)) { + defaultSettings.disableautocards = true; + } + notify("Restoring all settings and prompts to their default values"); + return defaultSettings; + })()); + O.f(oldConfig); + if ((settings.deleteallautomaticstorycards === true) && (AC.config.deleteAllAutoCards === null)) { + AC.config.deleteAllAutoCards = true; + } else if (settings.showdetailedguide === true) { + AC.signal.outputReplacement = Words.guide; + } + let cfg; + if (parseConfig("pinthisconfigcardnearthetop", false, "pinConfigureCard")) { + if (cfg) { + pinAndSortCards(configureCard); + notify("The settings config card will now be pinned near the top of your story cards list"); + } else { + const index = storyCards.indexOf(configureCard); + if (index !== -1) { + storyCards.splice(index, 1); + storyCards.push(configureCard); + } + notify("The settings config card will no longer be pinned near the top of your story cards list"); + } + } + if (parseConfig("minimumturnscooldownfornewcards", true, "addCardCooldown")) { + const oldCooldown = AC.config.addCardCooldown; + AC.config.addCardCooldown = validateCooldown(cfg); + if (!isPendingGeneration() && !isAwaitingGeneration() && (0 < AC.generation.cooldown)) { + const quarterCooldown = validateCooldown(underQuarterInteger(AC.config.addCardCooldown)); + if ((AC.config.addCardCooldown < oldCooldown) && (quarterCooldown < AC.generation.cooldown)) { + // Reduce the next generation's cooldown counter by a factor of 4 + // But only if the new cooldown config is lower than it was before + // And also only if quarter cooldown is less than the current next gen cooldown + // (Just a random little user experience improvement) + AC.generation.cooldown = quarterCooldown; + } else if (oldCooldown < AC.config.addCardCooldown) { + if (oldCooldown === AC.generation.cooldown) { + AC.generation.cooldown = AC.config.addCardCooldown; + } else { + AC.generation.cooldown = validateCooldown(boundInteger( + 0, + AC.generation.cooldown + quarterCooldown, + AC.config.addCardCooldown + )); + } + } + } + switch(AC.config.addCardCooldown) { + case 9999: { + notify( + "You have disabled automatic card generation. To re-enable, simply set your cooldown config to any number lower than 9999. Or use the \"/ac\" in-game command to manually direct the card generation process" + ); + break; } + case 1: { + notify( + "A new card will be generated during alternating game turns, but only if your story contains available titles" + ); + break; } + case 0: { + notify( + "New cards will be immediately generated whenever valid titles exist within your recent story" + ); + break; } + default: { + notify( + "A new card will be generated once every " + AC.config.addCardCooldown + " turns, but only if your story contains available titles" + ); + break; } + } + } + if (parseConfig("newcardsuseabulletedlistformat", false, "bulletedListMode")) { + if (cfg) { + notify("New card entries will be generated using a bulleted list format"); + } else { + notify("New card entries will be generated using a pure prose format"); + } + } + if (parseConfig("maximumentrylengthfornewcards", true, "defaultEntryLimit")) { + AC.config.defaultEntryLimit = validateEntryLimit(cfg); + notify( + "New card entries will be limited to " + AC.config.defaultEntryLimit + " characters of generated text" + ); + } + if (parseConfig("newcardsperformmemoryupdates", false, "defaultCardsDoMemoryUpdates")) { + if (cfg) { + notify("Newly constructed cards will begin with memory updates enabled by default"); + } else { + notify("Newly constructed cards will begin with memory updates disabled by default"); + } + } + if (parseConfig("cardmemorybankpreferredlength", true, "defaultMemoryLimit")) { + AC.config.defaultMemoryLimit = validateMemoryLimit(cfg); + notify( + "Newly constructed cards will begin with their memory bank length preference set to " + AC.config.defaultMemoryLimit + " characters of text" + ); + } + if (parseConfig("memorysummarycompressionratio", true, "memoryCompressionRatio")) { + AC.config.memoryCompressionRatio = validateMemCompRatio(cfg); + notify( + "Freshly summarized card memory banks will be approximately " + (AC.config.memoryCompressionRatio / 10) + "x shorter than their originals" + ); + } + if (parseConfig("excludeallcapsfromtitledetection", false, "ignoreAllCapsTitles")) { + if (cfg) { + notify("All-caps text will be ignored during title detection to help prevent bad cards"); + } else { + notify("All-caps text may be considered during title detection processes"); + } + } + if (parseConfig("alsodetecttitlesfromplayerinputs", false, "readFromInputs")) { + if (cfg) { + notify("Titles may be detected from player Do/Say/Story action inputs"); + } else { + notify("Title detection will skip player Do/Say/Story action inputs for grammatical leniency"); + } + } + if (parseConfig("minimumturnsagefortitledetection", true, "minimumLookBackDistance")) { + AC.config.minimumLookBackDistance = validateMinLookBackDist(cfg); + notify( + "Titles and names mentioned in your story may become eligible for future card generation attempts once they are at least " + AC.config.minimumLookBackDistance + " actions old" + ); + } + cfg = settings.uselivescriptinterfacev2; + if (typeof cfg === "boolean") { + if (AC.config.LSIv2 === null) { + if (cfg) { + AC.config.LSIv2 = true; + state.LSIv2 = AC; + AutoCards("initialize"); + notify("Live Script Interface v2 is now embedded within your adventure!"); + } + } else { + if (!cfg) { + AC.config.LSIv2 = null; + notify("Live Script Interface v2 has been removed from your adventure"); + } + } + } + if (parseConfig("logdebugdatainaseparatecard" , false, "showDebugData")) { + if (data === null) { + if (cfg) { + notify("State may now be viewed within the \"Debug Data\" story card"); + } else { + notify("The \"Debug Data\" story card has been removed"); + } + } else if (cfg) { + notify("Debug data will be shared with the \"Critical Data\" story card to conserve memory"); + } else { + notify("Debug mode has been disabled"); + } + } + if ((settings.disableautocards === true) && (AC.signal.forceToggle !== true)) { + disableAutoCards(); + break; + } else { + // Apply the new card entry and proceed to implement Auto-Cards onContext + configureCard.entry = getConfigureCardEntry(); + } + function parseConfig(settingsKey, isNumber, configKey) { + cfg = settings[settingsKey]; + if (isNumber) { + return checkConfig("number"); + } else if (!checkConfig("boolean")) { + return false; + } + AC.config[configKey] = cfg; + function checkConfig(type) { + return ((typeof cfg === type) && ( + (notEmptyObj(oldConfig) && (oldConfig[configKey] !== cfg)) + || (AC.config[configKey] !== cfg) + )); + } + return true; + } + } + if (AC.signal.forceToggle === false) { + disableAutoCards(); + break; + } + AC.signal.forceToggle = null; + if (0 < AC.chronometer.postpone) { + CODOMAIN.initialize(TEXT); + break; + } + // Fully implement Auto-Cards onContext + const forceStep = AC.signal.recheckRetryOrErase; + const currentTurn = getTurn(); + const nearestUnparsedAction = boundInteger(0, currentTurn - AC.config.minimumLookBackDistance); + if (AC.signal.recheckRetryOrErase || (nearestUnparsedAction <= AC.database.titles.lastActionParsed)) { + // The player erased or retried an unknown number of actions + // Purge recent candidates and perform a safety recheck + if (nearestUnparsedAction <= AC.database.titles.lastActionParsed) { + AC.signal.recheckRetryOrErase = true; + } else { + AC.signal.recheckRetryOrErase = false; + } + AC.database.titles.lastActionParsed = boundInteger(-1, nearestUnparsedAction - 8); + for (let i = AC.database.titles.candidates.length - 1; 0 <= i; i--) { + const candidate = AC.database.titles.candidates[i]; + for (let j = candidate.length - 1; 0 < j; j--) { + if (AC.database.titles.lastActionParsed < candidate[j]) { + candidate.splice(j, 1); + } + } + if (candidate.length <= 1) { + AC.database.titles.candidates.splice(i, 1); + } + } + } + const pendingCandidates = new Map(); + if ((0 < nearestUnparsedAction) && (AC.database.titles.lastActionParsed < nearestUnparsedAction)) { + const actions = []; + for ( + let actionToParse = AC.database.titles.lastActionParsed + 1; + actionToParse <= nearestUnparsedAction; + actionToParse++ + ) { + // I wrote this whilst sleep-deprived, somehow it works + const lookBack = currentTurn - actionToParse - (function() { + if (isDoSayStory(readPastAction(0).type)) { + // Inputs count as 2 actions instead of 1, conditionally offset lookBack by 1 + return 0; + } else { + return 1; + } + })(); + if (history.length <= lookBack) { + // history cannot be indexed with a negative integer + continue; + } + const action = readPastAction(lookBack); + const thisTextHash = new StringsHashed(4096).add(action.text).serialize(); + if (actionToParse === nearestUnparsedAction) { + if (AC.signal.recheckRetryOrErase || (thisTextHash === AC.database.titles.lastTextHash)) { + // Additional safety to minimize duplicate candidate additions during retries or erases + AC.signal.recheckRetryOrErase = true; + break; + } else { + // Action parsing will proceed + AC.database.titles.lastActionParsed = nearestUnparsedAction; + AC.database.titles.lastTextHash = thisTextHash; + } + } else if ( + // Special case where a consecutive retry>erase>continue cancels out + AC.signal.recheckRetryOrErase + && (actionToParse === (nearestUnparsedAction - 1)) + && (thisTextHash === AC.database.titles.lastTextHash) + ) { + AC.signal.recheckRetryOrErase = false; + } + actions.push([action, actionToParse]); + } + if (!AC.signal.recheckRetryOrErase) { + for (const [action, turn] of actions) { + if ( + (action.type === "see") + || (action.type === "unknown") + || (!AC.config.readFromInputs && isDoSayStory(action.type)) + || /^[^\p{Lu}]*$/u.test(action.text) + || action.text.includes("<<<") + || /\/\s*A\s*C/i.test(action.text) + || /CONFIRM\s*DELETE/i.test(action.text) + ) { + // Skip see actions + // Skip input actions (only if input title detection has been disabled in the config) + // Skip strings without capital letters + // Skip utility actions + continue; + } + const words = (prettifyEmDashes(action.text) + // Nuh uh + .replace(/[ā€œā€]/g, "\"").replace(/[ā€˜ā€™]/g, "'").replaceAll("Ā“", "`") + .replaceAll("怂", ".").replaceAll("?", "?").replaceAll("!", "!") + // Replace special clause opening punctuation with colon ":" terminators + .replace(/(^|\s+)["'`]\s*/g, ": ").replace(/\s*[\(\[{]\s*/g, ": ") + // Likewise for end-quotes (curbs a common AI grammar mistake) + .replace(/\s*,?\s*["'`](?:\s+|$)/g, ": ") + // Replace funky wunky symbols with regular spaces + .replace(/[ŲŸŲŒĀ«Ā»ĀæĀ”ā€žā€œā€¦Ā§ļ¼Œć€\*_~><\)\]}#"`\s]/g, " ") + // Replace some mid-sentence punctuation symbols with a placeholder word + .replace(/\s*[—;,\/\\]\s*/g, " %@% ") + // Replace "I", "I'm", "I'd", "I'll", and "I've" with a placeholder word + .replace(/(?:^|\s+|-)I(?:'(?:m|d|ll|ve))?(?:\s+|-|$)/gi, " %@% ") + // Remove "'s" only if not followed by a letter + .replace(/'s(?![a-zA-Z])/g, "") + // Replace "s'" with "s" only if preceded but not followed by a letter + .replace(/(?<=[a-zA-Z])s'(?![a-zA-Z])/g, "s") + // Remove apostrophes not between letters (preserve contractions like "don't") + .replace(/(? ": " + next.toUpperCase()) + // Condense consecutive whitespace + .trim().replace(/\s+/g, " ") + ).split(" "); + if (!Array.isArray(words) || (words.length < 2)) { + continue; + } + const titles = []; + const incompleteTitle = []; + let previousWordTerminates = true; + for (let i = 0; i < words.length; i++) { + let word = words[i]; + if (startsWithTerminator()) { + // This word begins on a terminator, push the preexisting incomplete title to titles and proceed with the next sentence's beginning + pushTitle(); + previousWordTerminates = true; + // Ensure no leading terminators remain + while ((word !== "") && startsWithTerminator()) { + word = word.slice(1); + } + } + if (word === "") { + continue; + } else if (previousWordTerminates) { + // We cannot detect titles from sentence beginnings due to sentence capitalization rules. The previous sentence was recently terminated, implying the current series of capitalized words (plus lowercase minor words) occurs near the beginning of the current sentence + if (endsWithTerminator()) { + continue; + } else if (startsWithUpperCase()) { + if (isMinorWord(word)) { + // Special case where a capitalized minor word precedes a named entity, clear the previous termination status + previousWordTerminates = false; + } + // Otherwise, proceed without clearing + } else if (!isMinorWord(word) && !/^(?:and|&)(?:$|[\.\?!:]$)/.test(word)) { + // Previous sentence termination status is cleared by the first new non-minor lowercase word encountered during forward iteration through the action text's words + previousWordTerminates = false; + } + continue; + } + // Words near the beginning of this sentence have been skipped, proceed with named entity detection using capitalization rules. An incomplete title will be pushed to titles if A) a non-minor lowercase word is encountered, B) three consecutive minor words occur in a row, C) a terminator symbol is encountered at the end of a word. Otherwise, continue pushing words to the incomplete title + if (endsWithTerminator()) { + previousWordTerminates = true; + while ((word !== "") && endsWithTerminator()) { + word = word.slice(0, -1); + } + if (word === "") { + pushTitle(); + continue; + } + } + if (isMinorWord(word)) { + if (0 < incompleteTitle.length) { + // Titles cannot start with a minor word + if ( + (2 < incompleteTitle.length) && !(isMinorWord(incompleteTitle[incompleteTitle.length - 1]) && isMinorWord(incompleteTitle[incompleteTitle.length - 2])) + ) { + // Titles cannot have 3 or more consecutive minor words in a row + pushTitle(); + continue; + } else { + // Titles may contain minor words in their middles. Ex: "Ace of Spades" + incompleteTitle.push(word.toLowerCase()); + } + } + } else if (startsWithUpperCase()) { + // Add this proper noun to the incomplete title + incompleteTitle.push(word); + } else { + // The full title has a non-minor lowercase word to its immediate right + pushTitle(); + continue; + } + if (previousWordTerminates) { + pushTitle(); + } + function pushTitle() { + while ( + (1 < incompleteTitle.length) + && isMinorWord(incompleteTitle[incompleteTitle.length - 1]) + ) { + incompleteTitle.pop(); + } + if (0 < incompleteTitle.length) { + titles.push(incompleteTitle.join(" ")); + // Empty the array + incompleteTitle.length = 0; + } + return; + } + function isMinorWord(testWord) { + return Words.minor.includes(testWord.toLowerCase()); + } + function startsWithUpperCase() { + return /^\p{Lu}/u.test(word); + } + function startsWithTerminator() { + return /^[\.\?!:]/.test(word); + } + function endsWithTerminator() { + return /[\.\?!:]$/.test(word); + } + } + for (let i = titles.length - 1; 0 <= i; i--) { + titles[i] = formatTitle(titles[i]).newTitle; + if (titles[i] === "" || ( + AC.config.ignoreAllCapsTitles + && (2 < titles[i].replace(/[^a-zA-Z]/g, "").length) + && (titles[i] === titles[i].toUpperCase()) + )) { + titles.splice(i, 1); + } + } + // Remove duplicates + const uniqueTitles = [...new Set(titles)]; + if (uniqueTitles.length === 0) { + continue; + } else if ( + // No reason to keep checking long past the max lookback distance + (currentTurn < 256) + && (action.type === "start") + // This is only used here so it doesn't need its own AC.config property or validation + && (DEFAULT_BAN_TITLES_FROM_OPENING !== false) + ) { + // Titles in the opening prompt are banned by default, hopefully accounting for the player character's name and other established setting details + uniqueTitles.forEach(title => banTitle(title)); + } else { + // Schedule new titles for later insertion within the candidates database + for (const title of uniqueTitles) { + const pendingHashKey = title.toLowerCase(); + if (pendingCandidates.has(pendingHashKey)) { + // Consolidate pending candidates with matching titles but different turns + pendingCandidates.get(pendingHashKey).turns.push(turn); + } else { + pendingCandidates.set(pendingHashKey, O.s({title, turns: [turn]})); + } + } + } + function buildKiller(words) { + return (new RegExp(("(?:^|\\s+|-)(?:" + (words + .map(word => word.replace(".", "\\.")) + .join("|") + ) + ")(?:\\s+|-|$)"), "gi")); + } + } + } + } + // Measure the minimum and maximum turns of occurance for all title candidates + let minTurn = currentTurn; + let maxTurn = 0; + for (let i = AC.database.titles.candidates.length - 1; 0 <= i; i--) { + const candidate = AC.database.titles.candidates[i]; + const title = candidate[0]; + if (isUsedOrBanned(title) || isNamed(title)) { + // Retroactively ensure AC.database.titles.candidates contains no used / banned titles + AC.database.titles.candidates.splice(i, 1); + } else { + const pendingHashKey = title.toLowerCase(); + if (pendingCandidates.has(pendingHashKey)) { + // This candidate title matches one of the pending candidates, collect the pending turns + candidate.push(...pendingCandidates.get(pendingHashKey).turns); + // Remove this pending candidate + pendingCandidates.delete(pendingHashKey); + } + if (2 < candidate.length) { + // Ensure all recorded turns of occurance are unique for this candidate + // Sort the turns from least to greatest + const sortedTurns = [...new Set(candidate.slice(1))].sort((a, b) => (a - b)); + if (625 < sortedTurns.length) { + sortedTurns.splice(0, sortedTurns.length - 600); + } + candidate.length = 1; + candidate.push(...sortedTurns); + } + setCandidateTurnBounds(candidate); + } + } + for (const pendingCandidate of pendingCandidates.values()) { + // Insert any remaining pending candidates (validity has already been ensured) + const newCandidate = [pendingCandidate.title, ...pendingCandidate.turns]; + setCandidateTurnBounds(newCandidate); + AC.database.titles.candidates.push(newCandidate); + } + const isCandidatesSorted = (function() { + if (425 < AC.database.titles.candidates.length) { + // Sorting a large title candidates database is computationally expensive + sortCandidates(); + AC.database.titles.candidates.splice(400); + // Flag this operation as complete for later consideration + return true; + } else { + return false; + } + })(); + Internal.getUsedTitles(); + for (const titleKey in AC.database.memories.associations) { + if (isAuto(titleKey)) { + // Reset the lifespan counter + AC.database.memories.associations[titleKey][0] = 999; + } else if (AC.database.memories.associations[titleKey][0] < 1) { + // Forget this set of memory associations + delete AC.database.memories.associations[titleKey]; + } else if (!isAwaitingGeneration()) { + // Decrement the lifespan counter + AC.database.memories.associations[titleKey][0]--; + } + } + // This copy of TEXT may be mutated + let context = TEXT; + const titleHeaderPatternGlobal = /\s*{\s*titles?\s*:\s*([\s\S]*?)\s*}\s*/gi; + // Card events govern the parsing of memories from raw context as well as card memory bank injection + const cardEvents = (function() { + // Extract memories from the initial text (not TEXT as called from within the context modifier!) + const contextMemories = (function() { + const memoriesMatch = text.match(/Memories\s*:\s*([\s\S]*?)\s*(?:Recent\s*Story\s*:|$)/i); + if (!memoriesMatch) { + return new Set(); + } + const uniqueMemories = new Set(isolateMemories(memoriesMatch[1])); + if (uniqueMemories.size === 0) { + return uniqueMemories; + } + const duplicatesHashed = StringsHashed.deserialize(AC.database.memories.duplicates, 65536); + const duplicateMemories = new Set(); + const seenMemories = new Set(); + for (const memoryA of uniqueMemories) { + if (duplicatesHashed.has(memoryA)) { + // Remove to ensure the insertion order for this duplicate changes + duplicatesHashed.remove(memoryA); + duplicateMemories.add(memoryA); + } else if ((function() { + for (const memoryB of seenMemories) { + if (0.42 < similarityScore(memoryA, memoryB)) { + // This memory is too similar to another memory + duplicateMemories.add(memoryA); + return false; + } + } + return true; + })()) { + seenMemories.add(memoryA); + } + } + if (0 < duplicateMemories.size) { + // Add each near duplicate's hashcode to AC.database.memories.duplicates + // Then remove duplicates from uniqueMemories and the context window + for (const duplicate of duplicateMemories) { + duplicatesHashed.add(duplicate); + uniqueMemories.delete(duplicate); + context = context.replaceAll("\n" + duplicate, ""); + } + // Only the 2000 most recent duplicate memory hashcodes are remembered + AC.database.memories.duplicates = duplicatesHashed.latest(2000).serialize(); + } + return uniqueMemories; + })(); + const leftBoundary = "^|\\s|\"|'|—|\\(|\\[|{"; + const rightBoundary = "\\s|\\.|\\?|!|,|;|\"|'|—|\\)|\\]|}|$"; + // Murder, homicide if you will, nothing to see here + const theKiller = new RegExp("(?:" + leftBoundary + ")the[\\s\\S]*$", "i"); + const peerageKiller = new RegExp(( + "(?:" + leftBoundary + ")(?:" + Words.peerage.join("|") + ")(?:" + rightBoundary + ")" + ), "gi"); + const events = new Map(); + for (const contextMemory of contextMemories) { + for (const titleKey of auto) { + if (!(new RegExp(( + "(?<=" + leftBoundary + ")" + (titleKey + .replace(theKiller, "") + .replace(peerageKiller, "") + .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ) + "(?=" + rightBoundary + ")" + ), "i")).test(contextMemory)) { + continue; + } + // AC card titles found in active memories will promote card events + if (events.has(titleKey)) { + events.get(titleKey).pendingMemories.push(contextMemory); + continue; + } + events.set(titleKey, O.s({ + pendingMemories: [contextMemory], + titleHeader: "" + })); + } + } + const titleHeaderMatches = [...context.matchAll(titleHeaderPatternGlobal)]; + for (const [titleHeader, title] of titleHeaderMatches) { + if (!isAuto(title)) { + continue; + } + // Unique title headers found in context will promote card events + const titleKey = title.toLowerCase(); + if (events.has(titleKey)) { + events.get(titleKey).titleHeader = titleHeader; + continue; + } + events.set(titleKey, O.s({ + pendingMemories: [], + titleHeader: titleHeader + })); + } + return events; + })(); + // Remove auto card title headers from active story card entries and contextualize their respective memory banks + // Also handle the growth and maintenance of card memory banks + let isRemembering = false; + for (const card of storyCards) { + // Iterate over each card to handle pending card events and forenames/surnames + const titleHeaderMatcher = /^{title: \s*([\s\S]*?)\s*}/; + let breakForCompression = isPendingCompression(); + if (breakForCompression) { + break; + } else if (!card.entry.startsWith("{title: ")) { + continue; + } else if (exceedsMemoryLimit()) { + const titleHeaderMatch = card.entry.match(titleHeaderMatcher); + if (titleHeaderMatch && isAuto(titleHeaderMatch[1])) { + prepareMemoryCompression(titleHeaderMatch[1].toLowerCase()); + break; + } + } + // Handle card events + const lowerEntry = card.entry.toLowerCase(); + for (const titleKey of cardEvents.keys()) { + if (!lowerEntry.startsWith("{title: " + titleKey + "}")) { + continue; + } + const cardEvent = cardEvents.get(titleKey); + if ( + (0 < cardEvent.pendingMemories.length) + && /{\s*updates?\s*:\s*true\s*,\s*limits?\s*:[\s\S]*?}/i.test(card.description) + ) { + // Add new card memories + const associationsHashed = (function() { + if (titleKey in AC.database.memories.associations) { + return StringsHashed.deserialize(AC.database.memories.associations[titleKey][1], 65536); + } else { + AC.database.memories.associations[titleKey] = [999, ""]; + return new StringsHashed(65536); + } + })(); + const oldMemories = isolateMemories(extractCardMemories().text); + for (let i = 0; i < cardEvent.pendingMemories.length; i++) { + if (associationsHashed.has(cardEvent.pendingMemories[i])) { + // Remove first to alter the insertion order + associationsHashed.remove(cardEvent.pendingMemories[i]); + } else if (!oldMemories.some(oldMemory => ( + (0.8 < similarityScore(oldMemory, cardEvent.pendingMemories[i])) + ))) { + // Ensure no near-duplicate memories are appended + card.description += "\n- " + cardEvent.pendingMemories[i]; + } + associationsHashed.add(cardEvent.pendingMemories[i]); + } + AC.database.memories.associations[titleKey][1] = associationsHashed.latest(3500).serialize(); + if (associationsHashed.size() === 0) { + delete AC.database.memories.associations[titleKey]; + } + if (exceedsMemoryLimit()) { + breakForCompression = prepareMemoryCompression(titleKey); + break; + } + } + if (cardEvent.titleHeader !== "") { + // Replace this card's title header in context + const cardMemoriesText = extractCardMemories().text; + if (cardMemoriesText === "") { + // This card contains no card memories to contextualize + context = context.replace(cardEvent.titleHeader, "\n\n"); + } else { + // Insert card memories within context and ensure they occur uniquely + const cardMemories = cardMemoriesText.split("\n").map(cardMemory => cardMemory.trim()); + for (const cardMemory of cardMemories) { + if (25 < cardMemory.length) { + context = (context + .replaceAll(cardMemory, "<#>") + .replaceAll(cardMemory.replace(/^-+\s*/, ""), "<#>") + ); + } + } + context = context.replace(cardEvent.titleHeader, ( + "\n\n{%@MEM@%" + cardMemoriesText + "%@MEM@%}\n" + )); + isRemembering = true; + } + } + cardEvents.delete(titleKey); + break; + } + if (breakForCompression) { + break; + } + // Simplify auto-card titles which contain an obvious surname + const titleHeaderMatch = card.entry.match(titleHeaderMatcher); + if (!titleHeaderMatch) { + continue; + } + const [oldTitleHeader, oldTitle] = titleHeaderMatch; + if (!isAuto(oldTitle)) { + continue; + } + const surname = isNamed(oldTitle, true); + if (typeof surname !== "string") { + continue; + } + const newTitle = oldTitle.replace(" " + surname, ""); + const [oldTitleKey, newTitleKey] = [oldTitle, newTitle].map(title => title.toLowerCase()); + if (oldTitleKey === newTitleKey) { + continue; + } + // Preemptively mitigate some global state considered within the formatTitle scope + clearTransientTitles(); + AC.database.titles.used = ["%@%"]; + [used, forenames, surnames].forEach(nameset => nameset.add("%@%")); + // Premature optimization is the root of all evil + const newKey = formatTitle(newTitle).newKey; + clearTransientTitles(); + if (newKey === "") { + Internal.getUsedTitles(); + continue; + } + if (oldTitleKey in AC.database.memories.associations) { + AC.database.memories.associations[newTitleKey] = AC.database.memories.associations[oldTitleKey]; + delete AC.database.memories.associations[oldTitleKey]; + } + if (AC.compression.titleKey === oldTitleKey) { + AC.compression.titleKey = newTitleKey; + } + card.entry = card.entry.replace(oldTitleHeader, oldTitleHeader.replace(oldTitle, newTitle)); + card.keys = buildKeys(card.keys.replaceAll(" " + surname, ""), newKey); + Internal.getUsedTitles(); + function exceedsMemoryLimit() { + return ((function() { + const memoryLimitMatch = card.description.match(/limits?\s*:\s*(\d+)\s*}/i); + if (memoryLimitMatch) { + return validateMemoryLimit(parseInt(memoryLimitMatch[1], 10)); + } else { + return AC.config.defaultMemoryLimit; + } + })() < (function() { + const cardMemories = extractCardMemories(); + if (cardMemories.missing) { + return card.description; + } else { + return cardMemories.text; + } + })().length); + } + function prepareMemoryCompression(titleKey) { + AC.compression.oldMemoryBank = isolateMemories(extractCardMemories().text); + if (AC.compression.oldMemoryBank.length === 0) { + return false; + } + AC.compression.completed = 0; + AC.compression.titleKey = titleKey; + AC.compression.vanityTitle = cleanSpaces(card.title.trim()); + AC.compression.responseEstimate = (function() { + const responseEstimate = estimateResponseLength(); + if (responseEstimate === -1) { + return 1400 + } else { + return responseEstimate; + } + })(); + AC.compression.lastConstructIndex = -1; + AC.compression.newMemoryBank = []; + return true; + } + function extractCardMemories() { + const memoryHeaderMatch = card.description.match( + /(?<={\s*updates?\s*:[\s\S]*?,\s*limits?\s*:[\s\S]*?})[\s\S]*$/i + ); + if (memoryHeaderMatch) { + return O.f({missing: false, text: cleanSpaces(memoryHeaderMatch[0].trim())}); + } else { + return O.f({missing: true, text: ""}); + } + } + } + // Remove repeated memories plus any remaining title headers + context = (context + .replace(/(\s*<#>\s*)+/g, "\n") + .replace(titleHeaderPatternGlobal, "\n\n") + .replace(/World\s*Lore\s*:\s*/i, "World Lore:\n") + .replace(/Memories\s*:\s*(?=Recent\s*Story\s*:|$)/i, "") + ); + // Prompt the AI to generate a new card entry, compress an existing card's memories, or continue the story + let isGenerating = false; + let isCompressing = false; + if (isPendingGeneration()) { + promptGeneration(); + } else if (isAwaitingGeneration()) { + AC.generation.workpiece = AC.generation.pending.shift(); + promptGeneration(); + } else if (isPendingCompression()) { + promptCompression(); + } else if (AC.signal.recheckRetryOrErase) { + // Do nothing 😜 + } else if ((AC.generation.cooldown <= 0) && (0 < AC.database.titles.candidates.length)) { + // Prepare to automatically construct a new plot-relevant story card by selecting a title + let selectedTitle = (function() { + if (AC.database.titles.candidates.length === 1) { + return AC.database.titles.candidates[0][0]; + } else if (!isCandidatesSorted) { + sortCandidates(); + } + const mostRelevantTitle = AC.database.titles.candidates[0][0]; + if ((AC.database.titles.candidates.length < 16) || (Math.random() < 0.6667)) { + // Usually, 2/3 of the time, the most relevant title is selected + return mostRelevantTitle; + } + // Occasionally (1/3 of the time once the candidates databases has at least 16 titles) make a completely random selection between the top 4 most recently occuring title candidates which are NOT the top 2 most relevant titles. Note that relevance !== recency + // This gives non-character titles slightly better odds of being selected for card generation due to the relevance sorter's inherent bias towards characters; they tend to appear far more often in prose + return (AC.database.titles.candidates + // Create a shallow copy to avoid modifying AC.database.titles.candidates itself + // Add index to preserve original positions whenever ties occur during sorting + .map((candidate, index) => ({candidate, index})) + // Sort by each candidate's most recent turn + .sort((a, b) => { + const turnDiff = b.candidate[b.candidate.length - 1] - a.candidate[a.candidate.length - 1]; + if (turnDiff === 0) { + // Don't change indices in the case of a tie + return (a.index - b.index); + } else { + // No tie here, sort by recency + return turnDiff; + } + }) + // Get the top 6 most recent titles (4 + 2 because the top 2 relevant titles may be present) + .slice(0, 6) + // Extract only the title names + .map(element => element.candidate[0]) + // Exclude the top 2 most relevant titles + .filter(title => ((title !== mostRelevantTitle) && (title !== AC.database.titles.candidates[1][0]))) + // Ensure only 4 titles remain + .slice(0, 4) + )[Math.floor(Math.random() * 4)]; + })(); + while (!Internal.generateCard(O.f({title: selectedTitle}))) { + // This is an emergency precaution, I don't expect the interior of this while loop to EVER execute + // That said, it's crucial for the while condition be checked at least once, because Internal.generateCard appends an element to AC.generation.pending as a side effect + const lowerSelectedTitle = formatTitle(selectedTitle).newTitle.toLowerCase(); + const index = AC.database.titles.candidates.findIndex(candidate => { + return (formatTitle(candidate[0]).newTitle.toLowerCase() === lowerSelectedTitle); + }); + if (index === -1) { + // Should be impossible + break; + } + AC.database.titles.candidates.splice(index, 1); + if (AC.database.titles.candidates.length === 0) { + break; + } + selectedTitle = AC.database.titles.candidates[0][0]; + } + if (isAwaitingGeneration()) { + // Assign the workpiece so card generation may fully commence! + AC.generation.workpiece = AC.generation.pending.shift(); + promptGeneration(); + } else if (isPendingCompression()) { + promptCompression(); + } + } else if ( + (AC.chronometer.step || forceStep) + && (0 < AC.generation.cooldown) + && (AC.config.addCardCooldown !== 9999) + ) { + AC.generation.cooldown--; + } + if (shouldTrimContext()) { + // Truncate context based on AC.signal.maxChars, begin by individually removing the oldest sentences from the recent story portion of the context window + const recentStoryPattern = /Recent\s*Story\s*:\s*([\s\S]*?)(%@GEN@%|%@COM@%|\s\[\s*Author's\s*note\s*:|$)/i; + const recentStoryMatch = context.match(recentStoryPattern); + if (recentStoryMatch) { + const recentStory = recentStoryMatch[1]; + let sentencesJoined = recentStory; + // Split by the whitespace chars following each sentence (without consuming) + const sentences = splitBySentences(recentStory); + // [minimum num of story sentences] = ([max chars for context] / 6) / [average chars per sentence] + const sentencesMinimum = Math.ceil( + (AC.signal.maxChars / 6) / ( + boundInteger(1, context.length) / boundInteger(1, sentences.length) + ) + ) + 1; + do { + if (sentences.length < sentencesMinimum) { + // A minimum of n many recent story sentences must remain + // Where n represents a sentence count equal to roughly 16.7% of the full context chars + break; + } + // Remove the first (oldest) recent story sentence + sentences.shift(); + // Check if the total length exceeds the AC.signal.maxChars limit + sentencesJoined = sentences.join(""); + } while (AC.signal.maxChars < (context.length - recentStory.length + sentencesJoined.length + 3)); + // Rebuild the context with the truncated recentStory + context = context.replace(recentStoryPattern, "Recent Story:\n" + sentencesJoined + recentStoryMatch[2]); + } + if (isRemembering && shouldTrimContext()) { + // Next remove loaded card memories (if any) with top-down priority, one card at a time + do { + // This matcher relies on its case-sensitivity + const cardMemoriesMatch = context.match(/{%@MEM@%([\s\S]+?)%@MEM@%}/); + if (!cardMemoriesMatch) { + break; + } + context = context.replace(cardMemoriesMatch[0], (cardMemoriesMatch[0] + .replace(cardMemoriesMatch[1], "") + // Set the MEM tags to lowercase to avoid repeated future matches + .toLowerCase() + )); + } while (AC.signal.maxChars < (context.length + 3)); + } + if (shouldTrimContext()) { + // If the context is still too long, just trim from the beginning I guess šŸ¤·ā€ā™€ļø + context = context.slice(context.length - AC.signal.maxChars + 1); + } + } + if (isRemembering) { + // Card memory flags serve no further purpose + context = (context + // Case-insensitivity is crucial here + .replace(/(?<={%@MEM@%)\s*/gi, "") + .replace(/\s*(?=%@MEM@%})/gi, "") + .replace(/{%@MEM@%%@MEM@%}\s?/gi, "") + .replaceAll("{%@MEM@%", "{ Memories:\n") + .replaceAll("%@MEM@%}", " }") + ); + } + if (isGenerating) { + // Likewise for the card entry generation delimiter + context = context.replaceAll("%@GEN@%", ""); + } else if (isCompressing) { + // Or the (mutually exclusive) card memory compression delimiter + context = context.replaceAll("%@COM@%", ""); + } + CODOMAIN.initialize(context); + function isolateMemories(memoriesText) { + return (memoriesText + .split("\n") + .map(memory => cleanSpaces(memory.trim().replace(/^-+\s*/, ""))) + .filter(memory => (memory !== "")) + ); + } + function isAuto(title) { + return auto.has(title.toLowerCase()); + } + function promptCompression() { + isGenerating = false; + const cardEntryText = (function() { + const card = getAutoCard(AC.compression.titleKey); + if (card === null) { + return null; + } + const entryLines = formatEntry(card.entry).trimEnd().split("\n"); + if (Object.is(entryLines[0].trim(), "")) { + return ""; + } + for (let i = 0; i < entryLines.length; i++) { + entryLines[i] = entryLines[i].trim(); + if (/[a-zA-Z]$/.test(entryLines[i])) { + entryLines[i] += "."; + } + entryLines[i] += " "; + } + return entryLines.join(""); + })(); + if (cardEntryText === null) { + // Safety measure + resetCompressionProperties(); + return; + } + repositionAN(); + // The "%COM%" substring serves as a temporary delimiter for later context length trucation + context = context.trimEnd() + "\n\n" + cardEntryText + ( + [...AC.compression.newMemoryBank, ...AC.compression.oldMemoryBank].join(" ") + ) + "%@COM@%\n\n" + (function() { + const memoryConstruct = (function() { + if (AC.compression.lastConstructIndex === -1) { + for (let i = 0; i < AC.compression.oldMemoryBank.length; i++) { + AC.compression.lastConstructIndex = i; + const memoryConstruct = buildMemoryConstruct(); + if (( + (AC.config.memoryCompressionRatio / 10) * AC.compression.responseEstimate + ) < memoryConstruct.length) { + return memoryConstruct; + } + } + } else { + // The previous card memory compression attempt produced a bad output + AC.compression.lastConstructIndex = boundInteger( + 0, AC.compression.lastConstructIndex + 1, AC.compression.oldMemoryBank.length - 1 + ); + } + return buildMemoryConstruct(); + })(); + // Fill all %{title} placeholders + const precursorPrompt = insertTitle(AC.config.compressionPrompt, AC.compression.vanityTitle).trim(); + const memoryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*memor(y|ies)\s*}+/gi; + if (memoryPlaceholderPattern.test(precursorPrompt)) { + // Fill all %{memory} placeholders with a selection of pending old memories + return precursorPrompt.replace(memoryPlaceholderPattern, memoryConstruct); + } else { + // Append the partial entry to the end of context + return precursorPrompt + "\n\n" + memoryConstruct; + } + })() + "\n\n"; + isCompressing = true; + return; + } + function promptGeneration() { + repositionAN(); + // All %{title} placeholders were already filled during this workpiece's initialization + // The "%GEN%" substring serves as a temporary delimiter for later context length trucation + context = context.trimEnd() + "%@GEN@%\n\n" + (function() { + // For context only, remove the title header from this workpiece's partially completed entry + const partialEntry = formatEntry(AC.generation.workpiece.entry); + const entryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*entry\s*}+/gi; + if (entryPlaceholderPattern.test(AC.generation.workpiece.prompt)) { + // Fill all %{entry} placeholders with the partial entry + return AC.generation.workpiece.prompt.replace(entryPlaceholderPattern, partialEntry); + } else { + // Append the partial entry to the end of context + return AC.generation.workpiece.prompt.trimEnd() + "\n\n" + partialEntry; + } + })(); + isGenerating = true; + return; + } + function repositionAN() { + // Move the Author's Note further back in context during card generation (should still be considered) + const authorsNotePattern = /\s*(\[\s*Author's\s*note\s*:[\s\S]*\])\s*/i; + const authorsNoteMatch = context.match(authorsNotePattern); + if (!authorsNoteMatch) { + return; + } + const leadingSpaces = context.match(/^\s*/)[0]; + context = context.replace(authorsNotePattern, " ").trimStart(); + const recentStoryPattern = /\s*Recent\s*Story\s*:\s*/i; + if (recentStoryPattern.test(context)) { + // Remove author's note from its original position and insert above "Recent Story:\n" + context = (context + .replace(recentStoryPattern, "\n\n" + authorsNoteMatch[1] + "\n\nRecent Story:\n") + .trimStart() + ); + } else { + context = authorsNoteMatch[1] + "\n\n" + context; + } + context = leadingSpaces + context; + return; + } + function sortCandidates() { + if (AC.database.titles.candidates.length < 2) { + return; + } + const turnRange = boundInteger(1, maxTurn - minTurn); + const recencyExponent = Math.log10(turnRange) + 1.85; + // Sort the database of available title candidates by relevance + AC.database.titles.candidates.sort((a, b) => { + return relevanceScore(b) - relevanceScore(a); + }); + function relevanceScore(candidate) { + // weight = (((turn - minTurn) / (maxTurn - minTurn)) + 1)^(log10(maxTurn - minTurn) + 1.85) + return candidate.slice(1).reduce((sum, turn) => { + // Apply exponential scaling to give far more weight to recent turns + return sum + Math.pow(( + // The recency weight's exponent scales by log10(turnRange) + 1.85 + // Shhh don't question it 😜 + ((turn - minTurn) / turnRange) + 1 + ), recencyExponent); + }, 0); + } + return; + } + function shouldTrimContext() { + return (AC.signal.maxChars <= context.length); + } + function setCandidateTurnBounds(candidate) { + // candidate: ["Example Title", 0, 1, 2, 3] + minTurn = boundInteger(0, minTurn, candidate[1]); + maxTurn = boundInteger(candidate[candidate.length - 1], maxTurn); + return; + } + function disableAutoCards() { + AC.signal.forceToggle = null; + // Auto-Cards has been disabled + AC.config.doAC = false; + // Deconstruct the "Configure Auto-Cards" story card + unbanTitle(configureCardTemplate.title); + eraseCard(configureCard); + // Signal the construction of "Edit to enable Auto-Cards" during the next onOutput hook + AC.signal.swapControlCards = true; + // Post a success message + notify("Disabled! Use the \"Edit to enable Auto-Cards\" story card to undo"); + CODOMAIN.initialize(TEXT); + return; + } + break; } + case "output": { + // AutoCards was called within the output modifier + const output = prettifyEmDashes(TEXT); + if (0 < AC.chronometer.postpone) { + // Do not capture or replace any outputs during this turn + promoteAmnesia(); + if (permitOutput()) { + CODOMAIN.initialize(output); + } + } else if (AC.signal.swapControlCards) { + if (permitOutput()) { + CODOMAIN.initialize(output); + } + } else if (isPendingGeneration()) { + const textClone = prettifyEmDashes(text); + AC.chronometer.amnesia = 0; + AC.generation.completed++; + const generationsRemaining = (function() { + if ( + textClone.includes("\"") + || /(?<=^|\s|—|\(|\[|{)sa(ys?|id)(?=\s|\.|\?|!|,|;|—|\)|\]|}|$)/i.test(textClone) + ) { + // Discard full outputs containing "say" or quotations + // To build coherent entries, the AI must not attempt to continue the story + return skip(estimateRemainingGens()); + } + const oldSentences = (splitBySentences(formatEntry(AC.generation.workpiece.entry)) + .map(sentence => sentence.trim()) + .filter(sentence => (2 < sentence.length)) + ); + const seenSentences = new Set(); + const entryAddition = splitBySentences(textClone + .replace(/[\*_~]/g, "") + .replace(/:+/g, "#") + .replace(/\s+/g, " ") + ).map(sentence => (sentence + .trim() + .replace(/^-+\s*/, "") + )).filter(sentence => ( + // Remove empty strings + (sentence !== "") + // Remove colon ":" headers or other stinky symbols because me no like 😠 + && !/[#><@]/.test(sentence) + // Remove previously repeated sentences + && !oldSentences.some(oldSentence => (0.75 < similarityScore(oldSentence, sentence))) + // Remove repeated sentences from within entryAddition itself + && ![...seenSentences].some(seenSentence => (0.75 < similarityScore(seenSentence, sentence))) + // Simply ensure this sentence is henceforth unique + && seenSentences.add(sentence) + )).join(" ").trim() + " "; + if (entryAddition === " ") { + return skip(estimateRemainingGens()); + } else if ( + /^{title:[\s\S]*?}$/.test(AC.generation.workpiece.entry.trim()) + && (AC.generation.workpiece.entry.length < 111) + ) { + AC.generation.workpiece.entry += "\n" + entryAddition; + } else { + AC.generation.workpiece.entry += entryAddition; + } + if (AC.generation.workpiece.limit < AC.generation.workpiece.entry.length) { + let exit = false; + let truncatedEntry = AC.generation.workpiece.entry.trimEnd(); + const sentences = splitBySentences(truncatedEntry); + for (let i = sentences.length - 1; 0 <= i; i--) { + if (!sentences[i].includes("\n")) { + sentences.splice(i, 1); + truncatedEntry = sentences.join("").trimEnd(); + if (truncatedEntry.length <= AC.generation.workpiece.limit) { + break; + } + continue; + } + // Lines only matter for initial entries provided via AutoCards().API.generateCard + const lines = sentences[i].split("\n"); + for (let j = lines.length - 1; 0 <= j; j--) { + lines.splice(j, 1); + sentences[i] = lines.join("\n"); + truncatedEntry = sentences.join("").trimEnd(); + if (truncatedEntry.length <= AC.generation.workpiece.limit) { + // Exit from both loops + exit = true; + break; + } + } + if (exit) { + break; + } + } + if (truncatedEntry.length < 150) { + // Disregard the previous sentence/line-based truncation attempt + AC.generation.workpiece.entry = limitString( + AC.generation.workpiece.entry, AC.generation.workpiece.limit + ); + // Attempt to remove the last word/fragment + truncatedEntry = AC.generation.workpiece.entry.replace(/\s*\S+$/, ""); + if (150 <= truncatedEntry) { + AC.generation.workpiece.entry = truncatedEntry; + } + } else { + AC.generation.workpiece.entry = truncatedEntry; + } + return 0; + } else if ((AC.generation.workpiece.limit - 50) <= AC.generation.workpiece.entry.length) { + AC.generation.workpiece.entry = AC.generation.workpiece.entry.trimEnd(); + return 0; + } + function skip(remaining) { + if (AC.generation.permitted <= AC.generation.completed) { + AC.generation.workpiece.entry = AC.generation.workpiece.entry.trimEnd(); + return 0; + } + return remaining; + } + function estimateRemainingGens() { + const responseEstimate = estimateResponseLength(); + if (responseEstimate === -1) { + return 1; + } + const remaining = boundInteger(1, Math.round( + (150 + AC.generation.workpiece.limit - AC.generation.workpiece.entry.length) / responseEstimate + )); + if (AC.generation.permitted === 34) { + AC.generation.permitted = boundInteger(6, Math.floor(3.5 * remaining), 32); + } + return remaining; + } + return skip(estimateRemainingGens()); + })(); + postOutputMessage(textClone, AC.generation.completed / Math.min( + AC.generation.permitted, + AC.generation.completed + generationsRemaining + )); + if (generationsRemaining <= 0) { + notify("\"" + AC.generation.workpiece.title + "\" was successfully added to your story cards!"); + constructCard(O.f({ + type: AC.generation.workpiece.type, + title: AC.generation.workpiece.title, + keys: AC.generation.workpiece.keys, + entry: (function() { + if (!AC.config.bulletedListMode) { + return AC.generation.workpiece.entry; + } + const sentences = splitBySentences( + formatEntry( + AC.generation.workpiece.entry.replace(/\s+/g, " ") + ).replace(/:+/g, "#") + ).map(sentence => { + sentence = (sentence + .replaceAll("#", ":") + .trim() + .replace(/^-+\s*/, "") + ); + if (sentence.length < 12) { + return sentence; + } else { + return "\n- " + sentence.replace(/\s*[\.\?!]+$/, ""); + } + }); + const titleHeader = "{title: " + AC.generation.workpiece.title + "}"; + if (sentences.every(sentence => (sentence.length < 12))) { + const sentencesJoined = sentences.join(" ").trim(); + if (sentencesJoined === "") { + return titleHeader; + } else { + return limitString(titleHeader + "\n" + sentencesJoined, 2000); + } + } + for (let i = sentences.length - 1; 0 <= i; i--) { + const bulletedEntry = cleanSpaces(titleHeader + sentences.join(" ")).trimEnd(); + if (bulletedEntry.length <= 2000) { + return bulletedEntry; + } + if (sentences.length === 1) { + break; + } + sentences.splice(i, 1); + } + return limitString(AC.generation.workpiece.entry, 2000); + })(), + description: AC.generation.workpiece.description, + }), newCardIndex()); + AC.generation.cooldown = AC.config.addCardCooldown; + AC.generation.completed = 0; + AC.generation.permitted = 34; + AC.generation.workpiece = O.f({}); + clearTransientTitles(); + } + } else if (isPendingCompression()) { + const textClone = prettifyEmDashes(text); + AC.chronometer.amnesia = 0; + AC.compression.completed++; + const compressionsRemaining = (function() { + const newMemory = (textClone + // Remove some dumb stuff + .replace(/^[\s\S]*:/g, "") + .replace(/[\*_~#><@\[\]{}`\\]/g, " ") + // Remove bullets + .trim().replace(/^-+\s*/, "").replace(/\s*-+$/, "").replace(/\s*-\s+/g, " ") + // Condense consecutive whitespace + .replace(/\s+/g, " ") + ); + if ((AC.compression.oldMemoryBank.length - 1) <= AC.compression.lastConstructIndex) { + // Terminate this compression cycle; the memory construct cannot grow any further + AC.compression.newMemoryBank.push(newMemory); + return 0; + } else if ((newMemory.trim() !== "") && (newMemory.length < buildMemoryConstruct().length)) { + // Good output, preserve and then proceed onwards + AC.compression.oldMemoryBank.splice(0, AC.compression.lastConstructIndex + 1); + AC.compression.lastConstructIndex = -1; + AC.compression.newMemoryBank.push(newMemory); + } else { + // Bad output, discard and then try again + AC.compression.responseEstimate += 200; + } + return boundInteger(1, joinMemoryBank(AC.compression.oldMemoryBank).length) / AC.compression.responseEstimate; + })(); + postOutputMessage(textClone, AC.compression.completed / (AC.compression.completed + compressionsRemaining)); + if (compressionsRemaining <= 0) { + const card = getAutoCard(AC.compression.titleKey); + if (card === null) { + notify( + "Failed to apply summarized memories for \"" + AC.compression.vanityTitle + "\" due to a missing or invalid AC card title header!" + ); + } else { + const memoryHeaderMatch = card.description.match( + /(?<={\s*updates?\s*:[\s\S]*?,\s*limits?\s*:[\s\S]*?})[\s\S]*$/i + ); + if (memoryHeaderMatch) { + // Update the card memory bank + notify("Memories for \"" + AC.compression.vanityTitle + "\" were successfully summarized!"); + card.description = card.description.replace(memoryHeaderMatch[0], ( + "\n" + joinMemoryBank(AC.compression.newMemoryBank) + )); + } else { + notify( + "Failed to apply summarizes memories for \"" + AC.compression.vanityTitle + "\" due to a missing or invalid AC card memory header!" + ); + } + } + resetCompressionProperties(); + } else if (AC.compression.completed === 1) { + notify("Summarizing excess memories for \"" + AC.compression.vanityTitle + "\""); + } + function joinMemoryBank(memoryBank) { + return cleanSpaces("- " + memoryBank.join("\n- ")); + } + } else if (permitOutput()) { + CODOMAIN.initialize(output); + } + concludeOutputBlock((function() { + if (AC.signal.swapControlCards) { + return getConfigureCardTemplate(); + } else { + return null; + } + })()) + function postOutputMessage(textClone, ratio) { + if (!permitOutput()) { + // Do nothing + } else if (0.5 < similarityScore(textClone, output)) { + // To improve Auto-Cards' compatability with other scripts, I only bother to replace the output text when the original and new output texts have a similarity score above a particular threshold. Otherwise, I may safely assume the output text has already been replaced by another script and thus skip this step. + CODOMAIN.initialize( + getPrecedingNewlines() + ">>> please select \"continue\" (" + Math.round(ratio * 100) + "%) <<<\n\n" + ); + } else { + CODOMAIN.initialize(output); + } + return; + } + break; } + default: { + CODOMAIN.initialize(TEXT); + break; } + } + // Get an individual story card reference via titleKey + function getAutoCard(titleKey) { + return Internal.getCard(card => card.entry.toLowerCase().startsWith("{title: " + titleKey + "}")); + } + function buildMemoryConstruct() { + return (AC.compression.oldMemoryBank + .slice(0, AC.compression.lastConstructIndex + 1) + .join(" ") + ); + } + // Estimate the average AI response char count based on recent continue outputs + function estimateResponseLength() { + if (!Array.isArray(history) || (history.length === 0)) { + return -1; + } + const charCounts = []; + for (let i = 0; i < history.length; i++) { + const action = readPastAction(i); + if ((action.type === "continue") && !action.text.includes("<<<")) { + charCounts.push(action.text.length); + } + } + if (charCounts.length < 7) { + if (charCounts.length === 0) { + return -1; + } else if (charCounts.length < 4) { + return boundInteger(350, charCounts[0]); + } + charCounts.splice(3); + } + return boundInteger(175, Math.floor( + charCounts.reduce((sum, charCount) => { + return sum + charCount; + }, 0) / charCounts.length + )); + } + // Evalute how similar two strings are on the range [0, 1] + function similarityScore(strA, strB) { + if (strA === strB) { + return 1; + } + // Normalize both strings for further comparison purposes + const [cleanA, cleanB] = [strA, strB].map(str => limitString((str + .replace(/[0-9\s]/g, " ") + .trim() + .replace(/ +/g, " ") + .toLowerCase() + ), 1400)); + if (cleanA === cleanB) { + return 1; + } + // Compute the Levenshtein distance + const [lengthA, lengthB] = [cleanA, cleanB].map(str => str.length); + // I love DP ā¤ļø (dynamic programming) + const dp = Array(lengthA + 1).fill(null).map(() => Array(lengthB + 1).fill(0)); + for (let i = 0; i <= lengthA; i++) { + dp[i][0] = i; + } + for (let j = 0; j <= lengthB; j++) { + dp[0][j] = j; + } + for (let i = 1; i <= lengthA; i++) { + for (let j = 1; j <= lengthB; j++) { + if (cleanA[i - 1] === cleanB[j - 1]) { + // No cost if chars match, swipe right šŸ˜Ž + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min( + // Deletion + dp[i - 1][j] + 1, + // Insertion + dp[i][j - 1] + 1, + // Substitution + dp[i - 1][j - 1] + 1 + ); + } + } + } + // Convert distance to similarity score (1 - (distance / maxLength)) + return 1 - (dp[lengthA][lengthB] / Math.max(lengthA, lengthB)); + } + function splitBySentences(prose) { + // Don't split sentences on honorifics or abbreviations such as "Mr.", "Mrs.", "etc." + return (prose + .replace(new RegExp("(?<=\\s|\"|\\(|—|\\[|'|{|^)(?:" + ([...Words.honorifics, ...Words.abbreviations] + .map(word => word.replace(".", "")) + .join("|") + ) + ")\\.", "gi"), "$1%@%") + .split(/(?<=[\.\?!:]["\)'\]}]?\s+)(?=[^\p{Ll}\s])/u) + .map(sentence => sentence.replaceAll("%@%", ".")) + ); + } + function formatEntry(partialEntry) { + const cleanedEntry = cleanSpaces(partialEntry + .replace(/^{title:[\s\S]*?}/, "") + .replace(/[#><@*_~]/g, "") + .trim() + ).replace(/(?<=^|\n)-+\s*/g, ""); + if (cleanedEntry === "") { + return ""; + } else { + return cleanedEntry + " "; + } + } + // Resolve malformed em dashes (common AI cliche) + function prettifyEmDashes(str) { + return str.replace(/(? Auto-Cards automatically creates and updates plot-relevant story cards while you play. You may configure the following settings by replacing \"false\" with \"true\" (and vice versa) or by adjusting numbers for the appropriate settings.", + "> Disable Auto-Cards: false", + "> Show detailed guide: false", + "> Delete all automatic story cards: false", + "> Reset all config settings and prompts: false", + "> Pin this config card near the top: " + AC.config.pinConfigureCard, + "> Minimum turns cooldown for new cards: " + AC.config.addCardCooldown, + "> New cards use a bulleted list format: " + AC.config.bulletedListMode, + "> Maximum entry length for new cards: " + AC.config.defaultEntryLimit, + "> New cards perform memory updates: " + AC.config.defaultCardsDoMemoryUpdates, + "> Card memory bank preferred length: " + AC.config.defaultMemoryLimit, + "> Memory summary compression ratio: " + AC.config.memoryCompressionRatio, + "> Exclude all-caps from title detection: " + AC.config.ignoreAllCapsTitles, + "> Also detect titles from player inputs: " + AC.config.readFromInputs, + "> Minimum turns age for title detection: " + AC.config.minimumLookBackDistance, + "> Use Live Script Interface v2: " + (AC.config.LSIv2 !== null), + "> Log debug data in a separate card: " + AC.config.showDebugData + ); + } + function getConfigureCardDescription() { + return limitString(O.v(prose( + Words.delimiter, + "> AI prompt to generate new cards:", + limitString(AC.config.generationPrompt.trim(), 4350).trimEnd(), + Words.delimiter, + "> AI prompt to summarize card memories:", + limitString(AC.config.compressionPrompt.trim(), 4350).trimEnd(), + Words.delimiter, + "> Titles banned from new card creation:", + AC.database.titles.banned.join(", ") + )), 9850); + } + } else { + // Auto-Cards is currently disabled + switch(HOOK) { + case "input": { + if (/\/\s*A\s*C/i.test(text)) { + CODOMAIN.initialize(doPlayerCommands(text)); + } else { + CODOMAIN.initialize(TEXT); + } + break; } + case "context": { + // AutoCards was called within the context modifier + advanceChronometer(); + // Get or construct the "Edit to enable Auto-Cards" story card + const enableCardTemplate = getEnableCardTemplate(); + const enableCard = getSingletonCard(true, enableCardTemplate); + banTitle(enableCardTemplate.title); + pinAndSortCards(enableCard); + if (AC.signal.forceToggle) { + enableAutoCards(); + } else if (enableCard.entry !== enableCardTemplate.entry) { + if ((extractSettings(enableCard.entry)?.enableautocards === true) && (AC.signal.forceToggle !== false)) { + // Use optional chaining to check the existence of enableautocards before accessing its value + enableAutoCards(); + } else { + // Repair the damaged card entry + enableCard.entry = enableCardTemplate.entry; + } + } + AC.signal.forceToggle = null; + CODOMAIN.initialize(TEXT); + function enableAutoCards() { + // Auto-Cards has been enabled + AC.config.doAC = true; + // Deconstruct the "Edit to enable Auto-Cards" story card + unbanTitle(enableCardTemplate.title); + eraseCard(enableCard); + // Signal the construction of "Configure Auto-Cards" during the next onOutput hook + AC.signal.swapControlCards = true; + // Post a success message + notify("Enabled! You may now edit the \"Configure Auto-Cards\" story card"); + return; + } + break; } + case "output": { + // AutoCards was called within the output modifier + promoteAmnesia(); + if (permitOutput()) { + CODOMAIN.initialize(TEXT); + } + concludeOutputBlock((function() { + if (AC.signal.swapControlCards) { + return getEnableCardTemplate(); + } else { + return null; + } + })()); + break; } + default: { + CODOMAIN.initialize(TEXT); + break; } + } + function getEnableCardTemplate() { + const names = getControlVariants().enable; + return O.f({ + type: AC.config.defaultCardType, + title: names.title, + keys: names.keys, + entry: prose( + "> Auto-Cards automatically creates and updates plot-relevant story cards while you play. To enable this system, simply edit the \"false\" below to say \"true\" instead!", + "> Enable Auto-Cards: false"), + description: "Perform any Do/Say/Story/Continue action within your adventure to apply this change!" + }); + } + } + function hoistConst() { return (class Const { + // This helps me debug stuff uwu + #constant; + constructor(...args) { + if (args.length !== 0) { + this.constructor.#throwError([[(args.length === 1), "Const cannot be instantiated with a parameter"], ["Const cannot be instantiated with parameters"]]); + } else { + O.f(this); + return this; + } + } + declare(...args) { + if (args.length !== 0) { + this.constructor.#throwError([[(args.length === 1), "Instances of Const cannot be declared with a parameter"], ["Instances of Const cannot be declared with parameters"]]); + } else if (this.#constant === undefined) { + this.#constant = null; + return this; + } else if (this.#constant === null) { + this.constructor.#throwError("Instances of Const cannot be redeclared"); + } else { + this.constructor.#throwError("Instances of Const cannot be redeclared after initialization"); + } + } + initialize(...args) { + if (args.length !== 1) { + this.constructor.#throwError([[(args.length === 0), "Instances of Const cannot be initialized without a parameter"], ["Instances of Const cannot be initialized with multiple parameters"]]); + } else if (this.#constant === null) { + this.#constant = [args[0]]; + return this; + } else if (this.#constant === undefined) { + this.constructor.#throwError("Instances of Const cannot be initialized before declaration"); + } else { + this.constructor.#throwError("Instances of Const cannot be reinitialized"); + } + } + read(...args) { + if (args.length !== 0) { + this.constructor.#throwError([[(args.length === 1), "Instances of Const cannot be read with a parameter"], ["Instances of Const cannot read with any parameters"]]); + } else if (Array.isArray(this.#constant)) { + return this.#constant[0]; + } else if (this.#constant === null) { + this.constructor.#throwError("Despite prior declaration, instances of Const cannot be read before initialization"); + } else { + this.constructor.#throwError("Instances of Const cannot be read before initialization"); + } + } + // An error condition is paired with an error message [condition, message], call #throwError with an array of pairs to throw the message corresponding with the first true condition [[cndtn1, msg1], [cndtn2, msg2], [cndtn3, msg3], ...] The first conditionless array element always evaluates to true ('else') + static #throwError(...args) { + // Look, I thought I was going to use this more at the time okay + const [conditionalMessagesTable] = args; + const codomain = new Const().declare(); + const error = O.f(new Error((function() { + const codomain = new Const().declare(); + if (Array.isArray(conditionalMessagesTable)) { + const chosenPair = conditionalMessagesTable.find(function(...args) { + const [pair] = args; + const codomain = new Const().declare(); + if (Array.isArray(pair)) { + if ((pair.length === 1) && (typeof pair[0] === "string")) { + codomain.initialize(true); + } else if ( + (pair.length === 2) + && (typeof pair[0] === "boolean") + && (typeof pair[1] === "string") + ) { + codomain.initialize(pair[0]); + } else { + Const.#throwError("Const.#throwError encountered an invalid array element of conditionalMessagesTable"); + } + } else { + Const.#throwError("Const.#throwError encountered a non-array element within conditionalMessagesTable"); + } + return codomain.read(); + }); + if (Array.isArray(chosenPair)) { + if (chosenPair.length === 1) { + codomain.initialize(chosenPair[0]); + } else { + codomain.initialize(chosenPair[1]); + } + } else { + codomain.initialize("Const.#throwError was not called with any true conditions"); + } + } else if (typeof conditionalMessagesTable === "string") { + codomain.initialize(conditionalMessagesTable); + } else { + codomain.initialize("Const.#throwError could not parse the given argument"); + } + return codomain.read(); + })())); + if (error.stack) { + codomain.initialize(error.stack + .replace(/\(:/gi, "(") + .replace(/Error:|at\s*(?:#throwError|Const.(?:declare|initialize|read)|new\s*Const)\s*\(\d+:\d+\)/gi, "") + .replace(/AutoCards\s*\((\d+):(\d+)\)\s*at\s*:\d+:\d+\s*$/i, "AutoCards ($1:$2)") + .trim() + .replace(/\s+/g, " ") + ); + } else { + codomain.initialize(error.message); + } + throw codomain.read(); + } + }); } + function hoistO() { return (class O { + // Some Object class methods are annoyingly verbose for how often I use them šŸ‘æ + static f(obj) { + return Object.freeze(obj); + } + static v(base) { + return see(Words.copy) + base; + } + static s(obj) { + return Object.seal(obj); + } + }); } + function hoistWords() { return (class Words { static #cache = {}; static { + // Each word list is initialized only once before being cached! + const wordListInitializers = { + // Special-cased honorifics which are excluded from titles and ignored during split-by-sentences operations + honorifics: () => [ + "mr.", "ms.", "mrs.", "dr." + ], + // Other special-cased abbreviations used to reformat titles and split-by-sentences + abbreviations: () => [ + "sr.", "jr.", "etc.", "st.", "ex.", "inc." + ], + // Lowercase minor connector words which may exist within titles + minor: () => [ + "&", "the", "for", "of", "le", "la", "el" + ], + // Removed from shortened titles for improved memory detection and trigger keword assignments + peerage: () => [ + "sir", "lord", "lady", "king", "queen", "majesty", "duke", "duchess", "noble", "royal", "emperor", "empress", "great", "prince", "princess", "count", "countess", "baron", "baroness", "archduke", "archduchess", "marquis", "marquess", "viscount", "viscountess", "consort", "grand", "sultan", "sheikh", "tsar", "tsarina", "czar", "czarina", "viceroy", "monarch", "regent", "imperial", "sovereign", "president", "prime", "minister", "nurse", "doctor", "saint", "general", "private", "commander", "captain", "lieutenant", "sergeant", "admiral", "marshal", "baronet", "emir", "chancellor", "archbishop", "bishop", "cardinal", "abbot", "abbess", "shah", "maharaja", "maharani", "councillor", "squire", "lordship", "ladyship", "monseigneur", "mayor", "princeps", "chief", "chef", "their", "my", "his", "him", "he'd", "her", "she", "she'd", "you", "your", "yours", "you'd", "you've", "you'll", "yourself", "mine", "myself", "highness", "excellency", "farmer", "sheriff", "officer", "detective", "investigator", "miss", "mister", "colonel", "professor", "teacher", "agent", "heir", "heiress", "master", "mistress", "headmaster", "headmistress", "principal", "papa", "mama", "mommy", "daddy", "mother", "father", "grandma", "grandpa", "aunt", "auntie", "aunty", "uncle", "cousin", "sister", "brother", "holy", "holiness", "almighty", "senator", "congressman" + ], + // Common named entities represent special-cased INVALID card titles. Because these concepts are already abundant within the AI's training data, generating story cards for any of these would be both annoying and superfluous. Therefore, Words.entities is accessed during banned titles initialization to prevent their appearance + entities: () => [ + // Seasons + "spring", "summer", "autumn", "fall", "winter", + // Holidays + "halloween", "christmas", "thanksgiving", "easter", "hanukkah", "passover", "ramadan", "eid", "diwali", "new year", "new year eve", "valentine day", "oktoberfest", + // People terms + "mom", "dad", "child", "grandmother", "grandfather", "ladies", "gentlemen", "gentleman", "slave", + // Capitalizable pronoun thingys + "his", "him", "he'd", "her", "she", "she'd", "you", "your", "yours", "you'd", "you've", "you'll", "you're", "yourself", "mine", "myself", "this", "that", + // Religious figures & deities + "god", "jesus", "buddha", "allah", "christ", + // Religious texts & concepts + "bible", "holy bible", "qur'an", "quran", "hadith", "tafsir", "tanakh", "talmud", "torah", "vedas", "vatican", "paganism", "pagan", + // Religions & belief systems + "hindu", "hinduism", "christianity", "islam", "jew", "judaism", "taoism", "buddhist", "buddhism", "catholic", "baptist", + // Common locations + "earth", "moon", "sun", "new york city", "london", "paris", "tokyo", "beijing", "mumbai", "sydney", "berlin", "moscow", "los angeles", "san francisco", "chicago", "miami", "seattle", "vancouver", "toronto", "ottawa", "mexico city", "rio de janeiro", "cape town", "sao paulo", "bangkok", "delhi", "amsterdam", "seoul", "shanghai", "new delhi", "atlanta", "jerusalem", "africa", "north america", "south america", "central america", "asia", "north africa", "south africa", "boston", "rome", "america", "siberia", "new england", "manhattan", "bavaria", "catalonia", "greenland", "hong kong", "singapore", + // Countries & political entities + "china", "india", "japan", "germany", "france", "spain", "italy", "canada", "australia", "brazil", "south africa", "russia", "north korea", "south korea", "iran", "iraq", "syria", "saudi arabia", "afghanistan", "pakistan", "uk", "britain", "england", "scotland", "wales", "northern ireland", "usa", "united states", "united states of america", "mexico", "turkey", "greece", "portugal", "poland", "netherlands", "belgium", "sweden", "norway", "finland", "denmark", + // Organizations & unions + "united nations", "european union", "state", "nato", "nfl", "nba", "fbi", "cia", "harvard", "yale", "princeton", "ivy league", "little league", "nasa", "nsa", "noaa", "osha", "nascar", "daytona 500", "grand prix", "wwe", "mba", "superbowl", + // Currencies + "dollar", "euro", "pound", "yen", "rupee", "peso", "franc", "dinar", "bitcoin", "ethereum", "ruble", "won", "dirham", + // Landmarks + "sydney opera house", "eiffel tower", "statue of liberty", "big ben", "great wall of china", "taj mahal", "pyramids of giza", "grand canyon", "mount everest", + // Events + "world war i", "world war 1", "wwi", "wwii", "world war ii", "world war 2", "wwii", "ww2", "cold war", "brexit", "american revolution", "french revolution", "holocaust", "cuban missile crisis", + // Companies + "google", "microsoft", "apple", "amazon", "facebook", "tesla", "ibm", "intel", "samsung", "sony", "coca-cola", "nike", "ford", "chevy", "pontiac", "chrysler", "volkswagen", "lambo", "lamborghini", "ferrari", "pizza hut", "taco bell", "ai dungeon", "openai", "mcdonald", "mcdonalds", "kfc", "burger king", "disney", + // Nationalities & languages + "english", "french", "spanish", "german", "italian", "russian", "chinese", "japanese", "korean", "arabic", "portuguese", "hindi", "american", "canadian", "mexican", "brazilian", "indian", "australian", "egyptian", "greek", "swedish", "norwegian", "danish", "dutch", "turkish", "iranian", "ukraine", "asian", "british", "european", "polish", "thai", "vietnamese", "filipino", "malaysian", "indonesian", "finnish", "estonian", "latvian", "lithuanian", "czech", "slovak", "hungarian", "romanian", "bulgarian", "serbian", "croatian", "bosnian", "slovenian", "albanian", "georgian", "armenian", "azerbaijani", "kazakh", "uzbek", "mongolian", "hebrew", "persian", "pashto", "urdu", "bengali", "tamil", "telugu", "marathi", "gujarati", "swahili", "zulu", "xhosa", "african", "north african", "south african", "north american", "south american", "central american", "colombian", "argentinian", "chilean", "peruvian", "venezuelan", "ecuadorian", "bolivian", "paraguayan", "uruguayan", "cuban", "dominican", "arabian", "roman", "haitian", "puerto rican", "moroccan", "algerian", "tunisian", "saudi", "emirati", "qatarian", "bahraini", "omani", "yemeni", "syrian", "lebanese", "iraqi", "afghan", "pakistani", "sri lankan", "burmese", "laotian", "cambodian", "hawaiian", "victorian", + // Fantasy stuff + "elf", "elves", "elven", "dwarf", "dwarves", "dwarven", "human", "man", "men", "mankind", "humanity", + // IPs + "pokemon", "pokĆ©mon", "minecraft", "beetles", "band-aid", "bandaid", "band aid", "big mac", "gpt", "chatgpt", "gpt-2", "gpt-3", "gpt-4", "gpt-4o", "mixtral", "mistral", "linux", "windows", "mac", "happy meal", "disneyland", "disneyworld", + // US states + "alabama", "alaska", "arizona", "arkansas", "california", "colorado", "connecticut", "delaware", "florida", "georgia", "hawaii", "idaho", "illinois", "indiana", "iowa", "kansas", "kentucky", "louisiana", "maine", "massachusetts", "michigan", "minnesota", "mississippi", "missouri", "nebraska", "nevada", "new hampshire", "new jersey", "new mexico", "new york", "north carolina", "north dakota", "ohio", "oklahoma", "oregon", "pennsylvania", "rhode island", "south carolina", "south dakota", "tennessee", "texas", "utah", "vermont", "west virginia", "wisconsin", "wyoming", + // Canadian Provinces & Territories + "british columbia", "manitoba", "new brunswick", "labrador", "nova scotia", "ontario", "prince edward island", "quebec", "saskatchewan", "northwest territories", "nunavut", "yukon", "newfoundland", + // Australian States & Territories + "new south wales", "queensland", "south australia", "tasmania", "western australia", "australian capital territory", + // idk + "html", "javascript", "python", "java", "c++", "php", "bluetooth", "json", "sql", "word", "dna", "icbm", "npc", "usb", "rsvp", "omg", "brb", "lol", "rofl", "smh", "ttyl", "rubik", "adam", "t-shirt", "tshirt", "t shirt", "led", "leds", "laser", "lasers", "qna", "q&a", "vip", "human resource", "human resources", "llm", "llc", "ceo", "cfo", "coo", "office", "blt", "suv", "suvs", "ems", "emt", "cbt", "cpr", "ferris wheel", "toy", "pet", "plaything", "m o" + ], + // Unwanted values + undesirables: () => [ + [343332, 451737, 323433, 377817], [436425, 356928, 363825, 444048], [323433, 428868, 310497, 413952], [350097, 66825, 436425, 413952, 406593, 444048], [316932, 330000, 436425, 392073], [444048, 356928, 323433], [451737, 444048, 363825], [330000, 310497, 392073, 399300] + ], + delimiter: () => ( + "——————————————————————————" + ), + // Source code location + copy: () => [ + 126852, 33792, 211200, 384912, 336633, 310497, 436425, 336633, 33792, 459492, 363825, 436425, 363825, 444048, 33792, 392073, 483153, 33792, 139425, 175857, 33792, 152592, 451737, 399300, 350097, 336633, 406593, 399300, 33792, 413952, 428868, 406593, 343332, 363825, 384912, 336633, 33792, 135168, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 33792, 310497, 399300, 330000, 33792, 428868, 336633, 310497, 330000, 33792, 392073, 483153, 33792, 316932, 363825, 406593, 33792, 343332, 406593, 428868, 33792, 436425, 363825, 392073, 413952, 384912, 336633, 33792, 363825, 399300, 436425, 444048, 428868, 451737, 323433, 444048, 363825, 406593, 399300, 436425, 33792, 406593, 399300, 33792, 310497, 330000, 330000, 363825, 399300, 350097, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 444048, 406593, 33792, 483153, 406593, 451737, 428868, 33792, 436425, 323433, 336633, 399300, 310497, 428868, 363825, 406593, 436425, 35937, 33792, 3355672848, 139592360193, 3300, 3300, 356928, 444048, 444048, 413952, 436425, 111012, 72897, 72897, 413952, 384912, 310497, 483153, 69828, 310497, 363825, 330000, 451737, 399300, 350097, 336633, 406593, 399300, 69828, 323433, 406593, 392073, 72897, 413952, 428868, 406593, 343332, 363825, 384912, 336633, 72897, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 3300, 3300, 126852, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 459492, 79233, 69828, 76032, 69828, 76032, 33792, 363825, 436425, 33792, 310497, 399300, 33792, 406593, 413952, 336633, 399300, 66825, 436425, 406593, 451737, 428868, 323433, 336633, 33792, 436425, 323433, 428868, 363825, 413952, 444048, 33792, 343332, 406593, 428868, 33792, 139425, 175857, 33792, 152592, 451737, 399300, 350097, 336633, 406593, 399300, 33792, 392073, 310497, 330000, 336633, 33792, 316932, 483153, 33792, 190608, 336633, 467313, 330000, 190608, 336633, 310497, 356928, 69828, 33792, 261393, 406593, 451737, 33792, 356928, 310497, 459492, 336633, 33792, 392073, 483153, 33792, 343332, 451737, 384912, 384912, 33792, 413952, 336633, 428868, 392073, 363825, 436425, 436425, 363825, 406593, 399300, 33792, 444048, 406593, 33792, 451737, 436425, 336633, 33792, 139425, 451737, 444048, 406593, 66825, 148137, 310497, 428868, 330000, 436425, 33792, 467313, 363825, 444048, 356928, 363825, 399300, 33792, 483153, 406593, 451737, 428868, 33792, 413952, 336633, 428868, 436425, 406593, 399300, 310497, 384912, 33792, 406593, 428868, 33792, 413952, 451737, 316932, 384912, 363825, 436425, 356928, 336633, 330000, 33792, 436425, 323433, 336633, 399300, 310497, 428868, 363825, 406593, 436425, 35937, 3300, 126852, 33792, 261393, 406593, 451737, 50193, 428868, 336633, 33792, 310497, 384912, 436425, 406593, 33792, 467313, 336633, 384912, 323433, 406593, 392073, 336633, 33792, 444048, 406593, 33792, 336633, 330000, 363825, 444048, 33792, 444048, 356928, 336633, 33792, 139425, 175857, 33792, 413952, 428868, 406593, 392073, 413952, 444048, 436425, 33792, 310497, 399300, 330000, 33792, 444048, 363825, 444048, 384912, 336633, 33792, 336633, 475200, 323433, 384912, 451737, 436425, 363825, 406593, 399300, 436425, 33792, 413952, 428868, 406593, 459492, 363825, 330000, 336633, 330000, 33792, 316932, 336633, 384912, 406593, 467313, 69828, 33792, 175857, 33792, 436425, 363825, 399300, 323433, 336633, 428868, 336633, 384912, 483153, 33792, 356928, 406593, 413952, 336633, 33792, 483153, 406593, 451737, 33792, 336633, 399300, 370788, 406593, 483153, 33792, 483153, 406593, 451737, 428868, 33792, 310497, 330000, 459492, 336633, 399300, 444048, 451737, 428868, 336633, 436425, 35937, 33792, 101128769412, 106046468352, 3300 + ], + // Card interface names reserved for use within LSIv2 + reserved: () => ({ + library: "Shared Library", input: "Input Modifier", context: "Context Modifier", output: "Output Modifier", guide: "LSIv2 Guide", state: "State Display", log: "Console Log" + }), + // Acceptable config settings which are coerced to true + trues: () => [ + "true", "t", "yes", "y", "on" + ], + // Acceptable config settings which are coerced to false + falses: () => [ + "false", "f", "no", "n", "off" + ], + guide: () => prose( + ">>> Detailed Guide:", + "Auto-Cards was made by LewdLeah ā¤ļø", + "", + Words.delimiter, + "", + "šŸ’” What is Auto-Cards?", + "Auto-Cards is a plug-and-play script for AI Dungeon that watches your story and automatically writes plot-relevant story cards during normal gameplay. A forgetful AI breaks my immersion, therefore my primary goal was to address the \"object permanence problem\" by extending story cards and memories with deeper automation. Auto-Cards builds a living reference of your adventure's world as you go. For your own convenience, all of this stuff is handled in the background. Though you're certainly welcome to customize various settings or use in-game commands for more precise control", + "", + Words.delimiter, + "", + " šŸ“Œ Main Features", + "- Detects named entities from your story and periodically writes new cards", + "- Smart long-term memory updates and summaries for important cards", + "- Fully customizable AI card generation and memory summarization prompts", + "- Optional in-game commands to manually direct the card generation process", + "- Free and open source for anyone to use within their own projects", + "- Compatible with other scripts and includes an external API", + "- Optional in-game scripting interface (LSIv2)", + "", + Words.delimiter, + "", + "āš™ļø Config Settings", + "You may, at any time, fine-tune your settings in-game by editing their values within the config card's entry section. Simply swap true/false or tweak numbers where appropriate", + "", + "> Disable Auto-Cards:", + "Turns the whole system off if true", + "", + "> Show detailed guide:", + "If true, shows this player guide in-game", + "", + "> Delete all automatic story cards:", + "Removes every auto-card present in your adventure", + "", + "> Reset all config settings and prompts:", + "Restores all settings and prompts to their original default values", + "", + "> Pin this config card near the top:", + "Keeps the config card pinned high on your cards list", + "", + "> Minimum turns cooldown for new cards:", + "How many turns (minimum) to wait between generating new cards. Using 9999 will pause periodic card generation while still allowing card memory updates to continue", + "", + "> New cards use a bulleted list format:", + "If true, new entries will use bullet points instead of pure prose", + "", + "> Maximum entry length for new cards:", + "Caps how long newly generated card entries can be (in characters)", + "", + "> New cards perform memory updates:", + "If true, new cards will automatically experience memory updates over time", + "", + "> Card memory bank preferred length:", + "Character count threshold before card memories are summarized to save space", + "", + "> Memory summary compression ratio:", + "Controls how much to compress when summarizing long card memory banks", + "(ratio = 10 * old / new ... such that 25 -> 2.5x shorter)", + "", + "> Exclude all-caps from title detection:", + "Prevents all-caps words like \"RUN\" from being parsed as viable titles", + "", + "> Also detect titles from player inputs:", + "Allows your typed Do/Say/Story action inputs to help suggest new card topics. Set to false if you have bad grammar, or if you're German (due to idiosyncratic noun capitalization habits)", + "", + "> Minimum turns age for title detection:", + "How many actions back the script looks when parsing recent titles from your story", + "", + "> Use Live Script Interface v2:", + "Enables LSIv2 for extra scripting magic and advanced control via arbitrary code execution", + "", + "> Log debug data in a separate card:", + "Shows a debug card if set to true", + "", + Words.delimiter, + "", + "āœļø AI Prompts", + "You may specify how the AI handles story card processes by editing either of these two prompts within the config card's notes section", + "", + "> AI prompt to generate new cards:", + "Used when Auto-Cards writes a new card entry. It tells the AI to focus on important plot stuff, avoid fluff, and write in a consistent, polished style. I like to add some personal preferences here when playing my own adventures. \"%{title}\" and \"%{entry}\" are dynamic placeholders for their namesakes", + "", + "> AI prompt to summarize card memories:", + "Summarizes older details within card memory banks to keep everything concise and neat over the long-run. Maintains only the most important details, written in the past tense. \"%{title}\" and \"%{memory}\" are dynamic placeholders for their namesakes", + "", + Words.delimiter, + "", + "ā›” Banned Titles List", + "This list prevents new cards from being created for super generic or unhelpful titles such as North, Tuesday, or December. You may edit these at the bottom of the config card's notes section. Capitalization and plural/singular forms are handled for you, so no worries about that", + "", + "> Titles banned from automatic new card generation:", + "North, East, South, West, and so on...", + "", + Words.delimiter, + "", + "šŸ”‘ In-Game Commands (/ac)", + "Use these commands to manually interact with Auto-Cards, simply type them into a Do/Say/Story input action", + "", + "/ac", + "Sets your actual cooldown to 0 and immediately attempts to generate a new card for the most relevant unused title from your story (if one exists)", + "", + "/ac Your Title Goes Here", + "Will immediately begin generating a new story card with the given title", + "Example use: \"/ac Leah\"", + "", + "/ac Your Title Goes Here / Your extra prompt details go here", + "Similar to the previous case, but with additional context to include with the card generation prompt", + "Example use: \"/ac Leah / Focus on Leah's works of artifice and ingenuity\"", + "", + "/ac Your Title Goes Here / Your extra prompt details go here / Your starter entry goes here", + "Again, similar to the previous case, but with an initial card entry for the generator to build upon", + "Example use: \"/ac Leah / Focus on Leah's works of artifice and ingenuity / You are a woman named Leah.\"", + "", + "/ac redo Your Title Goes Here", + "Rewrites your chosen story card, using the old card entry, memory bank, and story context for inspiration. Useful for recreating cards after important character development has occurred", + "Example use: \"/ac redo Leah\"", + "", + "/ac redo Your Title Goes Here / New info goes here", + "Similar to the previous case, but with additional info provided to guide the rewrite according to your additional specifications", + "Example use: \"/ac redo Leah / Leah recently achieved immortality\"", + "", + "/ac redo all", + "Recreates every single auto-card in your adventure. I must warn you though: This is very risky", + "", + "Extra Info:", + "- Invalid titles will fail. It's a technical limitation, sorry šŸ¤·ā€ā™€ļø", + "- Titles must be unique, unless you're attempting to use \"/ac redo\" for an existing card", + "- You may submit multiple commands using a single input to queue up a chained sequence of requests", + "- Capitalization doesn't matter, titles will be reformatted regardless", + "", + Words.delimiter, + "", + "šŸ”§ External API Functions (quick summary)", + "These are mainly for other JavaScript programmers to use, so feel free to ignore this section if that doesn't apply to you. Anyway, here's what each one does in plain terms, though please do refer to my source code for the full documentation", + "", + "AutoCards().API.postponeEvents();", + "Pauses Auto-Cards activity for n many turns", + "", + "AutoCards().API.emergencyHalt();", + "Emergency stop or resume", + "", + "AutoCards().API.suppressMessages();", + "Hides Auto-Cards toasts by preventing assignment to state.message", + "", + "AutoCards().API.debugLog();", + "Writes to the debug log card", + "", + "AutoCards().API.toggle();", + "Turns Auto-Cards on/off", + "", + "AutoCards().API.generateCard();", + "Initiates AI generation of the requested card", + "", + "AutoCards().API.redoCard();", + "Regenerates an existing card", + "", + "AutoCards().API.setCardAsAuto();", + "Flags or unflags a card as automatic", + "", + "AutoCards().API.addCardMemory();", + "Adds a memory to a specific card", + "", + "AutoCards().API.eraseAllAutoCards();", + "Deletes all auto-cards", + "", + "AutoCards().API.getUsedTitles();", + "Lists all current card titles and keys", + "", + "AutoCards().API.getBannedTitles();", + "Shows your current banned titles list", + "", + "AutoCards().API.setBannedTitles();", + "Replaces the banned titles list with a new list", + "", + "AutoCards().API.buildCard();", + "Makes a new card from scratch, using exact parameters", + "", + "AutoCards().API.getCard();", + "Finds cards that match a filter", + "", + "AutoCards().API.eraseCard();", + "Deletes cards matching a filter", + "", + "These API functions also work from within the LSIv2 scope, by the way", + "", + Words.delimiter, + "", + "ā¤ļø Special Thanks", + "This project flourished due to the incredible help, feedback, and encouragement from the AI Dungeon community. Your ideas, bug reports, testing, and support made Auto-Cards smarter, faster, and more fun for all. Please refer to my source code to learn more about everyone's specific contributions", + "", + "AHotHamster22, BinKompliziert, Boo, bottledfox, Bruno, Burnout, bweni, DebaczX, Dirty Kurtis, Dragranis, effortlyss, Hawk, Idle Confusion, ImprezA, Kat-Oli, KryptykAngel, Mad19pumpkin, Magic, Mirox80, Nathaniel Wyvern, NobodyIsUgly, OnyxFlame, Purplejump, Randy Viosca, RustyPawz, sinner, Sleepy pink, Vutinberg, Wilmar, Yi1i1i", + "", + Words.delimiter, + "", + "šŸŽ“ Random Tips", + "- The default setup works great out of the box, just play normally and watch your world build itself", + "- Enable AI Dungeon's built-in memory system for the best results", + "- Gameplay -> AI Models -> Memory System -> Memory Bank -> Toggle-ON to enable", + "- \"t\" and \"f\" are valid shorthand for \"true\" and \"false\" inside the config card", + "- If Auto-Cards goes overboard with new cards, you can pause it by setting the cooldown config to 9999", + "- Write \"{title:}\" anywhere within a regular story card's entry to transform it into an automatic card", + "- Feel free to import/export entire story card decks at any time", + "- Please copy my source code from here: https://play.aidungeon.com/profile/LewdLeah", + "", + Words.delimiter, + "", + "Happy adventuring! ā¤ļø", + "Please erase before continuing! <<<" + ) + }; + for (const wordList in wordListInitializers) { + // Define a lazy getter for every word list + Object.defineProperty(Words, wordList, { + configurable: false, + enumerable: true, + get() { + // If not already in cache, initialize and store the word list + if (!(wordList in Words.#cache)) { + Words.#cache[wordList] = O.f(wordListInitializers[wordList]()); + } + return Words.#cache[wordList]; + } + }); + } + } }); } + function hoistStringsHashed() { return (class StringsHashed { + // Used for information-dense past memory recognition + // Strings are converted to (reasonably) unique hashcodes for efficient existence checking + static #defaultSize = 65536; + #size; + #store; + constructor(size = StringsHashed.#defaultSize) { + this.#size = size; + this.#store = new Set(); + return this; + } + static deserialize(serialized, size = StringsHashed.#defaultSize) { + const stringsHashed = new StringsHashed(size); + stringsHashed.#store = new Set(serialized.split(",")); + return stringsHashed; + } + serialize() { + return Array.from(this.#store).join(","); + } + has(str) { + return this.#store.has(this.#hash(str)); + } + add(str) { + this.#store.add(this.#hash(str)); + return this; + } + remove(str) { + this.#store.delete(this.#hash(str)); + return this; + } + size() { + return this.#store.size; + } + latest(keepLatestCardinality) { + if (this.#store.size <= keepLatestCardinality) { + return this; + } + const excess = this.#store.size - keepLatestCardinality; + const iterator = this.#store.values(); + for (let i = 0; i < excess; i++) { + // The oldest hashcodes are removed first (insertion order matters!) + this.#store.delete(iterator.next().value); + } + return this; + } + #hash(str) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((31 * hash) + str.charCodeAt(i)) % this.#size; + } + return hash.toString(36); + } + }); } + function hoistInternal() { return (class Internal { + // Some exported API functions are internally reused by AutoCards + // Recursively calling AutoCards().API is computationally wasteful + // AutoCards uses this collection of static methods as an internal proxy + static generateCard(request, predefinedPair = ["", ""]) { + // Method call guide: + // Internal.generateCard({ + // // All properties except 'title' are optional + // type: "card type, defaults to 'class' for ease of filtering", + // title: "card title", + // keysStart: "preexisting card triggers", + // entryStart: "preexisting card entry", + // entryPrompt: "prompt the AI will use to complete this entry", + // entryPromptDetails: "extra details to include with this card's prompt", + // entryLimit: 750, // target character count for the generated entry + // description: "card notes", + // memoryStart: "preexisting card memory", + // memoryUpdates: true, // card updates when new relevant memories are formed + // memoryLimit: 2750, // max characters before the card memory is compressed + // }); + const titleKeyPair = formatTitle((request.title ?? "").toString()); + const title = predefinedPair[0] || titleKeyPair.newTitle; + if ( + (title === "") + || (("title" in AC.generation.workpiece) && (title === AC.generation.workpiece.title)) + || (isAwaitingGeneration() && (AC.generation.pending.some(pendingWorkpiece => ( + ("title" in pendingWorkpiece) && (title === pendingWorkpiece.title) + )))) + ) { + logEvent("The title '" + request.title + "' is invalid or unavailable for card generation", true); + return false; + } + AC.generation.pending.push(O.s({ + title: title, + type: limitString((request.type || AC.config.defaultCardType).toString().trim(), 100), + keys: predefinedPair[1] || buildKeys((request.keysStart ?? "").toString(), titleKeyPair.newKey), + entry: limitString("{title: " + title + "}" + cleanSpaces((function() { + const entry = (request.entryStart ?? "").toString().trim(); + if (entry === "") { + return ""; + } else { + return ("\n" + entry + (function() { + if (/[a-zA-Z]$/.test(entry)) { + return "."; + } else { + return ""; + } + })() + " "); + } + })()), 2000), + description: limitString(( + (function() { + const description = limitString((request.description ?? "").toString().trim(), 9900); + if (description === "") { + return ""; + } else { + return description + "\n\n"; + } + })() + "Auto-Cards will contextualize these memories:\n{updates: " + (function() { + if (typeof request.memoryUpdates === "boolean") { + return request.memoryUpdates; + } else { + return AC.config.defaultCardsDoMemoryUpdates; + } + })() + ", limit: " + validateMemoryLimit( + parseInt((request.memoryLimit || AC.config.defaultMemoryLimit), 10) + ) + "}" + (function() { + const cardMemoryBank = cleanSpaces((request.memoryStart ?? "").toString().trim()); + if (cardMemoryBank === "") { + return ""; + } else { + return "\n" + cardMemoryBank.split("\n").map(memory => addBullet(memory)).join("\n"); + } + })() + ), 10000), + prompt: (function() { + let prompt = insertTitle(( + (request.entryPrompt ?? "").toString().trim() || AC.config.generationPrompt.trim() + ), title); + let promptDetails = insertTitle(( + cleanSpaces((request.entryPromptDetails ?? "").toString().trim()) + ), title); + if (promptDetails !== "") { + const spacesPrecedingTerminalEntryPlaceholder = (function() { + const terminalEntryPlaceholderPattern = /(?:[%\$]+\s*|[%\$]*){+\s*entry\s*}+$/i; + if (terminalEntryPlaceholderPattern.test(prompt)) { + prompt = prompt.replace(terminalEntryPlaceholderPattern, ""); + const trailingSpaces = prompt.match(/(\s+)$/); + if (trailingSpaces) { + prompt = prompt.trimEnd(); + return trailingSpaces[1]; + } else { + return "\n\n"; + } + } else { + return ""; + } + })(); + switch(prompt[prompt.length - 1]) { + case "]": { encapsulateBothPrompts("[", true, "]"); break; } + case ">": { encapsulateBothPrompts(null, false, ">"); break; } + case "}": { encapsulateBothPrompts("{", true, "}"); break; } + case ")": { encapsulateBothPrompts("(", true, ")"); break; } + case "/": { encapsulateBothPrompts("/", true, "/"); break; } + case "#": { encapsulateBothPrompts("#", true, "#"); break; } + case "-": { encapsulateBothPrompts(null, false, "-"); break; } + case ":": { encapsulateBothPrompts(":", true, ":"); break; } + case "<": { encapsulateBothPrompts(">", true, "<"); break; } + }; + if (promptDetails.includes("\n")) { + const lines = promptDetails.split("\n"); + for (let i = 0; i < lines.length; i++) { + lines[i] = addBullet(lines[i].trim()); + } + promptDetails = lines.join("\n"); + } else { + promptDetails = addBullet(promptDetails); + } + prompt += "\n" + promptDetails + (function() { + if (spacesPrecedingTerminalEntryPlaceholder !== "") { + // Prompt previously contained a terminal %{entry} placeholder, re-append it + return spacesPrecedingTerminalEntryPlaceholder + "%{entry}"; + } + return ""; + })(); + function encapsulateBothPrompts(leftSymbol, slicesAtMiddle, rightSymbol) { + if (slicesAtMiddle) { + prompt = prompt.slice(0, -1).trim(); + if (promptDetails.startsWith(leftSymbol)) { + promptDetails = promptDetails.slice(1).trim(); + } + } + if (!promptDetails.endsWith(rightSymbol)) { + promptDetails += rightSymbol; + } + return; + } + } + return limitString(prompt, Math.floor(0.8 * AC.signal.maxChars)); + })(), + limit: validateEntryLimit(parseInt((request.entryLimit || AC.config.defaultEntryLimit), 10)) + })); + notify("Generating card for \"" + title + "\""); + function addBullet(str) { + return "- " + str.replace(/^-+\s*/, ""); + } + return true; + } + static redoCard(request, useOldInfo, newInfo) { + const card = getIntendedCard(request.title)[0]; + const oldCard = O.f({...card}); + if (!eraseCard(card)) { + return false; + } else if (newInfo !== "") { + request.entryPromptDetails = (request.entryPromptDetails ?? "").toString() + "\n" + newInfo; + } + O.f(request); + Internal.getUsedTitles(true); + if (!Internal.generateCard(request) && !Internal.generateCard(request, [ + (oldCard.entry.match(/^{title: ([\s\S]*?)}/)?.[1] || request.title.replace(/\w\S*/g, word => ( + word[0].toUpperCase() + word.slice(1).toLowerCase() + ))), oldCard.keys + ])) { + constructCard(oldCard, newCardIndex()); + Internal.getUsedTitles(true); + return false; + } else if (!useOldInfo) { + return true; + } + AC.generation.pending[AC.generation.pending.length - 1].prompt = (( + removeAutoProps(oldCard.entry) + "\n\n" + + removeAutoProps(isolateNotesAndMemories(oldCard.description)[1]) + ).trimEnd() + "\n\n" + AC.generation.pending[AC.generation.pending.length - 1].prompt).trim(); + return true; + } + // Sometimes it's helpful to log information elsewhere during development + // This log card is separate and distinct from the LSIv2 console log + static debugLog(...args) { + const debugCardName = "Debug Log"; + banTitle(debugCardName); + const card = getSingletonCard(true, O.f({ + type: AC.config.defaultCardType, + title: debugCardName, + keys: debugCardName, + entry: "The debug console log will print to the notes section below.", + description: Words.delimiter + "\nBEGIN DEBUG LOG" + })); + logToCard(card, ...args); + return card; + } + static eraseAllAutoCards() { + const cards = []; + Internal.getUsedTitles(true); + for (const card of storyCards) { + if (card.entry.startsWith("{title: ")) { + cards.push(card); + } + } + for (const card of cards) { + eraseCard(card); + } + auto.clear(); + forgetStuff(); + clearTransientTitles(); + AC.generation.pending = []; + AC.database.memories.associations = {}; + if (AC.config.deleteAllAutoCards) { + AC.config.deleteAllAutoCards = null; + } + return cards.length; + } + static getUsedTitles(isExternal = false) { + if (isExternal) { + bans.clear(); + isBanned("", true); + } else if (0 < AC.database.titles.used.length) { + return AC.database.titles.used; + } + // All unique used titles and keys encountered during this iteration + const seen = new Set(); + auto.clear(); + clearTransientTitles(); + AC.database.titles.used = ["%@%"]; + for (const card of storyCards) { + // Perform some common-sense maintenance while we're here + card.type = card.type.trim(); + card.title = card.title.trim(); + // card.keys should be left as-is + card.entry = card.entry.trim(); + card.description = card.description.trim(); + if (isExternal) { + O.s(card); + } else if (!shouldProceed()) { + checkRemaining(); + continue; + } + // An ideal auto-card's entry starts with "{title: Example of Greatness}" (example) + // An ideal auto-card's description contains "{updates: true, limit: 2750}" (example) + if (checkPlurals(denumberName(card.title.replace("\n", "")), t => isBanned(t))) { + checkRemaining(); + continue; + } else if (!card.keys.includes(",")) { + const cleanKeys = denumberName(card.keys.trim()); + if ((2 < cleanKeys.length) && checkPlurals(cleanKeys, t => isBanned(t))) { + checkRemaining(); + continue; + } + } + // Detect and repair malformed auto-card properties in a fault-tolerant manner + const traits = [card.entry, card.description].map((str, i) => { + // Absolute abomination uwu + const hasUpdates = /updates?\s*:[\s\S]*?(?:(?:title|limit)s?\s*:|})/i.test(str); + const hasLimit = /limits?\s*:[\s\S]*?(?:(?:title|update)s?\s*:|})/i.test(str); + return [(function() { + if (hasUpdates || hasLimit) { + if (/titles?\s*:[\s\S]*?(?:(?:limit|update)s?\s*:|})/i.test(str)) { + return 2; + } + return false; + } else if (/titles?\s*:[\s\S]*?}/i.test(str)) { + return 1; + } else if (!( + (i === 0) + && /{[\s\S]*?}/.test(str) + && (str.match(/{/g)?.length === 1) + && (str.match(/}/g)?.length === 1) + )) { + return false; + } + const badTitleHeaderMatch = str.match(/{([\s\S]*?)}/); + if (!badTitleHeaderMatch) { + return false; + } + const inferredTitle = badTitleHeaderMatch[1].split(",")[0].trim(); + if ( + (2 < inferredTitle.length) + && (inferredTitle.length <= 100) + && (badTitleHeaderMatch[0].length < str.length) + ) { + // A rare case where the title's existence should be inferred from the enclosing {curly brackets} + return inferredTitle; + } + return false; + })(), hasUpdates, hasLimit]; + }).flat(); + if (traits.every(trait => !trait)) { + // This card contains no auto-card traits, not even malformed ones + checkRemaining(); + continue; + } + const [ + hasEntryTitle, + hasEntryUpdates, + hasEntryLimit, + hasDescTitle, + hasDescUpdates, + hasDescLimit + ] = traits; + // Handle all story cards which belong to the Auto-Cards ecosystem + // May flag this damaged auto-card for later repairs + // May flag this duplicate auto-card for deformatting (will become a regular story card) + let repair = false; + let release = false; + const title = (function() { + let title = ""; + if (typeof hasEntryTitle === "string") { + repair = true; + title = formatTitle(hasEntryTitle).newTitle; + if (hasDescTitle && bad()) { + title = parseTitle(false); + } + } else if (hasEntryTitle) { + title = parseTitle(true); + if (hasDescTitle) { + repair = true; + if (bad()) { + title = parseTitle(false); + } + } else if (1 < card.entry.match(/titles?\s*:/gi)?.length) { + repair = true; + } + } else if (hasDescTitle) { + repair = true; + title = parseTitle(false); + } + if (bad()) { + repair = true; + title = formatTitle(card.title).newTitle; + if (bad()) { + release = true; + } else { + seen.add(title); + auto.add(title.toLowerCase()); + } + } else { + seen.add(title); + auto.add(title.toLowerCase()); + const titleHeader = "{title: " + title + "}"; + if (!repair && !((card.entry === titleHeader) || card.entry.startsWith(titleHeader + "\n"))) { + repair = true; + } + } + function bad() { + return ((title === "") || checkPlurals(title, t => auto.has(t))); + } + function parseTitle(fromEntry) { + const [sourceType, sourceText] = (function() { + if (fromEntry) { + return [hasEntryTitle, card.entry]; + } else { + return [hasDescTitle, card.description]; + } + })() + switch(sourceType) { + case 1: { + return formatTitle(isolateProperty( + sourceText, + /titles?\s*:[\s\S]*?}/i, + /(?:titles?\s*:|})/gi + )).newTitle; } + case 2: { + return formatTitle(isolateProperty( + sourceText, + /titles?\s*:[\s\S]*?(?:(?:limit|update)s?\s*:|})/i, + /(?:(?:title|update|limit)s?\s*:|})/gi + )).newTitle; } + default: { + return ""; } + } + } + return title; + })(); + if (release) { + // Remove Auto-Cards properties from this incompatible story card + safeRemoveProps(); + card.description = (card.description + .replace(/\s*Auto(?:-|\s*)Cards\s*will\s*contextualize\s*these\s*memories\s*:\s*/gi, "") + .replaceAll("%@%", "\n\n") + .trim() + ); + seen.delete(title); + checkRemaining(); + continue; + } + const memoryProperties = "{updates: " + (function() { + let updates = null; + if (hasDescUpdates) { + updates = parseUpdates(false); + if (hasEntryUpdates) { + repair = true; + if (bad()) { + updates = parseUpdates(true); + } + } else if (1 < card.description.match(/updates?\s*:/gi)?.length) { + repair = true; + } + } else if (hasEntryUpdates) { + repair = true; + updates = parseUpdates(true); + } + if (bad()) { + repair = true; + updates = AC.config.defaultCardsDoMemoryUpdates; + } + function bad() { + return (updates === null); + } + function parseUpdates(fromEntry) { + const updatesText = (isolateProperty( + (function() { + if (fromEntry) { + return card.entry; + } else { + return card.description; + } + })(), + /updates?\s*:[\s\S]*?(?:(?:title|limit)s?\s*:|})/i, + /(?:(?:title|update|limit)s?\s*:|})/gi + ).toLowerCase().replace(/[^a-z]/g, "")); + if (Words.trues.includes(updatesText)) { + return true; + } else if (Words.falses.includes(updatesText)) { + return false; + } else { + return null; + } + } + return updates; + })() + ", limit: " + (function() { + let limit = -1; + if (hasDescLimit) { + limit = parseLimit(false); + if (hasEntryLimit) { + repair = true; + if (bad()) { + limit = parseLimit(true); + } + } else if (1 < card.description.match(/limits?\s*:/gi)?.length) { + repair = true; + } + } else if (hasEntryLimit) { + repair = true; + limit = parseLimit(true); + } + if (bad()) { + repair = true; + limit = AC.config.defaultMemoryLimit; + } else { + limit = validateMemoryLimit(limit); + } + function bad() { + return (limit === -1); + } + function parseLimit(fromEntry) { + const limitText = (isolateProperty( + (function() { + if (fromEntry) { + return card.entry; + } else { + return card.description; + } + })(), + /limits?\s*:[\s\S]*?(?:(?:title|update)s?\s*:|})/i, + /(?:(?:title|update|limit)s?\s*:|})/gi + ).replace(/[^0-9]/g, "")); + if ((limitText === "")) { + return -1; + } else { + return parseInt(limitText, 10); + } + } + return limit.toString(); + })() + "}"; + if (!repair && (new RegExp("(?:^|\\n)" + memoryProperties + "(?:\\n|$)")).test(card.description)) { + // There are no serious repairs to perform + card.entry = cleanSpaces(card.entry); + const [notes, memories] = isolateNotesAndMemories(card.description); + const pureMemories = cleanSpaces(memories.replace(memoryProperties, "").trim()); + rejoinDescription(notes, memoryProperties, pureMemories); + checkRemaining(); + continue; + } + // Damage was detected, perform an adaptive repair on this auto-card's configurable properties + card.description = card.description.replaceAll("%@%", "\n\n"); + safeRemoveProps(); + card.entry = limitString(("{title: " + title + "}\n" + card.entry).trimEnd(), 2000); + const [left, right] = card.description.split("%@%"); + rejoinDescription(left, memoryProperties, right); + checkRemaining(); + function safeRemoveProps() { + if (typeof hasEntryTitle === "string") { + card.entry = card.entry.replace(/{[\s\S]*?}/g, ""); + } + card.entry = removeAutoProps(card.entry); + const [notes, memories] = isolateNotesAndMemories(card.description); + card.description = notes + "%@%" + removeAutoProps(memories); + return; + } + function rejoinDescription(notes, memoryProperties, memories) { + card.description = limitString((notes + (function() { + if (notes === "") { + return ""; + } else if (notes.endsWith("Auto-Cards will contextualize these memories:")) { + return "\n"; + } else { + return "\n\n"; + } + })() + memoryProperties + (function() { + if (memories === "") { + return ""; + } else { + return "\n"; + } + })() + memories), 10000); + return; + } + function isolateProperty(sourceText, propMatcher, propCleaner) { + return ((sourceText.match(propMatcher)?.[0] || "") + .replace(propCleaner, "") + .split(",")[0] + .trim() + ); + } + // Observe literal card titles and keys + function checkRemaining() { + const literalTitles = [card.title, ...card.keys.split(",")]; + for (let i = 0; i < literalTitles.length; i++) { + // The pre-format set inclusion check helps avoid superfluous formatTitle calls + literalTitles[i] = (literalTitles[i] + .replace(/["\.\?!;\(\):\[\]—{}]/g, " ") + .trim() + .replace(/\s+/g, " ") + .replace(/^'\s*/, "") + .replace(/\s*'$/, "") + ); + if (seen.has(literalTitles[i])) { + continue; + } + literalTitles[i] = formatTitle(literalTitles[i]).newTitle; + if (literalTitles[i] !== "") { + seen.add(literalTitles[i]); + } + } + return; + } + function denumberName(name) { + if (2 < (name.match(/[^\d\s]/g) || []).length) { + // Important for identifying LSIv2 auxiliary code cards when banned + return name.replace(/\s*\d+$/, ""); + } else { + return name; + } + } + } + clearTransientTitles(); + AC.database.titles.used = [...seen]; + return AC.database.titles.used; + } + static getBannedTitles() { + // AC.database.titles.banned is an array, not a set; order matters + return AC.database.titles.banned; + } + static setBannedTitles(newBans, isFinalAssignment) { + AC.database.titles.banned = []; + AC.database.titles.pendingBans = []; + AC.database.titles.pendingUnbans = []; + for (let i = newBans.length - 1; 0 <= i; i--) { + banTitle(newBans[i], isFinalAssignment); + } + return AC.database.titles.banned; + } + static getCard(predicate, getAll) { + if (getAll) { + // Return an array of card references which satisfy the given condition + const collectedCards = []; + for (const card of storyCards) { + if (predicate(card)) { + O.s(card); + collectedCards.push(card); + } + } + return collectedCards; + } + // Return a reference to the first card which satisfies the given condition + for (const card of storyCards) { + if (predicate(card)) { + return O.s(card); + } + } + return null; + } + }); } + function validateCooldown(cooldown) { + return boundInteger(0, cooldown, 9999, 22); + } + function validateEntryLimit(entryLimit) { + return boundInteger(200, entryLimit, 2000, 750); + } + function validateMemoryLimit(memoryLimit) { + return boundInteger(1750, memoryLimit, 9900, 2750); + } + function validateMemCompRatio(memCompressRatio) { + return boundInteger(20, memCompressRatio, 1250, 25); + } + function validateMinLookBackDist(minLookBackDist) { + return boundInteger(2, minLookBackDist, 88, 7); + } + function getDefaultConfig() { + function check(value, fallback = true, type = "boolean") { + if (typeof value === type) { + return value; + } else { + return fallback; + } + } + return O.s({ + // Is Auto-Cards enabled? + doAC: check(DEFAULT_DO_AC), + // Delete all previously generated story cards? + deleteAllAutoCards: null, + // Pin the configuration interface story card near the top? + pinConfigureCard: check(DEFAULT_PIN_CONFIGURE_CARD), + // Minimum number of turns in between automatic card generation events? + addCardCooldown: validateCooldown(DEFAULT_CARD_CREATION_COOLDOWN), + // Use bulleted list mode for newly generated card entries? + bulletedListMode: check(DEFAULT_USE_BULLETED_LIST_MODE), + // Maximum allowed length for newly generated story card entries? + defaultEntryLimit: validateEntryLimit(DEFAULT_GENERATED_ENTRY_LIMIT), + // Do newly generated cards have memory updates enabled by default? + defaultCardsDoMemoryUpdates: check(DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES), + // Default character limit before the card's memory bank is summarized? + defaultMemoryLimit: validateMemoryLimit(DEFAULT_NEW_CARDS_MEMORY_LIMIT), + // Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new) + memoryCompressionRatio: validateMemCompRatio(DEFAULT_MEMORY_COMPRESSION_RATIO), + // Ignore all-caps during title candidate detection? + ignoreAllCapsTitles: check(DEFAULT_IGNORE_ALL_CAPS_TITLES), + // Should player input actions (Do/Say/Story) be considered for future named entity detection? + readFromInputs: check(DEFAULT_DETECT_TITLES_FROM_INPUTS), + // How many (minimum) actions in the past does Auto-Cards look for named entities? + minimumLookBackDistance: validateMinLookBackDist(DEFAULT_MINIMUM_LOOK_BACK_DISTANCE), + // Is Live Script Interface v2 enabled? + LSIv2: (function() { + if (DEFAULT_DO_LSI_V2 === true) { + return true; + } else { + // Intrepret "false" as null, both here and for later config card reads + return null; + } + })(), + // Should the debug data card be visible? + showDebugData: check(DEFAULT_SHOW_DEBUG_DATA, false), + // How should the AI be prompted when generating new story card entries? + generationPrompt: check(DEFAULT_CARD_GENERATION_PROMPT, prose( + "-----", + "", + "", + "# Stop the story and ignore previous instructions. Write a brief and coherent informational entry for %{title} following these instructions:", + "- Write only third-person pure prose information about %{title} using complete sentences with correct punctuation", + "- Avoid short-term temporary details or appearances, instead focus on plot-significant information", + "- Prioritize story-relevant details about %{title} first to ensure seamless integration with the previous plot", + "- Create new information based on the context and story direction", + "- Mention %{title} in every sentence", + "- Use semicolons if needed", + "- Add additional details about %{title} beneath incomplete entries", + "- Be concise and grounded", + "- Imitate the story's writing style and infer the reader's preferences", + "", + "Continue the entry for %{title} below while avoiding repetition:", + "%{entry}" + ), "string"), + // How should the AI be prompted when summarizing memories for a given story card? + compressionPrompt: check(DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT, prose( + "-----", + "", + "", + "# Stop the story and ignore previous instructions. Summarize and condense the given paragraph into a narrow and focused memory passage while following these guidelines:", + "- Ensure the passage retains the core meaning and most essential details", + "- Use the third-person perspective", + "- Prioritize information-density, accuracy, and completeness", + "- Remain brief and concise", + "- Write firmly in the past tense", + "- The paragraph below pertains to old events from far earlier in the story", + "- Integrate %{title} naturally within the memory; however, only write about the events as they occurred", + "- Only reference information present inside the paragraph itself, be specific", + "", + "Write a summarized old memory passage for %{title} based only on the following paragraph:", + "\"\"\"", + "%{memory}", + "\"\"\"", + "Summarize below:" + ), "string"), + // All cards constructed by AC will inherit this type by default + defaultCardType: check(DEFAULT_CARD_TYPE, "class", "string") + }); + } + function getDefaultConfigBans() { + if (typeof DEFAULT_BANNED_TITLES_LIST === "string") { + return uniqueTitlesArray(DEFAULT_BANNED_TITLES_LIST.split(",")); + } else { + return [ + "North", "East", "South", "West", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ]; + } + } + function uniqueTitlesArray(titles) { + const existingTitles = new Set(); + return (titles + .map(title => title.trim().replace(/\s+/g, " ")) + .filter(title => { + if (title === "") { + return false; + } + const lowerTitle = title.toLowerCase(); + if (existingTitles.has(lowerTitle)) { + return false; + } else { + existingTitles.add(lowerTitle); + return true; + } + }) + ); + } + function boundInteger(lowerBound, value, upperBound, fallback) { + if (!Number.isInteger(value)) { + if (!Number.isInteger(fallback)) { + throw new Error("Invalid arguments: value and fallback are not integers"); + } + value = fallback; + } + if (Number.isInteger(lowerBound) && (value < lowerBound)) { + if (Number.isInteger(upperBound) && (upperBound < lowerBound)) { + throw new Error("Invalid arguments: The inequality (lowerBound <= upperBound) must be satisfied"); + } + return lowerBound; + } else if (Number.isInteger(upperBound) && (upperBound < value)) { + return upperBound; + } else { + return value; + } + } + function limitString(str, lengthLimit) { + if (lengthLimit < str.length) { + return str.slice(0, lengthLimit).trim(); + } else { + return str; + } + } + function cleanSpaces(unclean) { + return (unclean + .replace(/\s*\n\s*/g, "\n") + .replace(/\t/g, " ") + .replace(/ +/g, " ") + ); + } + function isolateNotesAndMemories(str) { + const bisector = str.search(/\s*(?:{|(?:title|update|limit)s?\s*:)\s*/i); + if (bisector === -1) { + return [str, ""]; + } else { + return [str.slice(0, bisector), str.slice(bisector)]; + } + } + function removeAutoProps(str) { + return cleanSpaces(str + .replace(/\s*{([\s\S]*?)}\s*/g, (bracedMatch, enclosedProperties) => { + if (enclosedProperties.trim().length < 150) { + return "\n"; + } else { + return bracedMatch; + } + }) + .replace(( + /\s*(?:{|(?:title|update|limit)s?\s*:)(?:[\s\S]{0,150}?)(?=(?:title|update|limit)s?\s*:|})\s*/gi + ), "\n") + .replace(/\s*(?:{|(?:title|update|limit)s?\s*:|})\s*/gi, "\n") + .trim() + ); + } + function insertTitle(prompt, title) { + return prompt.replace(( + /(?:[%\$]+\s*|[%\$]*){+\s*(?:titles?|names?|characters?|class(?:es)?|races?|locations?|factions?)\s*}+/gi + ), title); + } + function prose(...args) { + return args.join("\n"); + } + function buildKeys(keys, key) { + key = key.trim().replace(/\s+/g, " "); + const keyset = []; + if (key === "") { + return keys; + } else if (keys.trim() !== "") { + keyset.push(...keys.split(",")); + const lowerKey = key.toLowerCase(); + for (let i = keyset.length - 1; 0 <= i; i--) { + const preKey = keyset[i].trim().replace(/\s+/g, " ").toLowerCase(); + if ((preKey === "") || preKey.includes(lowerKey)) { + keyset.splice(i, 1); + } + } + } + if (key.length < 6) { + keyset.push(...[ + " " + key + " ", " " + key + "'", "\"" + key + " ", " " + key + ".", " " + key + "?", " " + key + "!", " " + key + ";", "'" + key + " ", "(" + key + " ", " " + key + ")", " " + key + ":", " " + key + "\"", "[" + key + " ", " " + key + "]", "—" + key + " ", " " + key + "—", "{" + key + " ", " " + key + "}" + ]); + } else if (key.length < 9) { + keyset.push(...[ + key + " ", " " + key, key + "'", "\"" + key, key + ".", key + "?", key + "!", key + ";", "'" + key, "(" + key, key + ")", key + ":", key + "\"", "[" + key, key + "]", "—" + key, key + "—", "{" + key, key + "}" + ]); + } else { + keyset.push(key); + } + keys = keyset[0] || key; + let i = 1; + while ((i < keyset.length) && ((keys.length + 1 + keyset[i].length) < 101)) { + keys += "," + keyset[i]; + i++; + } + return keys; + } + // Returns the template-specified singleton card (or secondary varient) after: + // 1) Erasing all inferior duplicates + // 2) Repairing damaged titles and keys + // 3) Constructing a new singleton card if it doesn't exist + function getSingletonCard(allowConstruction, templateCard, secondaryCard) { + let singletonCard = null; + const excessCards = []; + for (const card of storyCards) { + O.s(card); + if (singletonCard === null) { + if ((card.title === templateCard.title) || (card.keys === templateCard.keys)) { + // The first potentially valid singleton card candidate to be found + singletonCard = card; + } + } else if (card.title === templateCard.title) { + if (card.keys === templateCard.keys) { + excessCards.push(singletonCard); + singletonCard = card; + } else { + eraseInferiorDuplicate(); + } + } else if (card.keys === templateCard.keys) { + eraseInferiorDuplicate(); + } + function eraseInferiorDuplicate() { + if ((singletonCard.title === templateCard.title) && (singletonCard.keys === templateCard.keys)) { + excessCards.push(card); + } else { + excessCards.push(singletonCard); + singletonCard = card; + } + return; + } + } + if (singletonCard === null) { + if (secondaryCard) { + // Fallback to a secondary card template + singletonCard = getSingletonCard(false, secondaryCard); + } + // No singleton card candidate exists + if (allowConstruction && (singletonCard === null)) { + // Construct a new singleton card from the given template + singletonCard = constructCard(templateCard); + } + } else { + if (singletonCard.title !== templateCard.title) { + // Repair any damage to the singleton card's title + singletonCard.title = templateCard.title; + } else if (singletonCard.keys !== templateCard.keys) { + // Repair any damage to the singleton card's keys + singletonCard.keys = templateCard.keys; + } + for (const card of excessCards) { + // Erase all excess singleton card candidates + eraseCard(card); + } + if (secondaryCard) { + // A secondary card match cannot be allowed to persist + eraseCard(getSingletonCard(false, secondaryCard)); + } + } + return singletonCard; + } + // Erases the given story card + function eraseCard(badCard) { + if (badCard === null) { + return false; + } + badCard.title = "%@%"; + for (const [index, card] of storyCards.entries()) { + if (card.title === "%@%") { + removeStoryCard(index); + return true; + } + } + return false; + } + // Constructs a new story card from a standardized story card template object + // {type: "", title: "", keys: "", entry: "", description: ""} + // Returns a reference to the newly constructed card + function constructCard(templateCard, insertionIndex = 0) { + addStoryCard("%@%"); + for (const [index, card] of storyCards.entries()) { + if (card.title !== "%@%") { + continue; + } + card.type = templateCard.type; + card.title = templateCard.title; + card.keys = templateCard.keys; + card.entry = templateCard.entry; + card.description = templateCard.description; + if (index !== insertionIndex) { + // Remove from the current position and reinsert at the desired index + storyCards.splice(index, 1); + storyCards.splice(insertionIndex, 0, card); + } + return O.s(card); + } + return {}; + } + function newCardIndex() { + return +AC.config.pinConfigureCard; + } + function getIntendedCard(targetCard) { + Internal.getUsedTitles(true); + const titleKey = targetCard.trim().replace(/\s+/g, " ").toLowerCase(); + const autoCard = Internal.getCard(card => (card.entry + .toLowerCase() + .startsWith("{title: " + titleKey + "}") + )); + if (autoCard !== null) { + return [autoCard, true, titleKey]; + } + return [Internal.getCard(card => ((card.title + .replace(/\s+/g, " ") + .toLowerCase() + ) === titleKey)), false, titleKey]; + } + function doPlayerCommands(input) { + let result = ""; + for (const command of ( + (function() { + if (/^\n> [\s\S]*? says? "[\s\S]*?"\n$/.test(input)) { + return input.replace(/\s*"\n$/, ""); + } else { + return input.trimEnd(); + } + })().split(/(?=\/\s*A\s*C)/i) + )) { + const prefixPattern = /^\/\s*A\s*C/i; + if (!prefixPattern.test(command)) { + continue; + } + const [requestTitle, requestDetails, requestEntry] = (command + .replace(/(?:{\s*)|(?:\s*})/g, "") + .replace(prefixPattern, "") + .replace(/(?:^\s*\/*\s*)|(?:\s*\/*\s*$)/g, "") + .split("/") + .map(requestArg => requestArg.trim()) + .filter(requestArg => (requestArg !== "")) + ); + if (!requestTitle) { + // Request with no args + AC.generation.cooldown = 0; + result += "/AC -> Success!\n\n"; + logEvent("/AC"); + } else { + const request = {title: requestTitle.replace(/\s*[\.\?!:]+$/, "")}; + const redo = (function() { + const redoPattern = /^(?:redo|retry|rewrite|remake)[\s\.\?!:,;"'—\)\]]+\s*/i; + if (redoPattern.test(request.title)) { + request.title = request.title.replace(redoPattern, ""); + if (/^(?:all|every)(?:\s|\.|\?|!|:|,|;|"|'|—|\)|\]|$)/i.test(request.title)) { + return []; + } else { + return true; + } + } else { + return false; + } + })(); + if (Array.isArray(redo)) { + // Redo all auto cards + Internal.getUsedTitles(true); + const titleMatchPattern = /^{title: ([\s\S]*?)}/; + redo.push(...Internal.getCard(card => ( + titleMatchPattern.test(card.entry) + && /{updates: (?:true|false), limit: \d+}/.test(card.description) + ), true)); + let count = 0; + for (const card of redo) { + const titleMatch = card.entry.match(titleMatchPattern); + if (titleMatch && Internal.redoCard(O.f({title: titleMatch[1]}), true, "")) { + count++; + } + } + const parsed = "/AC redo all"; + result += parsed + " -> "; + if (count === 0) { + result += "There were no valid auto-cards to redo"; + } else { + result += "Success!"; + if (1 < count) { + result += " Proceed to redo " + count + " cards"; + } + } + logEvent(parsed); + } else if (!requestDetails) { + // Request with only title + submitRequest(""); + } else if (!requestEntry || redo) { + // Request with title and details + request.entryPromptDetails = requestDetails; + submitRequest(" / {" + requestDetails + "}"); + } else { + // Request with title, details, and entry + request.entryPromptDetails = requestDetails; + request.entryStart = requestEntry; + submitRequest(" / {" + requestDetails + "} / {" + requestEntry + "}"); + } + result += "\n\n"; + function submitRequest(extra) { + O.f(request); + const [type, success] = (function() { + if (redo) { + return [" redo", Internal.redoCard(request, true, "")]; + } else { + Internal.getUsedTitles(true); + return ["", Internal.generateCard(request)]; + } + })(); + const left = "/AC" + type + " {"; + const right = "}" + extra; + if (success) { + const parsed = left + AC.generation.pending[AC.generation.pending.length - 1].title + right; + result += parsed + " -> Success!"; + logEvent(parsed); + } else { + const parsed = left + request.title + right; + result += parsed + " -> \"" + request.title + "\" is invalid or unavailable"; + logEvent(parsed); + } + return; + } + } + if (isPendingGeneration() || isAwaitingGeneration() || isPendingCompression()) { + if (AC.config.doAC) { + AC.signal.outputReplacement = ""; + } else { + AC.signal.forceToggle = true; + AC.signal.outputReplacement = ">>> please select \"continue\" (0%) <<<"; + } + } else if (AC.generation.cooldown === 0) { + if (0 < AC.database.titles.candidates.length) { + if (AC.config.doAC) { + AC.signal.outputReplacement = ""; + } else { + AC.signal.forceToggle = true; + AC.signal.outputReplacement = ">>> please select \"continue\" (0%) <<<"; + } + } else if (AC.config.doAC) { + result = result.trimEnd() + "\n"; + AC.signal.outputReplacement = "\n"; + } else { + AC.signal.forceToggle = true; + AC.signal.outputReplacement = ">>> Auto-Cards has been enabled! <<<"; + } + } else { + result = result.trimEnd() + "\n"; + AC.signal.outputReplacement = "\n"; + } + } + return getPrecedingNewlines() + result; + } + function advanceChronometer() { + const currentTurn = getTurn(); + if (Math.abs(history.length - currentTurn) < 2) { + // The two measures are within ±1, thus history hasn't been truncated yet + AC.chronometer.step = !(history.length < currentTurn); + } else { + // history has been truncated, fallback to a (slightly) worse step detection technique + AC.chronometer.step = (AC.chronometer.turn < currentTurn); + } + AC.chronometer.turn = currentTurn; + return; + } + function concludeEmergency() { + promoteAmnesia(); + endTurn(); + AC.message.pending = []; + AC.message.previous = getStateMessage(); + return; + } + function concludeOutputBlock(templateCard) { + if (AC.config.deleteAllAutoCards !== null) { + // A config-initiated event to delete all previously generated story cards is in progress + if (AC.config.deleteAllAutoCards) { + // Request in-game confirmation from the player before proceeding + AC.config.deleteAllAutoCards = false; + CODOMAIN.initialize(getPrecedingNewlines() + ">>> please submit the message \"CONFIRM DELETE\" using a Do, Say, or Story action to permanently delete all previously generated story cards <<<\n\n"); + } else { + // Check for player confirmation + const previousAction = readPastAction(0); + if (isDoSayStory(previousAction.type) && /CONFIRM\s*DELETE/i.test(previousAction.text)) { + let successMessage = "Confirmation Success: "; + const numCardsErased = Internal.eraseAllAutoCards(); + if (numCardsErased === 0) { + successMessage += "However, there were no previously generated story cards to delete!"; + } else { + successMessage += numCardsErased + " generated story card"; + if (numCardsErased === 1) { + successMessage += " was"; + } else { + successMessage += "s were"; + } + successMessage += " deleted"; + } + notify(successMessage); + } else { + notify("Confirmation Failure: No story cards were deleted"); + } + AC.config.deleteAllAutoCards = null; + CODOMAIN.initialize("\n"); + } + } else if (AC.signal.outputReplacement !== "") { + const output = AC.signal.outputReplacement.trim(); + if (output === "") { + CODOMAIN.initialize("\n"); + } else { + CODOMAIN.initialize(getPrecedingNewlines() + output + "\n\n"); + } + } + if (templateCard) { + // Auto-Cards was enabled or disabled during the previous onContext hook + // Construct the replacement control card onOutput + banTitle(templateCard.title); + getSingletonCard(true, templateCard); + AC.signal.swapControlCards = false; + } + endTurn(); + if (AC.config.LSIv2 === null) { + postMessages(); + } + return; + } + function endTurn() { + AC.database.titles.used = []; + AC.signal.outputReplacement = ""; + [AC.database.titles.pendingBans, AC.database.titles.pendingUnbans].map(pending => decrementAll(pending)); + if (0 < AC.signal.overrideBans) { + AC.signal.overrideBans--; + } + function decrementAll(pendingArray) { + if (pendingArray.length === 0) { + return; + } + for (let i = pendingArray.length - 1; 0 <= i; i--) { + if (0 < pendingArray[i][1]) { + pendingArray[i][1]--; + } else { + pendingArray.splice(i, 1); + } + } + return; + } + return; + } + // Example usage: notify("Message text goes here"); + function notify(message) { + if (typeof message === "string") { + AC.message.pending.push(message); + logEvent(message); + } else if (Array.isArray(message)) { + message.forEach(element => notify(element)); + } else if (message instanceof Set) { + notify([...message]); + } else { + notify(message.toString()); + } + return; + } + function logEvent(message, uncounted) { + if (uncounted) { + log("Auto-Cards event: " + message); + } else { + log("Auto-Cards event #" + (function() { + try { + AC.message.event++; + return AC.message.event; + } catch { + return 0; + } + })() + ": " + message.replace(/"/g, "'")); + } + return; + } + // Provide the story card object which you wish to log info within as the first argument + // All remaining arguments represent anything you wish to log + function logToCard(logCard, ...args) { + logEvent(args.map(arg => { + if ((typeof arg === "object") && (arg !== null)) { + return JSON.stringify(arg); + } else { + return String(arg); + } + }).join(", "), true); + if (logCard === null) { + return; + } + let desc = logCard.description.trim(); + const turnDelimiter = Words.delimiter + "\nAction #" + getTurn() + ":\n"; + let header = turnDelimiter; + if (!desc.startsWith(turnDelimiter)) { + desc = turnDelimiter + desc; + } + const scopesTable = [ + ["input", "Input Modifier"], + ["context", "Context Modifier"], + ["output", "Output Modifier"], + [null, "Shared Library"], + [undefined, "External API"], + [Symbol("default"), "Unknown Scope"] + ]; + const callingScope = (function() { + const pair = scopesTable.find(([condition]) => (condition === HOOK)); + if (pair) { + return pair[1]; + } else { + return scopesTable[scopesTable.length - 1][1]; + } + })(); + const hookDelimiterLeft = callingScope + " @ "; + if (desc.startsWith(turnDelimiter + hookDelimiterLeft)) { + const hookDelimiterOld = desc.match(new RegExp(( + "^" + turnDelimiter + "(" + hookDelimiterLeft + "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z:\n)" + ).replaceAll("\n", "\\n"))); + if (hookDelimiterOld) { + header += hookDelimiterOld[1]; + } else { + const hookDelimiter = getNewHookDelimiter(); + desc = desc.replace(hookDelimiterLeft, hookDelimiter); + header += hookDelimiter; + } + } else { + if ((new RegExp("^" + turnDelimiter.replaceAll("\n", "\\n") + "(" + (scopesTable + .map(pair => pair[1]) + .filter(scope => (scope !== callingScope)) + .join("|") + ) + ") @ ")).test(desc)) { + desc = desc.replace(turnDelimiter, turnDelimiter + "—————————\n"); + } + const hookDelimiter = getNewHookDelimiter(); + desc = desc.replace(turnDelimiter, turnDelimiter + hookDelimiter); + header += hookDelimiter; + } + const logDelimiter = (function() { + let logDelimiter = "Log #"; + if (desc.startsWith(header + logDelimiter)) { + desc = desc.replace(header, header + "———\n"); + const logCounter = desc.match(/Log #(\d+)/); + if (logCounter) { + logDelimiter += (parseInt(logCounter[1], 10) + 1).toString(); + } + } else { + logDelimiter += "0"; + } + return logDelimiter + ": "; + })(); + logCard.description = limitString(desc.replace(header, header + logDelimiter + args.map(arg => { + if ((typeof arg === "object") && (arg !== null)) { + return stringifyObject(arg); + } else { + return String(arg); + } + }).join(",\n") + "\n").trim(), 999999); + // The upper limit is actually closer to 3985621, but I think 1 million is reasonable enough as-is + function getNewHookDelimiter() { + return hookDelimiterLeft + (new Date().toISOString()) + ":\n"; + } + return; + } + // Makes nested objects not look like cancer within interface cards + function stringifyObject(obj) { + const seen = new WeakSet(); + // Each indentation is 4 spaces + return JSON.stringify(obj, (_key, value) => { + if ((typeof value === "object") && (value !== null)) { + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + } + switch(typeof value) { + case "function": { + return "[Function]"; } + case "undefined": { + return "[Undefined]"; } + case "symbol": { + return "[Symbol]"; } + default: { + return value; } + } + }, 4); + } + // Implement state.message toasts without interfering with the operation of other possible scripts + function postMessages() { + const preMessage = getStateMessage(); + if ((preMessage === AC.message.previous) && (AC.message.pending.length !== 0)) { + // No other scripts are attempting to update state.message during this turn + // One or more pending Auto-Cards messages exist + if (!AC.message.suppress) { + // Message suppression is off + let newMessage = "Auto-Cards:\n"; + if (AC.message.pending.length === 1) { + newMessage += AC.message.pending[0]; + } else { + newMessage += AC.message.pending.map( + (messageLine, index) => ("#" + (index + 1) + ": " + messageLine) + ).join("\n"); + } + if (preMessage === newMessage) { + // Introduce a minor variation to facilitate repetition of the previous message toast + newMessage = newMessage.replace("Auto-Cards:\n", "Auto-Cards: \n"); + } + state.message = newMessage; + } + // Clear the pending messages queue after posting or suppressing messages + AC.message.pending = []; + } + AC.message.previous = getStateMessage(); + return; + } + function getStateMessage() { + return state.message ?? ""; + } + function getPrecedingNewlines() { + const previousAction = readPastAction(0); + if (isDoSay(previousAction.type)) { + return ""; + } else if (previousAction.text.endsWith("\n")) { + if (previousAction.text.endsWith("\n\n")) { + return ""; + } else { + return "\n"; + } + } else { + return "\n\n"; + } + } + // Call with lookBack 0 to read the most recent action in history (or n many actions back) + function readPastAction(lookBack) { + const action = (function() { + if (Array.isArray(history)) { + return (history[(function() { + const index = history.length - 1 - Math.abs(lookBack); + if (index < 0) { + return 0; + } else { + return index; + } + })()]); + } else { + return O.f({}); + } + })(); + return O.f({ + text: action?.text ?? (action?.rawText ?? ""), + type: action?.type ?? "unknown" + }); + } + // Forget ongoing card generation/compression after passing or postponing completion over many consecutive turns + // Also decrement AC.chronometer.postpone regardless of retries or erases + function promoteAmnesia() { + // Decrement AC.chronometer.postpone in all cases + if (0 < AC.chronometer.postpone) { + AC.chronometer.postpone--; + } + if (!AC.chronometer.step) { + // Skip known retry/erase turns + return; + } + if (AC.chronometer.amnesia++ < boundInteger(16, (2 * AC.config.addCardCooldown), 64)) { + return; + } + AC.generation.cooldown = validateCooldown(underQuarterInteger(AC.config.addCardCooldown)); + forgetStuff(); + AC.chronometer.amnesia = 0; + return; + } + function forgetStuff() { + AC.generation.completed = 0; + AC.generation.permitted = 34; + AC.generation.workpiece = O.f({}); + // AC.generation.pending is not forgotten + resetCompressionProperties(); + return; + } + function resetCompressionProperties() { + AC.compression.completed = 0; + AC.compression.titleKey = ""; + AC.compression.vanityTitle = ""; + AC.compression.responseEstimate = 1400; + AC.compression.lastConstructIndex = -1; + AC.compression.oldMemoryBank = []; + AC.compression.newMemoryBank = []; + return; + } + function underQuarterInteger(someNumber) { + return Math.floor(someNumber / 4); + } + function getTurn() { + if (Number.isInteger(info?.actionCount)) { + // "But Leah, surely info.actionCount will never be negative?" + // You have no idea what nightmares I've seen... + return Math.abs(info.actionCount); + } else { + return 0; + } + } + // Constructs a JSON representation of various properties/settings pulled from raw text + // Used to parse the "Configure Auto-Cards" and "Edit to enable Auto-Cards" control card entries + function extractSettings(settingsText) { + const settings = {}; + // Lowercase everything + // Remove all non-alphanumeric characters (aside from ":" and ">") + // Split into an array of strings delimited by the ">" character + const settingLines = settingsText.toLowerCase().replace(/[^a-z0-9:>]+/g, "").split(">"); + for (const settingLine of settingLines) { + // Each setting line is preceded by ">" and bisected by ":" + const settingKeyValue = settingLine.split(":"); + if ((settingKeyValue.length !== 2) || settings.hasOwnProperty(settingKeyValue[0])) { + // The bisection failed or this setting line's key already exists + continue; + } + // Parse boolean and integer setting values + if (Words.falses.includes(settingKeyValue[1])) { + // This setting line's value is false + settings[settingKeyValue[0]] = false; + } else if (Words.trues.includes(settingKeyValue[1])) { + // This setting line's value is true + settings[settingKeyValue[0]] = true; + } else if (/^\d+$/.test(settingKeyValue[1])) { + // This setting line's value is an integer + // Negative integers are parsed as being positive (because "-" characters were removed) + settings[settingKeyValue[0]] = parseInt(settingKeyValue[1], 10); + } + } + // Return the settings object for later analysis + return settings; + } + // Ensure the given singleton card is pinned near the top of the player's list of story cards + function pinAndSortCards(pinnedCard) { + if (!storyCards || (storyCards.length < 2)) { + return; + } + storyCards.sort((cardA, cardB) => { + return readDate(cardB) - readDate(cardA); + }); + if (!AC.config.pinConfigureCard) { + return; + } + const index = storyCards.indexOf(pinnedCard); + if (0 < index) { + storyCards.splice(index, 1); + storyCards.unshift(pinnedCard); + } + function readDate(card) { + if (card && card.updatedAt) { + const timestamp = Date.parse(card.updatedAt); + if (!isNaN(timestamp)) { + return timestamp; + } + } + return 0; + } + return; + } + function see(arr) { + return String.fromCharCode(...arr.map(n => Math.sqrt(n / 33))); + } + function formatTitle(title) { + title = title.trim(); + const failureCase = O.f({newTitle: "", newKey: ""}); + if (short()) { + // This is an abundantly called function, return as early as possible to ensure superior performance + return failureCase; + } + title = (title + // Begone! + .replace(/[ā€“ć€‚ļ¼Ÿļ¼Ā“ā€œā€ŲŸŲŒĀ«Ā»ĀæĀ”ā€žā€œā€¦Ā§ļ¼Œć€\*_~><\(\)\[\]{}#"`:!—;\.\?,\s\\]/g, " ") + .replace(/[ā€˜ā€™]/g, "'").replace(/\s+'/g, " ") + // Remove the words "I", "I'm", "I'd", "I'll", and "I've" + .replace(/(?<=^|\s)(?:I|I'm|I'd|I'll|I've)(?=\s|$)/gi, "") + // Remove "'s" only if not followed by a letter + .replace(/'s(?![a-zA-Z])/g, "") + // Replace "s'" with "s" only if preceded but not followed by a letter + .replace(/(?<=[a-zA-Z])s'(?![a-zA-Z])/g, "s") + // Remove apostrophes not between letters (preserve contractions like "don't") + .replace(/(? word.replace(".", "")).join("|") + ")(?=\\s|-|\\/|$)", "gi"); + title = (title + // Capitalize the first letter of each word + .replace(/(?<=^|\s|-|\/)(?:\p{L})/gu, word => word.toUpperCase()) + // Lowercase minor words properly + .replace(/(?<=^|\s|-|\/)(?:\p{L}+)(?=\s|-|\/|$)/gu, word => { + const lowerWord = word.toLowerCase(); + if (Words.minor.includes(lowerWord)) { + return lowerWord; + } else { + return word; + } + }) + // Remove interior honorifics/abbreviations + .replace(honorAbbrevsKiller, "") + .trim() + ); + if (short()) { + return failureCase; + } + let titleWords = title.split(" "); + while ((2 < title.length) && (98 < title.length) && (1 < titleWords.length)) { + titleWords.pop(); + title = titleWords.join(" ").trim(); + const unboundedLength = title.length; + title = enforceBoundaryCondition(title); + if (unboundedLength !== title.length) { + titleWords = title.split(" "); + } + } + if (isUsedOrBanned(title) || isNamed(title)) { + return failureCase; + } + // Procedurally generated story card trigger keywords exclude certain words and patterns which are otherwise permitted in titles + let key = title; + const peerage = new Set(Words.peerage); + if (titleWords.some(word => ((word === "the") || peerage.has(word.toLowerCase())))) { + if (titleWords.length < 2) { + return failureCase; + } + key = enforceBoundaryCondition( + titleWords.filter(word => !peerage.has(word.toLowerCase())).join(" ") + ); + if (key.includes(" the ")) { + key = enforceBoundaryCondition(key.split(" the ")[0]); + } + if (isUsedOrBanned(key)) { + return failureCase; + } + } + function short() { + return (title.length < 3); + } + function enforceBoundaryCondition(str) { + while (leadingMinorWordsKiller.test(str)) { + str = str.replace(/^\S+\s+/, ""); + } + while (trailingMinorWordsKiller.test(str)) { + str = str.replace(/\s+\S+$/, ""); + } + return str; + } + return O.f({newTitle: title, newKey: key}); + } + // I really hate english grammar + function checkPlurals(title, predicate) { + function check(t) { return ((t.length < 3) || (100 < t.length) || predicate(t)); } + const t = title.toLowerCase(); + if (check(t)) { return true; } + // s>p : singular -> plural : p>s: plural -> singular + switch(t[t.length - 1]) { + // p>s : s -> _ : Birds -> Bird + case "s": if (check(t.slice(0, -1))) { return true; } + case "x": + // s>p : s, x, z -> ses, xes, zes : Mantis -> Mantises + case "z": if (check(t + "es")) { return true; } + break; + // s>p : o -> oes, os : Gecko -> Geckoes, Geckos + case "o": if (check(t + "es") || check(t + "s")) { return true; } + break; + // p>s : i -> us : Cacti -> Cactus + case "i": if (check(t.slice(0, -1) + "us")) { return true; } + // s>p : i, y -> ies : Kitty -> Kitties + case "y": if (check(t.slice(0, -1) + "ies")) { return true; } + break; + // s>p : f -> ves : Wolf -> Wolves + case "f": if (check(t.slice(0, -1) + "ves")) { return true; } + // s>p : !(s, x, z, i, y) -> +s : Turtle -> Turtles + default: if (check(t + "s")) { return true; } + break; + } switch(t.slice(-2)) { + // p>s : es -> _ : Foxes -> Fox + case "es": if (check(t.slice(0, -2))) { return true; } else if ( + (t.endsWith("ies") && ( + // p>s : ies -> y : Bunnies -> Bunny + check(t.slice(0, -3) + "y") + // p>s : ies -> i : Ravies -> Ravi + || check(t.slice(0, -2)) + // p>s : es -> is : Crises -> Crisis + )) || check(t.slice(0, -2) + "is")) { return true; } + break; + // s>p : us -> i : Cactus -> Cacti + case "us": if (check(t.slice(0, -2) + "i")) { return true; } + break; + // s>p : is -> es : Thesis -> Theses + case "is": if (check(t.slice(0, -2) + "es")) { return true; } + break; + // s>p : fe -> ves : Knife -> Knives + case "fe": if (check(t.slice(0, -2) + "ves")) { return true; } + break; + case "sh": + // s>p : sh, ch -> shes, ches : Fish -> Fishes + case "ch": if (check(t + "es")) { return true; } + break; + } return false; + } + function isUsedOrBanned(title) { + function isUsed(lowerTitle) { + if (used.size === 0) { + const usedTitles = Internal.getUsedTitles(); + for (let i = 0; i < usedTitles.length; i++) { + used.add(usedTitles[i].toLowerCase()); + } + if (used.size === 0) { + // Add a placeholder so compute isn't wasted on additional checks during this hook + used.add("%@%"); + } + } + return used.has(lowerTitle); + } + return checkPlurals(title, t => (isUsed(t) || isBanned(t))); + } + function isBanned(lowerTitle, getUsedIsExternal) { + if (bans.size === 0) { + // In order to save space, implicit bans aren't listed within the UI + const controlVariants = getControlVariants(); + const dataVariants = getDataVariants(); + const bansToAdd = [...lowArr([ + ...Internal.getBannedTitles(), + controlVariants.enable.title.replace("\n", ""), + controlVariants.enable.keys, + controlVariants.configure.title.replace("\n", ""), + controlVariants.configure.keys, + dataVariants.debug.title, + dataVariants.debug.keys, + dataVariants.critical.title, + dataVariants.critical.keys, + ...Object.values(Words.reserved) + ]), ...(function() { + if (shouldProceed() || getUsedIsExternal) { + // These proper nouns are way too common to waste card generations on; they already exist within the AI training data so this would be pointless + return [...Words.entities, ...Words.undesirables.map(undesirable => see(undesirable))]; + } else { + return []; + } + })()]; + for (let i = 0; i < bansToAdd.length; i++) { + bans.add(bansToAdd[i]); + } + } + return bans.has(lowerTitle); + } + function isNamed(title, returnSurname) { + const peerage = new Set(Words.peerage); + const minorWords = new Set(Words.minor); + if ((forenames.size === 0) || (surnames.size === 0)) { + const usedTitles = Internal.getUsedTitles(); + for (let i = 0; i < usedTitles.length; i++) { + const usedTitleWords = divideTitle(usedTitles[i]); + if ( + (usedTitleWords.length === 2) + && (2 < usedTitleWords[0].length) + && (2 < usedTitleWords[1].length) + ) { + forenames.add(usedTitleWords[0]); + surnames.add(usedTitleWords[1]); + } else if ( + (usedTitleWords.length === 1) + && (2 < usedTitleWords[0].length) + ) { + forenames.add(usedTitleWords[0]); + } + } + if (forenames.size === 0) { + forenames.add("%@%"); + } + if (surnames.size === 0) { + surnames.add("%@%"); + } + } + const titleWords = divideTitle(title); + if ( + returnSurname + && (titleWords.length === 2) + && (3 < titleWords[0].length) + && (3 < titleWords[1].length) + && forenames.has(titleWords[0]) + && surnames.has(titleWords[1]) + ) { + return (title + .split(" ") + .find(casedTitleWord => (casedTitleWord.toLowerCase() === titleWords[1])) + ); + } else if ( + (titleWords.length === 2) + && (2 < titleWords[0].length) + && (2 < titleWords[1].length) + && forenames.has(titleWords[0]) + ) { + return true; + } else if ( + (titleWords.length === 1) + && (2 < titleWords[0].length) + && (forenames.has(titleWords[0]) || surnames.has(titleWords[0])) + ) { + return true; + } + function divideTitle(undividedTitle) { + const titleWords = undividedTitle.toLowerCase().split(" "); + if (titleWords.some(word => minorWords.has(word))) { + return []; + } else { + return titleWords.filter(word => !peerage.has(word)); + } + } + return false; + } + function shouldProceed() { + return (AC.config.doAC && !AC.signal.emergencyHalt && (AC.chronometer.postpone < 1)); + } + function isDoSayStory(type) { + return (isDoSay(type) || (type === "story")); + } + function isDoSay(type) { + return ((type === "do") || (type === "say")); + } + function permitOutput() { + return ((AC.config.deleteAllAutoCards === null) && (AC.signal.outputReplacement === "")); + } + function isAwaitingGeneration() { + return (0 < AC.generation.pending.length); + } + function isPendingGeneration() { + return notEmptyObj(AC.generation.workpiece); + } + function isPendingCompression() { + return (AC.compression.titleKey !== ""); + } + function notEmptyObj(obj) { + return (obj && (0 < Object.keys(obj).length)); + } + function clearTransientTitles() { + AC.database.titles.used = []; + [used, forenames, surnames].forEach(nameset => nameset.clear()); + return; + } + function banTitle(title, isFinalAssignment) { + title = limitString(title.replace(/\s+/g, " ").trim(), 100); + const lowerTitle = title.toLowerCase(); + if (bans.size !== 0) { + bans.add(lowerTitle); + } + if (!lowArr(Internal.getBannedTitles()).includes(lowerTitle)) { + AC.database.titles.banned.unshift(title); + if (isFinalAssignment) { + return; + } + AC.database.titles.pendingBans.unshift([title, 3]); + const index = AC.database.titles.pendingUnbans.findIndex(pair => (pair[0].toLowerCase() === lowerTitle)); + if (index !== -1) { + AC.database.titles.pendingUnbans.splice(index, 1); + } + } + return; + } + function unbanTitle(title) { + title = title.replace(/\s+/g, " ").trim(); + const lowerTitle = title.toLowerCase(); + if (used.size !== 0) { + bans.delete(lowerTitle); + } + let index = lowArr(Internal.getBannedTitles()).indexOf(lowerTitle); + if (index !== -1) { + AC.database.titles.banned.splice(index, 1); + AC.database.titles.pendingUnbans.unshift([title, 3]); + index = AC.database.titles.pendingBans.findIndex(pair => (pair[0].toLowerCase() === lowerTitle)); + if (index !== -1) { + AC.database.titles.pendingBans.splice(index, 1); + } + } + return; + } + function lowArr(arr) { + return arr.map(str => str.toLowerCase()); + } + function getControlVariants() { + return O.f({ + configure: O.f({ + title: "Configure \nAuto-Cards", + keys: "Edit the entry above to adjust your story card automation settings", + }), + enable: O.f({ + title: "Edit to enable \nAuto-Cards", + keys: "Edit the entry above to enable story card automation", + }), + }); + } + function getDataVariants() { + return O.f({ + debug: O.f({ + title: "Debug Data", + keys: "You may view the debug state in the notes section below", + }), + critical: O.f({ + title: "Critical Data", + keys: "Never modify or delete this story card", + }), + }); + } + // Prepare to export the codomain + const codomain = CODOMAIN.read(); + const [stopPackaged, lastCall] = (function() { + // Tbh I don't know why I even bothered going through the trouble of implementing "stop" within LSIv2 + switch(HOOK) { + case "context": { + const haltStatus = []; + if (Array.isArray(codomain)) { + O.f(codomain); + haltStatus.push(true, codomain[1]); + } else { + haltStatus.push(false, STOP); + } + if ((AC.config.LSIv2 !== false) && (haltStatus[1] === true)) { + // AutoCards will return [text, (stop === true)] onContext + // The onOutput lifecycle hook will not be executed during this turn + concludeEmergency(); + } + return haltStatus; } + case "output": { + // AC.config.LSIv2 being either true or null implies (lastCall === true) + return [null, AC.config.LSIv2 ?? true]; } + default: { + return [null, null]; } + } + })(); + // Repackage AC to propagate its state forward in time + if (state.LSIv2) { + // Facilitates recursive calls of AutoCards + // The Auto-Cards external API is accessible through the LSIv2 scope + state.LSIv2 = AC; + } else { + const memoryOverflow = (38000 < (JSON.stringify(state).length + JSON.stringify(AC).length)); + if (memoryOverflow) { + // Memory overflow is imminent + const dataVariants = getDataVariants(); + if (lastCall) { + unbanTitle(dataVariants.debug.title); + banTitle(dataVariants.critical.title); + } + setData(dataVariants.critical, dataVariants.debug); + if (state.AutoCards) { + // Decouple state for safety + delete state.AutoCards; + } + } else { + if (lastCall) { + const dataVariants = getDataVariants(); + unbanTitle(dataVariants.critical.title); + if (AC.config.showDebugData) { + // Update the debug data card + banTitle(dataVariants.debug.title); + setData(dataVariants.debug, dataVariants.critical); + } else { + // There should be no data card + unbanTitle(dataVariants.debug.title); + if (data === null) { + data = getSingletonCard(false, O.f({...dataVariants.debug}), O.f({...dataVariants.critical})); + } + eraseCard(data); + data = null; + } + } else if (AC.config.showDebugData && (HOOK === undefined)) { + const dataVariants = getDataVariants(); + setData(dataVariants.debug, dataVariants.critical); + } + // Save a backup image to state + state.AutoCards = AC; + } + function setData(primaryVariant, secondaryVariant) { + const dataCardTemplate = O.f({ + type: AC.config.defaultCardType, + title: primaryVariant.title, + keys: primaryVariant.keys, + entry: (function() { + const mutualEntry = ( + "If you encounter an Auto-Cards bug or otherwise wish to help me improve this script by sharing your configs and game data, please send me the notes text found below. You may ping me @LewdLeah through the official AI Dungeon Discord server. Please ensure the content you share is appropriate for the server, otherwise DM me instead. 😌" + ); + if (memoryOverflow) { + return ( + "Seeing this means Auto-Cards detected an imminent memory overflow event. But fear not! As an emergency fallback, the full state of Auto-Cards' data has been serialized and written to the notes section below. This text will be deserialized during each lifecycle hook, therefore it's absolutely imperative that you avoid editing this story card!" + ) + (function() { + if (AC.config.showDebugData) { + return "\n\n" + mutualEntry; + } else { + return ""; + } + })(); + } else { + return ( + "This story card displays the full serialized state of Auto-Cards. To remove this card, simply set the \"log debug data\" setting to false within your \"Configure\" card. " + ) + mutualEntry; + } + })(), + description: JSON.stringify(AC) + }); + if (data === null) { + data = getSingletonCard(true, dataCardTemplate, O.f({...secondaryVariant})); + } + for (const propertyName of ["title", "keys", "entry", "description"]) { + if (data[propertyName] !== dataCardTemplate[propertyName]) { + data[propertyName] = dataCardTemplate[propertyName]; + } + } + const index = storyCards.indexOf(data); + if ((index !== -1) && (index !== (storyCards.length - 1))) { + // Ensure the data card is always at the bottom of the story cards list + storyCards.splice(index, 1); + storyCards.push(data); + } + return; + } + } + // This is the only return point within the parent scope of AutoCards + if (stopPackaged === false) { + return [codomain, STOP]; + } else { + return codomain; + } +} AutoCards(null); function isolateLSIv2(code, log, text, stop) { const console = Object.freeze({log}); try { eval(code); return [null, text, stop]; } catch (error) { return [error, text, stop]; } } diff --git a/Output.js b/Output.js index 4f5c7e7..a1d6a8f 100644 --- a/Output.js +++ b/Output.js @@ -1,5 +1,8 @@ const modifier = (text) => { - if (state.show == null) return { text } + if (state.show == null) { + text = AutoCards("output", text); + return { text } + } var character = getCharacter() var possessiveName = character == null ? null : getPossessiveName(character.name) @@ -87,12 +90,72 @@ const modifier = (text) => { break } break + case "setupAlly": + switch (state.setupAllyStep) { + case 0: + text += `***ALLY CREATION***\nWould you like to use a preset ally? (y/n/q to quit)\n` + break + case 1: + text += `What is the ally's name? This must be a unique name that has no duplicates in the current encounter. Typing the name of an existing ally will modify that ally's properties. Type q to quit.\n` + break + case 2: + text += `${!state.newAlly ? "Ally name already exists. You are now modifying the existing ally " + state.tempAlly.name + ". " : ""}What is the ally's health? This can be any positive integer or a dice roll (ie. 3d6+5). Type q to quit.${!state.newAlly ? " Type default to leave as current value " + state.tempAlly.health : ""}\n` + break + case 3: + text += `What is the ally's armor class (AC)? This can be any positive integer with 10 being easy and 20 being incredibly difficult. It can also be a dice roll (ie. 2d4+5). Type q to quit.${!state.newAlly ? " Type default to leave as current value " + state.tempAlly.ac : ""}\n` + break + case 4: + text += `What is the ally's hit modifier? This affects how accurate their attacks are. This can be any integer. 0 is normal accuracy. Type q to quit.${!state.newAlly ? " Type default to leave as current value " + state.tempAlly.hitModifier : ""}\n` + break + case 5: + text += `What is the ally's damage? This can be any positive integer or a dice roll (ie. 2d6+5). The dice roll is calculated at the time of each attack. Type q to quit.${!state.newAlly ? " Type default to leave as current value " + state.tempAlly.damage : ""}\n` + break + case 6: + text += `What is the ally's initiative? Initiative controls turn order. This can be any positive integer with higher numbers going first in battle. This can also be a dice roll (ie. 1d20+3). Type q to quit.${!state.newAlly ? " Type default to leave as current value " + state.tempAlly.initiative : ""}\n` + break + case 7: + text += `Enter the name of a spell that the ally knows. If it can target this spell at an enemy character, add a dice roll for the damage calculation after it (ie. Ray of Frost3d6+2). Type s to stop entering spells or type q to quit.${!state.newAlly ? " Type e to erase all current spells." : ""}\n` + break + case 8: + text += `Enter the name of another spell that the ally knows. If it can target this spell at an enemy character, add a dice roll for the damage calculation after it (ie. Ray of Frost3d6+2). Type s to stop entering spells or type q to quit.\n` + break + case 100: + text += `What ally preset will you choose?\nHeroes\n1. Fighter\n2. Cleric\n3. Rogue\n4. Ranger\n5. Barbarian\n6. Bard\n7. Druid\n8. Monk\n9. Paladin\n10. Wizard\n11. Sorcerer\n12. Warlock\n13. Artificer` + text += `\n\nHumanoid\n14. Commoner\n15. Bandit\n16. Guard\n17. Cultist\n18. Acolyte\n19. Apprentice\n20. Witch\n21. Buccaneer\n22. Spy\n123. Captain\n24. Bard\n25. Berserker\n26 Priest\n27. Knight\n28. Archer\n29. Warrior\n30. Conjurer\n31. Mage\n32. Assassin\n33. Evoker\n34. Necromancer\n35. Champion\n36. Warlord\n37. Archmage\n38. Archdruid` + text += `\n\nFamiliars\n39. Ape\n40. Badger\n41. Bat\n42. Black Bear\n43. Boar\n44. Brown Bear\n45. Camel\n46. Cat\n47. Constrictor Snake\n48. Crab\n49. Crocodile\n50. Dire Wolf\n51. Draft Horse\n52. Elephant\n53. Elk\n54. Frog\n55. Giant Badger\n56. Giant Crab\n57. Giant Goat\n58. Giant Seahorse\n59. Giant Spider\n60. Giant Weasel\n61. Goat\n62. Hawk\n63. Imp\n64. Lion\n65. Lizard\n66. Mastiff\n67. Mule\n68. Octopus\n69. Owl\n70. Panther\n71. Pony\n72. Pseudodragon\n73. Quasit\n74. Rat\n75. Raven\n76. Reef Shark\n77. Riding Horse\n78. Scorpion\n79. Skeleton\n80. Slaad Tadpole\n81. Sphinx of Wonder\n82. Spider\n83. Sprite\n84. Tiger\n85. Venomous Snake\n86. Warhorse\n87. Weasel\n88. Wolf\n89. Zombie` + text += `\n\nEnter the number or q to quit. If you want to rename the ally, add a space and type the name\n(ie. 25 Thuggish Zombie B)\n` + break + case 500: + var hashtag = `#addally "${state.tempAlly.name}" ${state.tempAlly.health} ${state.tempAlly.ac} ${state.tempAlly.hitModifier} ${state.tempAlly.damage} ${state.tempAlly.initiative}` + for (var spell of state.tempAlly.spells) { + hashtag += ` "${spell}"` + } + + text += `${state.tempAlly.name} has been created.\nType #showallies to show the list of all allies.\nCopy and paste the following hashtag to create another identical ally like this:\n${hashtag}\n***********\n` + break; + case null: + text += `[Ally creation has been aborted!]\n` + break + } + break case "stragedy": text += handleStragedy() break case "stragedyShop": text += handleStragedyShop() break + case "spellShop": + text += handleSpellShop() + break + case "itemShop": + text += handleItemShop() + break + case "lockpicking": + text += handleLockpicking() + break + case "memory": + text += handleMemory() + break case "bio": text += `*** ${possessiveName.toUpperCase()} BIO ***\n` text += `Class: ${character.className}\n` @@ -309,6 +372,20 @@ const modifier = (text) => { } } + text += "******************\n\n" + break + case "showAllies": + text += "*** ALLIES ***\n" + + if (state.allies.length == 0) { + text += "There are no allies present here. Type #encounter to generate a scripted set or #addally to add your own\n" + } else { + var index = 0 + for (var ally of state.allies) { + text += `${++index}. ${toTitleCase(ally.name)} (Health: ${ally.health} AC: ${ally.ac} Initiative: ${ally.initiative})\n` + } + } + text += "******************\n\n" break case "initiative": @@ -440,6 +517,12 @@ const modifier = (text) => { 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. Quotes are not necessary." + text += "\n#takeweapon damage_dice hit_bonus ability weapon_name" + text += "\n Allows a character to manually add a weapon to their inventory that is compatible with the #equip command. It is highly recommended to use #itemstore instead. damage_dice is the dice roll (e.g. 1d12+2) used to calculate the damage of the weapon. hit_bonus is a positive or negative number that modifies how accurate the weapon is. Ability is the base ability that is used in conjunction with the weapon. Typically, melee weapons use strength and ranged weapons use dexterity." + text += "\n#takearmor ac weapon_name" + text += "\n Allows a character to manually add armor to their inventory that is compatible with the #equip command. It is highly recommended to use #itemstore instead. ac is the armor class or how hard the character is to hit. If you have an item that adds to the current armor class, precede the number with a plus sign (e.g. +2)." + text += "\n#equip weapon_or_armor_name" + text += "\n Equips a weapon or armor and automatically changes the character's damage/weapon proficiency or armor class respectively. weapon_or_armor_name must be a weapon or type of armor purchased through #itemshop or added to the character inventory through #takeweapon or #takearmor. Shields should be equipped after equipping armor because shield AC is added to the total." 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 in the name. The words for, with, the, a, and an are ignored." text += "\n#sell (sell_quantity) sell_item (buy_quantity) buy_item" @@ -448,6 +531,10 @@ const modifier = (text) => { 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. Quotes are not necessary." 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. Quotes are not necessary for the item." + text += "\n#itemshop (default|weapons|armor|tools|gear|common|uncommon|rare|very rare|legendary|artifact) (free) (all)" + text += "\n This opens the items shop where characters can spend gold to purchase new equipment. default is a general store with a variety of items and a small chance for magically enhanced loot. The selection is randomized based on the day. Include the argument \"free\" to not require gold to purchase the item. Include the argument \"all\" to list all available items. Otherwise, the list is randomized and a small selection of the item list is presented." + text += "\n#reward (count) (default|weapons|armor|tools|gear|common|uncommon|rare|very rare|legendary|artifact)" + text += "\n Gives the character a random item selected from the given list. count determines how many rewards are drawn (default is 1). The default list has a weighted chance of drawing from any of the lists with increasing rarity." text += "\n#renameitem original_name new_name" text += "\n Renames the item indicated by original_name to the new_name. The quantity remains the same. Quotes are necessary for items with spaces in the name." text += "\n#inventory" @@ -464,6 +551,8 @@ const modifier = (text) => { 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#spellshop (bard|cleric|druid|paladin|ranger|sorcerer|warlock|wizard) (level) (free) (all)" + text += "\n This opens the spell shop where characters can spend gold to purchase new spells. The selection is randomized based on the day and on the character's class and spell level. Full casters, such as bards, clerics, druids, sorcerers, warlocks, and wizards, have spell levels from 0-9. Half casters, such as paladins and rangers, have spell levels from 1-5. Include the argument \"free\" to not require gold to purchase the spell. Include the argument \"all\" to list all available spells for that level. Otherwise, the list is randomized and a selection of lower level spells are included." text += "\n\n--Combat--" text += "\n#setupenemy" @@ -475,7 +564,9 @@ const modifier = (text) => { text += "\n#addenemy name health ac hitModifier damage initiative spells" text += "\n Adds the specified enemy to the list of enemies. health, ac, hitModifier, damage, and initiative can be numbers or dice rolls such as 3d6+5. Type the name in quotes if the name contains a space. The rest of the parameters can be a list of spells. Each spell must be typed in quotes if it has a space. If the spell does damage, write the name and damage roll in the following format: \"Ray of Frost5d10\"" text += "\n#removeenemy name or index" - text += "\n Removes the enemy as specified by the name or index. To delete multiple enemies, type the numbers with spaces or commas between them. This is safer than calling #removenote multiple times because the numbers shift as enemies are deleted. Quotes are not necessary." + text += "\n Removes the enemy as specified by the name or index. To delete multiple enemies, type the numbers with spaces or commas between them. This is safer than calling #removeenemy multiple times because the numbers shift as enemies are deleted. Quotes are not necessary." + text += "\n#clearenemies" + text += "\n Removes all enemies." text += "\n#initiative" text += "\n Assigns initiative to all characters and enemies. This begins combat." text += "\n#turn" @@ -487,6 +578,18 @@ const modifier = (text) => { text += "\n#flee (difficulty_class or automatic|effortless|easy|medium|hard|impossible)" text += "\n Attempt to flee from combat. If the difficulty is not specified, the default difficulty will be used instead." + text += "\n\n--Allies--" + text += "\n#setupally" + text += "\nFollow prompts to create an ally from a template or completely from scratch. It will be added to the existing encounter if there is one already specified." + text += "\n#showallies" + text += "\n Shows the list of current allies." + text += "\n#addally name health ac hitModifier damage initiative spells" + text += "\n Adds the specified ally to the list of allies. health, ac, hitModifier, damage, and initiative can be numbers or dice rolls such as 3d6+5. Type the name in quotes if the name contains a space. The rest of the parameters can be a list of spells. Each spell must be typed in quotes if it has a space. If the spell does damage, write the name and damage roll in the following format: \"Ray of Frost5d10\"" + text += "\n#removeally name or index" + text += "\n Removes the ally as specified by the name or index. To delete multiple allies, type the numbers with spaces or commas between them. This is safer than calling #removeally multiple times because the numbers shift as allies are deleted. Quotes are not necessary." + text += "\n#clearallies" + text += "\n Removes all allies." + text += "\n\n--Locations--" text += "\n#createlocation [(x) (y) or (here|far) or (distance)] location_name" text += "\n Creates a location at the given coordinates. The coordinates must be integers. If the coordinates are not provided, they are randomized within a range of 10 units from the party's current location. You can also use \"here\" to indicate that the location is at party's coordinates. \"far\" indicates that the coordinates will be randomly generated 50-100 units away. You may also just specify a distance. Multiple locations may exist at the same coordinates. A story card is created for the location. Quotes are not necessary." @@ -523,6 +626,14 @@ const modifier = (text) => { text += "\n#stragedy (automatic|effortless|easy|medium|hard|impossible)" text += "\n Initiates a game of Stragedy, a card game played against an AI opponent. Specifying a difficulty (default is easy) grants the opponent a corresponding deck. Please see the game manual on github for rules, tactics, and a complete tutorial: github.com/raeleus/Hashtag-DnD/" + text += "\n\n--Lockpicking Minigame--" + text += "\n#lockpick (automatic|effortless|easy|medium|hard|impossible)" + text += "\n Initiates a lockpicking minigame similar to Mastermind where you have to guess the correct combination with a limited number of tries in order to defeat a lock. Specifying a difficulty (default is easy) sets the number of combinations and tries accordingly. Please see the game manual on github for rules, tactics, and a complete tutorial: github.com/raeleus/Hashtag-DnD/" + + text += "\n\n--Memory Minigame--" + text += "\n#memory (automatic|effortless|easy|medium|hard|impossible)" + text += "\n Initiates a memory minigame where you have to flip cards one at a time until you make a matching pair. You only have a set number of turns to finish the game. Specifying a difficulty (default is easy) sets the number of cards and maximum turns accordingly." + text += "\n\n--Danger Zone--" text += "\n#reset" text += "\n Removes all characters, locations, and notes. Changes all settings to their defaults. Use with caution!" @@ -537,6 +648,8 @@ const modifier = (text) => { } state.show = null + + text = AutoCards("output", text); return { text } } @@ -563,6 +676,115 @@ function mapReplace(map, x, y, character) { return map } +function handleMemory() { + let text = " " + switch (state.memoryTurn) { + case "game": + text = `**Memory** +Select a card from below by typing its number or type q to quit: + +` + + let counter = 0 + for (let y = 0; y < state.memoryHeight; y++) { + for (let x = 0; x < state.memoryWidth; x++) { + counter++ + const solved = state.memorySolved[counter - 1] != null + + let cardText = "" + if (solved) { + cardText = state.memoryCards[counter - 1] + } else { + cardText = counter + } + + text += `${cardText.toString().length == 1 ? " " : ""}${cardText} ` + } + text += "\n" + } + + text += ` + +It is turn ${state.memoryTurns} of ${state.memoryMaxTurns}` + + break + case "win": + text = `You won the game in ${state.memoryTurns} out of ${state.memoryMaxTurns} turns!\n` + + let counter1 = 0 + for (let y = 0; y < state.memoryHeight; y++) { + for (let x = 0; x < state.memoryWidth; x++) { + counter1++ + text += `${state.memoryCards[counter1 - 1]} ` + } + text += "\n" + } + break + case "lose": + text = `After ${state.memoryMaxTurns} turns, you were unable to complete the game.\n` + + let counter2 = 0 + for (let y = 0; y < state.memoryHeight; y++) { + for (let x = 0; x < state.memoryWidth; x++) { + counter2++ + text += `${state.memoryCards[counter2 - 1]} ` + } + text += "\n" + } + break + case "forfeit": + text = "You decided to give up on finishing the game.\n" + + let counter3 = 0 + for (let y = 0; y < state.memoryHeight; y++) { + for (let x = 0; x < state.memoryWidth; x++) { + counter3++ + text += `${state.memoryCards[counter3 - 1]} ` + } + text += "\n" + } + break + } + + return text +} + +function handleLockpicking() { + var text = " " + switch (state.lockpickingTurn) { + case "intro": + text = `**Mastermind** +Welcome to Mastermind! A minigame to stand in for lockpicking, hacking, and other tasks of skill. +Please see the game manual on github for rules, tactics, and a complete tutorial: +github.com/raeleus/Hashtag-DnD/ +You must solve the ${state.lockpickingSlots} color combination within ${state.lockpickingGuessMax} guesses! +Colors: r (red), y (yellow), w (white), g (green), o (orange), b (blue) +Enter your first guess below by typing the letter for each color. Type "q" to quit: +` + break + case "game": + if (state.lockpickingInput.length != state.lockpickingSlots) text = `\nAn incorrect number of colors was input. Only type ${state.lockpickingSlots} letters!\n` + else text = ` +Correct: ${state.lockpickingCorrect}. Wrong position: ${state.lockpickingWrongPlace}. ${state.lockpickingGuessMax - state.lockpickingGuesses} ${state.lockpickingGuessMax - state.lockpickingGuesses == 1 ? "try" : "tries"} left. +Colors: r (red), y (yellow), w (white), g (green), o (orange), b (blue) +Enter your guess below by typing the letter for each color. Type "q" to quit: +` + break + case "win": + text = `You solved the combination with ${state.lockpickingGuesses} ${state.lockpickingGuesses == 1 ? "guess" : "guesses"}!` + break + case "lose": + text = `After ${state.lockpickingGuesses} ${state.lockpickingGuesses == 1 ? "guess" : "guesses"}, you were unable to solve the combination... +The combination was ${state.lockpickingCombination}` + break + case "forfeit": + text = "You decided to give up on solving the combination." + break + } + + return text +} + function handleStragedy() { var character = getCharacter() var haveWord = character.name == "You" ? "have" : "has" @@ -690,6 +912,501 @@ Type d to draw a card. ${!hasJokerOnBattlefield ? "Type r to retire. " : ""}Type return text } +function itemShopPushDeal(items, name) { + let quantity = 1 + let storyCardName = name + name = itemShopConvertGenericName(name) + + state.itemShopDeals.push({ + className: state.itemShopCategoryName, + name: name, + storyCardName: storyCardName, + price: 0, + quantity: quantity, + bought: false + }) +} + +var itemShopSeed + +function itemShopSelectItems(items, numberOfItems) { + if (numberOfItems == null) numberOfItems = 1 + + itemShopSeed += 100 + if (state.itemShopAll) { + for (let i = 0; i < items.length; i++) { + itemShopPushDeal(items, items[i]) + } + return + } + + shuffle(items, itemShopSeed) + for (let i = 0; i < numberOfItems; i++) { + itemShopPushDeal(items, items[i]) + } +} + +function handleItemShop() { + var character = getCharacter() + var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold") + var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity + var text = " " + itemShopSeed = state.day + + if (state.itemShopDeals == null || state.itemShopClearDeals) state.itemShopDeals = [] + + if (findItemShopDeals(state.itemShopCategoryName).length == 0) switch(state.itemShopCategoryName) { + case "weapons": + itemShopSelectItems(weaponsList, 5) + break + case "armor": + itemShopSelectItems(armorList, 5) + break + case "tools": + itemShopSelectItems(toolsList, 5) + break + case "gear": + itemShopSelectItems(gearList, 10) + break + case "common": + itemShopSelectItems(commonList, 5) + break + case "uncommon": + itemShopSelectItems(uncommonList, 5) + break + case "rare": + itemShopSelectItems(rareList, 5) + break + case "phenomenal": + itemShopSelectItems(phenomenalList, 5) + break + case "legendary": + itemShopSelectItems(legendaryList, 200000) + break + case "artifact": + itemShopSelectItems(artifactList, 5) + break + case "default": + let shuffled = [...weaponsList].sort(() => 0.5 - Math.random()); + let list = shuffled.slice(0, 3) + shuffled = [...armorList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 3)) + shuffled = [...toolsList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 3)) + shuffled = [...gearList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 5)) + + let rand = Math.random() + if (rand <= .50) { + shuffled = [...commonList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } else if (rand <= .70) { + shuffled = [...uncommonList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } else if (rand <= .86) { + shuffled = [...rareList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } else if (rand <= .94) { + shuffled = [...phenomenalList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } else if (rand <= .98) { + shuffled = [...legendaryList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } else { + shuffled = [...artifactList].sort(() => 0.5 - Math.random()); + list = list.concat(shuffled.slice(0, 1)) + } + + itemShopSelectItems(list, 15) + break + } + + switch (state.itemShopStep) { + case 0: + text = `**Welcome to the Item Shop** +-${toTitleCase(state.itemShopCategoryName)}- +Deals change every day!` + break + case 1: + text = "Item purchased!" + break + case 2: + text = "You do not have enough gold!" + } + + switch (state.itemShopStep) { + case 0: + case 1: + case 2: + text += ` +Select a number from the list below to purchase an item: + +` + let deals = findItemShopDeals(state.itemShopCategoryName, false) + deals = deals.filter(item => !item.bought) + + if (deals.length == 0) text += "There are no items left for sale!\n" + for (var i = 0; i < deals.length; i++) { + let itemStoryCard = findItemCard(deals[i].name, deals[i].storyCardName) + let description = itemStoryCard == null ? "\nERROR: Story card is missing. You may import the latest story cards from the Hashtag DnD Github: https://github.com/raeleus/Hashtag-DnD/blob/master/story-cards.json\n\n" : `:\n${itemStoryCard.entry}\n\n` + deals[i].price = itemStoryCard == null ? 0 : parseInt(itemStoryCard.description.split(",")[0]) + if (itemStoryCard != null && itemStoryCard.type == "weapon") { + deals[i].damage = itemStoryCard.description.split(",")[1] + deals[i].toHitBonus = itemStoryCard.description.split(",")[2] + deals[i].ability = itemStoryCard.description.split(",")[3] + } else if (itemStoryCard != null && itemStoryCard.type == "armor") { + deals[i].ac = itemStoryCard.description.split(",")[1] + } + text += `${i + 1}. ${deals[i].name}${state.itemShopIsFree ? "" : ` for ${numberWithCommas(deals[i].price)} gold`}` + text += description + } + + text += ` +${state.itemShopIsFree ? "These items come at no cost!" : `You have ${numberWithCommas(gold)} gold`} +Enter the number or q to quit: +` + break + case 500: + text = "Thank you for shopping at the Item Shop!" + break + } + + return text +} + +function spellShopPushDeal(items, name, price) { + state.spellShopDeals.push({ + className: state.spellShopClassName, + level: state.spellShopLevel, + name: name, + price: price, + bought: false + }) +} + +var spellShopSeed + +function spellShopSelectSpells(spells, price, numberOfSpells) { + if (numberOfSpells == null) numberOfSpells = 1 + + spellShopSeed += 100 + index = Math.floor(getRandom(spellShopSeed) * spells.length) + if (state.spellShopAll) { + for (const spell of spells) { + spellShopPushDeal(spells, spell, price) + } + return + } + + shuffle(spells, spellShopSeed) + for (let i = 0; i < numberOfSpells; i++) { + spellShopPushDeal(spells, spells[i], price) + } +} + +function handleSpellShop() { + var character = getCharacter() + var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold") + var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity + var text = " " + spellShopSeed = state.day + + if (state.spellShopDeals == null || state.spellShopClearDeals) state.spellShopDeals = [] + + if (findSpellShopDeals(state.spellShopClassName, state.spellShopLevel).length == 0) switch(state.spellShopClassName) { + case "bard": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Foresight", "Power Word Heal", "Power Word Kill", "Prismatic Wall", "True Polymorph"], 50000) + if (state.spellShopAll) break + case 8: + spellShopSelectSpells(["Antipathy/Sympathy", "Befuddlement", "Dominate Monster", "Glibness", "Mind Blank", "Power Word Stun"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Etherealness", "Forcecage", "Mirage Arcane", "Mordenkainen's Magnificent Mansion", "Mordenkainen's Sword", "Power Word Fortify", "Prismatic Spray", "Project Image", "Regenerate", "Resurrection", "Symbol", "Teleport"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Eyebite", "Find the Path", "Guards and Wards", "Heroes' Feast", "Mass Suggestion", "Otto's Irresistible Dance", "Programmed Illusion", "True Seeing"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Animate Objects", "Awaken", "Dominate Person", "Dream", "Dream", "Geas", "Greater Restoration", "Hold Monster", "Legend Lore", "Mass Cure Wounds", "Mislead", "Modify Memory", "Planar Binding", "Raise Dead", "Rary's Telepathic Bond", "Scrying", "Seeming", "Synaptic Static", "Teleportation Circle", "Yolande's Regal Presence"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Charm Monster", "Compulsion", "Confusion", "Dimension Door", "Fount of Moonlight", "Freedom of Movement", "Greater Invisibility", "Hallucinatory Terrain", "Locate Creature", "Phantasmal Killer", "Polymorph"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Bestow Curse", "Clairvoyance", "Dispel Magic", "Fear", "Feign Death", "Glyph of Warding", "Hypnotic Pattern", "Leomund's Tiny Hut", "Major Image", "Mass Healing Word", "Nondetection", "Plant Growth", "Sending", "Slow", "Speak with Dead", "Speak with Plants", "Stinking Cloud", "Tongues"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Aid", "Animal Messenger", "Blindness/Deafness", "Calm Emotions", "Cloud of Daggers", "Crown of Madness", "Detect Thoughts", "Enhance Ability", "Enlarge/Reduce", "Enthrall", "Heat Metal", "Hold Person", "Invisibility", "Knock", "Lesser Restoration", "Locate Animals or Plants", "Locate Object", "Magic Mouth", "Mirror Image", "Phantasmal Force", "See Invisibility", "Shater", "Silence", "Suggestion", "Zone of Truth"], 500, state.spellShopLevel == 2 ? 5 : 1) + case 1: + spellShopSelectSpells(["Animal Friendship", "Bane", "Charm Person", "Color Spray", "Command", "Comprehend Languages", "Cure Wounds", "Detect Magic", "Disguise Self", "Dissonant Whispers", "Faerie Fire", "Feather Fall", "Healing Word", "Heroism", "Identify", "Illusory Script", "Longstrider", "Silent Image", "Sleep", "Speak with Animals", "Tasha's Hideous Laughter", "Thunderwave", "Unseen Servant"], 250, state.spellShopLevel == 1 ? 5 : 1) + case 0: + spellShopSelectSpells(["Blade Ward", "Dancing Lights", "Friends", "Light", "Mage Hand", "Mending", "Message", "Minor Illusion", "Prestidigitation", "Starry Wisp", "Thunderclap", "True Strike", "Vicious Mockery"], 50, state.spellShopLevel == 0 ? 3 : 1) + } + break + case "cleric": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Astral Projection", "Gate", "Mass Heal", "Power Word Heal", "True Resurrection"], 50000) + if (state.spellShopAll) break + case 8: + spellShopSelectSpells(["Antimagic Field", "Control Weather", "Earthquake", "Holy Aura", "Sunburst"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Conjure Celestial", "Divine Word", "Etherealness", "Fire Storm", "Plane Shift", "Power Word Fortify", "Regenerate", "Resurrection", "Symbol"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Blade Barrier", "Create Undead", "Find the Path", "Forbiddance", "Harm", "Heroes' Feast", "Planar Ally", "Sunbeam", "True Seeing", "Word of Recall"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Circle of Power", "Commune", "Contagion", "Dispel Evil and Good", "Flame Strike", "Geas", "Greater Restoration", "Hallow", "Insect Plague", "Legend Lore", "Mass Cure Wounds", "Planar Binding", "Raise Dead", "Scrying", "Summon Celestial"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Aura of Life", "Aura of Purity", "Banishment", "Control Weather", "Death Ward", "Divination", "Freedom of Movement", "Guardian of Faith", "Locate Creature", "Stone Shape"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Animate Dead", "Aura of Vitality", "Beacon of Hope", "Bestow Curse", "Clairvoyance", "Create Food and Water", "Daylight", "Dispel Magic", "Feign Death", "Glyph of Warding", "Magic Circle", "Mass Healing Ward", "Meld into Stone", "Protection from Energy", "Remove Curse", "Revivify", "Sending", "Speak with Dead", "Spirit Guardians", "Tongues", "Water Walk"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Aid", "Augury", "Blindness/Deafness", "Calm Emotions", "Continual Flame", "Enhance Ability", "Find Traps", "Gentle Repose", "Hold Person", "Lesser Restoration", "Locate Object", "Prayer of Healing", "Protection from Poison", "Silence", "Spiritual Weapon", "Warding Bond", "Zone of Truth"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Bane", "Bless", "Command", "Create or Destroy Water", "Cure Wounds", "Detect Evil and Good", "Detect Magic", "Detect Poison and Disease", "Guiding Bolt", "Healing Word", "Inflict Wounds", "Protection from Evil and Good", "Purify Food and Drink", "Sanctuary", "Shield of Faith"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + case 0: + spellShopSelectSpells(["Guidance", "Light", "Mending", "Resistance", "Sacred Flame", "Spare the Dying", "Thaumaturgy", "Toll the Dead", "Word of Radiance"], 50, state.spellShopLevel == 0 ? 3 : 1) + break + } + break + case "druid": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Foresight", "Shapechange", "Storm of Vengeance", "True Resurrection"], 50000) + if (state.spellShopAll) break + case 8: + spellShopSelectSpells(["Animal Shapes", "Antipathy/Sympathy", "Befuddlement", "Control Weather", "Earthquake", "Incendiary Cloud", "Sunburst", "Tsunami"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Fire Storm", "Mirage Arcane", "Plane Shift", "Regenerate", "Reverse Gravity", "Symbol"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Conjure Fey", "Find the Path", "Flesh to Stone", "Heal", "Heroes' Feast", "Move Earth", "Sunbeam", "Transport via Plants", "Wall of Thorns", "Wind Walk"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Antilife Shell", "Awaken", "Commune with Nature", "Cone of Cold", "Conjure Elemental", "Contagion", "Geas", "Greater Restoration", "Insect Plague", "Mass Cure Wounds", "Planar Binding", "Reincarnate", "Scrying", "Tree Stride", "Wall of Stone"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Blight", "Charm Monster", "Confusion", "Conjure Woodland Beings", "Control Water", "Divination", "Dominate Beast", "Fire Shield", "Fount of Moonlight", "Freedom of Movement", "Giant Insect", "Grasping Vine", "Hallucinatory Terrain", "Ice Storm", "Locate Creature", "Polymorph", "Stone Shape", "Stoneskin", "Summon Elemental", "Wall of Fire"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Aura of Vitality", "Call Lightning", "Conjure Animals", "Daylight", "Dispel Magic", "Elemental Weapon", "Feign Death", "Meld into Stone", "Plant Growth", "Protection from Energy", "Revivify", "Sleet Storm", "Speak with Plants", "Summon Fey", "Water Breathing", "Water Walk", "Wind Wall"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Aid", "Animal Messenger", "Augury", "Barkskin", "Beast Sense", "Continual Flame", "Darkvision", "Enhance Ability", "Enlarge/Reduce", "Find Traps", "Flame Blade", "Flaming Sphere", "Gust of Wind", "Heat Metal", "Hold Person", "Lesser Restoration", "Locate Animals or Plants", "Locate Object", "Moonbeam", "Pass without Trace", "Protection from Poison", "Spike Growth", "Summon Beast"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Animal Friendship", "Charm Person", "Create or Destroy Water", "Cure Wounds", "Detect Magic", "Detect Poison and Disease", "Entangle", "Faerie Fire", "Fog Cloud", "Goodberry", "Healing Word", "Ice Knife", "Jump", "Longstrider", "Protection from Evil and Good", "Purify Food and Drink", "Speak with Animals", "Thunderwave"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + case 0: + spellShopSelectSpells(["Druidcraft", "Elementalism", "Guidance", "Mending", "Message", "Poison Spray", "Produce Flame", "Resistance", "Shillelagh", "Spare the Dying", "Starry Wisp", "Thorn Whip", "Thunderclap"], 50, state.spellShopLevel == 0 ? 3 : 1) + if (state.spellShopAll) break + break + } + break + case "paladin": + switch(state.spellShopLevel) { + case 5: + spellShopSelectSpells(["Banishing Smite", "Circle of Power", "Destructive Wave", "Dispel Evil and Good", "Geas", "Greater Restoration", "Raise Dead", "Summon Celestial"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Aura of Life", "Aura of Purity", "Banishment", "Death Ward", "Locate Creature", "Staggering Smite"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Aura of Vitality", "Blinding Smite", "Create Food and Water", "Crusader's Mantle", "Daylight", "Dispel Magic", "Elemental Weapon", "Magic Circle", "Remove Curse", "Revivify"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Aid", "Find Steed", "Gentle Repose", "Lesser Restoration", "Locate Object", "Magic Weapon", "Prayer of Healing", "Protection from Poison", "Shining Smite", "Warding Bond", "Zone of Truth"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Bless", "Command", "Compelled Duel", "Cure Wounds", "Detect Evil and Good", "Detect Magic", "Detect Poison and Disease", "Divine Favor", "Divine Smite", "Heroism", "Protection from Evil and Good", "Purify Food and Drink", "Searing Smite", "Shield of Faith", "Thunderous Smite", "Wrathful Smite"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + break + } + break + case "ranger": + switch(state.spellShopLevel) { + case 5: + spellShopSelectSpells(["Commune with Nature", "Conjure Volley", "Greater Restoration", "Steel Wind Strike", "Swift Quiver", "Tree Stride"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Conjure Woodland Beings", "Dominate Beast", "Freedom of Movement", "Grasping Vine", "Locate Creature", "Stoneskin", "Summon Elemental"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Conjure Animals", "Conjure Barrage", "Daylight", "Dispel Magic", "Elemental Weapon", "Lightning Arrow", "Meld into Stone", "Nondetection", "Plant Growth", "Protection from Energy", "Revivify", "Speak with Plants", "Summon Fey", "Water Breathing", "Water Walk", "Wind Wall"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Aid", "Animal Messenger", "Barkskin", "Beast Sense", "Cordon of Arrows", "Darkvision", "Enhance Ability", "Find Traps", "Gust of Wind", "Lesser Restoration", "Locate Animals or Plants", "Locate Object", "Magic Weapon", "Pass without Trace", "Protection from Poison", "Silence", "Spike Growth", "Summon Beast"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Alarm", "Animal Friendship", "Cure Wounds", "Detect Magic", "Detect Poison and Disease", "Ensnaring Strike", "Entangle", "Fog Cloud", "Goodberry", "Hail of Thorns", "Hunter's Mark", "Jump", "Longstrider", "Speak with Animals"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + break + } + break + case "sorcerer": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Gate", "Meteor Swarm", "Power Word Kill", "Time Stop", "Wish"], 50000) + if (state.spellShopAll) break + case 8: + spellShopSelectSpells(["Demiplane", "Dominate Monster", "Earthquake", "Incendiary Cloud", "Power Word Stun", "Sunburst"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Delayed Blast Fireball", "Etherealness", "Finger of Death", "Fire Storm", "Plane Shift", "Prismatic Spray", "Reverse Gravity", "Teleport"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Arcane Gate", "Chain Lightning", "Circle of Death", "Disintegrate", "Eyebite", "Flesh to Stone", "Globe of Invulnerability", "Mass Suggestion", "Move Earth", "Otiluke's Freezing Sphere", "Sunbeam", "True Seeing"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Animate Objects", "Bigby's Hand", "Cloudkill", "Cone of Cold", "Creation", "Dominate Person", "Hold Monster", "Insect Plague", "Seeming", "Synaptic Static", "Telekinesis", "Teleportation Circle", "Wall of Stone"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Banishment", "Blight", "Charm Monster", "Confusion", "Dimension Door", "Dominate Beast", "Fire Shield", "Greater Invisibility", "Ice Storm", "Polymorph", "Stoneskin", "Vitriolic Sphere", "Wall of Fire"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Blink", "Clairvoyance", "Counterspell", "Daylight", "Dispel Magic", "Fear", "Fireball", "Fly", "Gaseous Form", "Haste", "Hypnotic Pattern", "Lightning Bolt", "Major Image", "Protection from Energy", "Sleet Storm", "Slow", "Stinking Cloud", "Tongues", "Vampiric Touch", "Water Breathing", "Water Walk"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Alter Self", "Arcane Vigor", "Blindness/Deafness", "Blur", "Cloud of Daggers", "Crown of Madness", "Darkness", "Darkvision", "Detect Thoughts", "Dragon's Breath", "Enhance Ability", "Enlarge/Reduce", "Flame Blade", "Flaming Sphere", "Gust of Wind", "Hold Person", "Invisibility", "Knock", "Levitate", "Magic Weapon", "Mind Spike", "Mirror Image", "Misty Step", "Phantasmal Force", "Scorching Ray", "See Invisibility", "Shatter", "Spider Climb", "Suggestion", "Web"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Burning Hands", "Charm Person", "Chromatic Orb", "Color Spray", "Comprehend Languages", "Detect Magic", "Disguise Self", "Expeditious Retreat", "False Life", "Feather Fall", "Fog Cloud", "Grease", "Ice Knife", "Jump", "Mage Armor", "Magic Missile", "Ray of Sickness", "Shield", "Silent Image", "Sleep", "Thunderwave", "Witch Bolt"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + case 0: + spellShopSelectSpells(["Acid Splash", "Blade Ward", "Chill Touch", "Dancing Lights", "Elementalism", "Fire Bolt", "Friends", "Light", "Mage Hand", "Mending", "Message", "Minor Illusion", "Poison Spray", "Prestidigitation", "Ray of Frost", "Shocking Grasp", "Sorcerous Burst", "Thunderclap", "True Strike"], 50, state.spellShopLevel == 0 ? 3 : 1) + if (state.spellShopAll) break + break + } + break + case "warlock": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Astral Projection", "Dominate Person", "Foresight", "Gate", "Geas", "Greater Restoration", "Imprisonment", "Insect Plague", "Modify Memory", "Power Word Kill", "Seeming", "Summon Celestial", "Telekinesis", "True Polymorph", "Weird"], 50000) + case 8: + spellShopSelectSpells(["Befuddlement", "Demiplane", "Dominate Monster", "Glibness", "Power Word Stun"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Dominate Beast", "Etherealness", "Finger of Death", "Fire Shield", "Forcecage", "Greater Invisibility", "Guardian of Faith", "Plane Shift", "Wall of Fire"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Arcane Gate", "Circle of Death", "Create Undead", "Eyebite", "Summon Fiend", "Tasha's Bubbling Cauldron", "True Seeing"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Blink", "Clairvoyance", "Confusion", "Contact Other Plane", "Daylight", "Dream", "Fireball", "Hold Monster", "Hunger of Hadar", "Jallarzi's Storm of Radiance", "Mislead", "Planar Binding", "Plant Growth", "Revivify", "Scrying", "Stinking Cloud", "Summon Aberration", "Synaptic Static", "Teleportation Circle"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Banishment", "Blight", "Charm Monster", "Dimension Door", "Hallucinatory Terrain", "Summon Aberration"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Counterspell", "Dispel Magic", "Fear", "Fly", "Gaseous Form", "Hunger of Hadar", "Hypnotic Pattern", "Magic Circle", "Major Image", "Remove Curse", "Summon Fey", "Summon Undead", "Tongues", "Vampiric Touch"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Cloud of Daggers", "Crown of Madness", "Darkness", "Enthrall", "Hold Person", "Invisibility", "Mind Spike", "Mirror Image", "Misty Step", "Ray of Enfeeblement", "Spider Climb", "Suggestion"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Armor of Agathys", "Arms of Hadar", "Bane", "Charm Person", "Comprehend Languages", "Detect Magic", "Expeditious Retreat", "Hellish Rebuke", "Hex", "Illusory Script", "Protection from Evil and Good", "Speak with Animals", "Tasha's Hideous Laughter", "Unseen Servant", "Witch Bolt"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + case 0: + spellShopSelectSpells(["Blade Ward", "Chill Touch", "Eldritch Blast", "Friends", "Mage Hand", "Mind Sliver", "Minor Illusion", "Poison Spray"], 50, state.spellShopLevel == 0 ? 3 : 1) + if (state.spellShopAll) break + break + } + break + case "wizard": + switch(state.spellShopLevel) { + case 9: + spellShopSelectSpells(["Astral Projection", "Foresight", "Gate", "Imprisonment", "Meteor Swarm", "Power Word Kill", "Prismatic Wall", "Shapechange", "Time Stop", "True Polymorph", "Weird", "Wish"], 50000) + if (state.spellShopAll) break + case 8: + spellShopSelectSpells(["Antimagic Field", "Antipathy/Sympathy", "Befuddlement", "Clone", "Control Weather", "Demiplane", "Dominate Monster", "Incendiary Cloud", "Maze", "Mind Blank", "Power Word Stun", "Sunburst", "Telepathy"], 25000) + if (state.spellShopAll) break + case 7: + spellShopSelectSpells(["Delayed Blast Fireball", "Etherealness", "Finger of Death", "Forcecage", "Mirage Arcane", "Mordenkainen's Magnificent Mansion", "Mordenkainen's Sword", "Plane Shift", "Prismatic Spray", "Project Image", "Reverse Gravity", "Sequester", "Simulacrum", "Symbol", "Teleport"], 20000, state.spellShopLevel == 7 ? 2 : 1) + if (state.spellShopAll) break + case 6: + spellShopSelectSpells(["Arcane Gate", "Chain Lightning", "Circle of Death", "Contingency", "Create Undead", "Disintegrate", "Drawmij's Instant Summons", "Eyebite", "Flesh to Stone", "Globe of Invulnerability", "Guards and Wards", "Magic Jar", "Mass Suggestion", "Move Earth", "Otiluke's Freezing Sphere", "Otto's Irresistible Dance", "Programmed Illusion", "Summon Fiend", "Sunbeam", "Tasha's Bubbling Cauldron", "True Seeing", "Wall of Ice"], 10000, state.spellShopLevel == 6 ? 2 : 1) + if (state.spellShopAll) break + case 5: + spellShopSelectSpells(["Animate Objects", "Bigby's Hand", "Circle of Power", "Cloudkill", "Cone of Cold", "Conjure Elemental", "Contact Other Plane", "Creation", "Dominate Person", "Dream", "Geas", "Hold Monster", "Jallarzi's Storm of Radiance", "Legend Lore", "Mislead", "Modify Memory", "Passwall", "Planar Binding", "Rary's Telepathic Bond", "Scrying", "Seeming", "Steel Wind Strike", "Summon Dragon", "Synaptic Static", "Telekinesis", "Teleportation Circle", "Wall of Force", "Wall of Stone", "Yolande's Regal Presence"], 5000, state.spellShopLevel == 5 ? 3 : 1) + if (state.spellShopAll) break + case 4: + spellShopSelectSpells(["Arcane Eye", "Banishment", "Blight", "Charm Monster", "Confusion", "Conjure Minor Elementals", "Control Water", "Dimension Door", "Divination", "Evard's Black Tentacles", "Fabricate", "Fire Shield", "Greater Invisibility", "Hallucinatory Terrain", "Ice Storm", "Leomund's Secret Chest", "Locate Creature", "Mordenkainen's Faithful Hound", "Mordenkainen's Private Sanctum", "Otiluke's Resilient Sphere", "Phantasmal Killer", "Polymorph", "Stoneskin", "Summon Aberration", "Summon Construct", "Summon Elemental", "Vitriolic Sphere", "Wall of Fire"], 2500, state.spellShopLevel == 4 ? 3 : 1) + if (state.spellShopAll) break + case 3: + spellShopSelectSpells(["Animate Dead", "Bestow Curse", "Blink", "Clairvoyance", "Counterspell", "Dispel Magic", "Fear", "Feign Death", "Fireball", "Fly", "Gaseous Form", "Glyph of Warding", "Haste", "Hypnotic Pattern", "Leomund's Tiny Hut", "Lightning Bolt", "Magic Circle", "Major Image", "Nondetection", "Phantom Steed", "Protection from Energy", "Remove Curse", "Sending", "Sleet Storm", "Slow", "Speak with Dead", "Stinking Cloud", "Summon Fey", "Summon Undead", "Tongues", "Vampiric Touch", "Water Breathing"], 1000, state.spellShopLevel == 3 ? 5 : 1) + if (state.spellShopAll) break + case 2: + spellShopSelectSpells(["Alter Self", "Arcane Lock", "Arcane Vigor", "Augury", "Blindness/Deafness", "Blur", "Cloud of Daggers", "Continual Flame", "Crown of Madness", "Darkness", "Darkvision", "Detect Thoughts", "Dragon's Breath", "Enhance Ability", "Enlarge/Reduce", "Flaming Sphere", "Gentle Repose", "Gust of Wind", "Hold Person", "Invisibility", "Knock", "Levitate", "Locate Object", "Magic Mouth", "Magic Weapon", "Melf's Acid Arrow", "Mind Spike", "Mirror Image", "Misty Step", "Nystul's Magic Aura", "Phantasmal Force", "Ray of Enfeeblement", "Rope Trick", "Scorching Ray", "See Invisibility", "Shatter", "Spider Climb", "Suggestion", "Web"], 500, state.spellShopLevel == 2 ? 5 : 1) + if (state.spellShopAll) break + case 1: + spellShopSelectSpells(["Alarm", "Burning Hands", "Charm Person", "Chromatic Orb", "Color Spray", "Comprehend Languages", "Detect Magic", "Disguise Self", "Expeditious Retreat", "False Life", "Feather Fall", "Find Familiar", "Fog Cloud", "Grease", "Ice Knife","Identify", "Illusory Script", "Jump", "Longstrider", "Mage Armor", "Magic Missile", "Protection from Evil and Good", "Ray of Sickness", "Shield", "Silent Image", "Sleep", "Tasha's Hideous Laughter", "Tenser's Floating Disk", "Thunderwave", "Unseen Servant", "Witch Bolt"], 250, state.spellShopLevel == 1 ? 5 : 1) + if (state.spellShopAll) break + case 0: + spellShopSelectSpells(["Acid Splash", "Blade Ward", "Chill Touch", "Dancing Lights", "Elementalism", "Fire Bolt", "Friends", "Light", "Mage Hand", "Mending", "Message", "Mind Sliver", "Minor Illusion", "Poison Spray", "Prestidigitation", "Ray of Frost", "Shocking Grasp", "Thunderclap", "Toll the Dead", "True Strike"], 50, state.spellShopLevel == 0 ? 3 : 1) + if (state.spellShopAll) break + break + } + break + } + + switch (state.spellShopStep) { + case 0: + text = `**Welcome to the Spell Shop** +Deals change every day!` + break + case 1: + text = "Spell purchased!" + break + case 2: + text = "You do not have enough gold!" + case 3: + text = "You already know that spell!" + } + + switch (state.spellShopStep) { + case 0: + case 1: + case 2: + case 3: + text += ` +Select a number from the list below to purchase a spell: + +` + let deals = findSpellShopDeals(state.spellShopClassName, state.spellShopLevel, false) + if (deals.length == 0) text += "There are no spells left for sale!\n" + for (var i = 0; i < deals.length; i++) { + let spellStoryCard = findSpellCard(deals[i].name) + let description = spellStoryCard == null ? "\nERROR: Story card is missing. You may import the latest story cards from the Hashtag DnD Github: https://github.com/raeleus/Hashtag-DnD/blob/master/story-cards.json\n\n" : `:\n${spellStoryCard.entry}\n\n` + let found = character.spells.find((element) => element == deals[i].name) != undefined + text += `${i + 1}. ${deals[i].name}${state.spellShopIsFree ? "" : ` for ${numberWithCommas(deals[i].price)} gold`}` + if (found) text += " (Already Known)" + text += description + } + + text += ` +${state.spellShopIsFree ? "These spells come at no cost!" : `You have ${numberWithCommas(gold)} gold`} +Enter the number or q to quit: +` + break + case 500: + text = "Thank you for shopping at the Spell Shop!" + break + } + + return text +} + function handleStragedyShop() { var character = getCharacter() var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold") @@ -738,11 +1455,11 @@ Select a number from the list below to purchase a card: ` if (state.cardDeals.length == 0) text += "There are no cards left for sale!\n" for (var i = 0; i < state.cardDeals.length; i++) { - text += `${i + 1}. Stragedy ${state.cardDeals[i]} Card for ${state.cardPrices[i]} gold\n` + text += `${i + 1}. Stragedy ${state.cardDeals[i]} Card for ${numberWithCommas(state.cardPrices[i])} gold\n` } text += ` -You have ${gold} gold +You have ${numberWithCommas(gold)} gold Enter the number or q to quit: ` break diff --git a/README.md b/README.md index 0f4dfd8..cb43d32 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,43 @@ A Scenario script for AI Dungeon Hashtag DnD is a scripted AI Dungeon scenario that gives you a variety of tools to enhance your adventure! Features: -Fully working inventory system
-Health points and a focus on strategic combat.
+Fully working inventory system, loot drops, and item/spell shops
+Hit points, turn-based battles, and a focus on strategic combat
Skill/Ability based gameplay guarantees a challenge at every turn
Advanced dice rolling syntax allows you to set the precise odds
Unlimited party size with each character having their own inventory, stats, and biographies
Multiplayer compatible
Personalized note system that does not take up context space
-Create locations to travel to and view them in a map +Create locations to travel to and view them in a map
+Minigames including Mastermind, Memory, and Stragedy, a fully developed trading card game See the [user guide here](https://github.com/raeleus/Hashtag-DnD/wiki). Watch the [tutorial video](https://youtu.be/E5TYU7rDaBQ). +Hashtag-DnD has another new scenario! Check out the [VTOL-Knights Repository](https://github.com/raeleus/Hashtag-DnD/tree/VTOL-Knights) +This script implements Auto-Cards by LewdLeah. See more details [on github](https://github.com/LewdLeah/Auto-Cards) + +v. 0.8.0 +* Added Auto-Cards by LewdLeah. This feature is disabled by default. Activate it by changing the "Disabled" setting to "false" in the "Configure Auto-Cards" story card. + +v. 0.7.0 +* Added allies which are NPC characters that fight alongside the characters in encounters +* Fixed enemy spells showing the damage dice in the output text. + +v. 0.6.0 +* Added Memory/Matchmaking Game +* Added Item Shop - Make sure to import the latest story cards: https://github.com/raeleus/Hashtag-DnD/blob/master/story-cards.json +* Added commands #takeweapon, #takearmor, and #equip to allow automatic stat changes when using gear +* Added command #reward for random loot dropping +* Added descriptions for all DnD Player's Handbook and Dungeon Master's Guide items to story cards +* Fixed #spellshop not giving properly randomized spells +* Fixed #addcard not giving out numbered cards and instead giving out random ones + +v. 0.5.0 +* Added Mastermind/Lockpicking Game +* Added Spell Shop - Make sure to import the latest story cards: https://github.com/raeleus/Hashtag-DnD/blob/master/story-cards.json +* Added descriptions for all DnD Player's Handbook spells to story cards. + v. 0.4.0 * Added Stragedy Trading Card Game diff --git a/story-cards.json b/story-cards.json index e82d7ca..9daedeb 100644 --- a/story-cards.json +++ b/story-cards.json @@ -1,160 +1,7728 @@ [ { - "keys": "Thunderwave", - "value": "Creates a wave of force that thrusts everything within a short range in front of you away. This can push friend and foe alike, causing them to topple over.", - "type": "spell", - "title": "Thunderwave", - "description": "", + "keys": "Club", + "value": "A crude, light weapon held in one hand. It can slow the movement of an enemy with its bludgeoning damage.", + "type": "weapon", + "title": "Club", + "description": "1,1d4,0,strength", "useForCharacterCreation": false }, { - "keys": "Witch Bolt", - "value": "Creates a sustained arc of lightning between the caster and the target. It causes constant lightning damage, but requires the user to concentrate on the spell. The spell is broken when the caster is injured or loses concentration in any way.", - "type": "spell", - "title": "Witch Bolt", - "description": "", + "keys": "Dagger", + "value": "A light weapon that can be wielded with precision or brute strength. It can be dual-wielded and thrown at short range to cause piercing damage.", + "type": "weapon", + "title": "Dagger", + "description": "2,1d4,0,dexterity", "useForCharacterCreation": false }, { - "keys": "Eldritch Blast", - "value": "Casts a beam of pure energy at a creature, causing magical force damage.", - "type": "spell", - "title": "Eldritch Blast", - "description": "", + "keys": "Greatclub", + "value": "A crude, two-handed weapon that is capable of pushing back enemies with bludgeoning damage.", + "type": "weapon", + "title": "Greatclub", + "description": "1,1d8,0,strength", "useForCharacterCreation": false }, { - "keys": "Minor Illusion", - "value": "Creates an illusion of a sound or image of an object within range. The spell is broken if a physical interaction with the item reveals it to be a hoax.", - "type": "spell", - "title": "Minor Illusion", - "description": "", + "keys": "Handaxe", + "value": "A light weapon that slashes opponents. It can be thrown at short range.", + "type": "weapon", + "title": "Handaxe", + "description": "5,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Javelin", + "value": "A medium-ranged thrown weapon that causes piercing damage which can slow enemies.", + "type": "weapon", + "title": "Javelin", + "description": "1,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Light Hammer", + "value": "A light, bludgeoning weapon that can also be thrown at short range.", + "type": "weapon", + "title": "Light Hammer", + "description": "1,1d4,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Mace", + "value": "A simple melee weapon that can temporarily weaken the attack of the opponent with its bludgeoning damage.", + "type": "weapon", + "title": "Mace", + "description": "5,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Quarterstaff", + "value": "A versatile weapon that can topple opponents with its bludgeoning damage.", + "type": "weapon", + "title": "Quarterstaff", + "description": "1,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Sickle", + "value": "A light, slashing weapon that can be dual-wielded.", + "type": "weapon", + "title": "Sickle", + "description": "1,1d4,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Spear", + "value": "A versatile, piercing weapon that can also be thrown at short range and temporarily weaken the attack of opponents.", + "type": "weapon", + "title": "Spear", + "description": "1,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Dart", + "value": "A ranged weapon that can be wielded with precision or brute strength which gives you advantage on your next attack. Can be thrown a short range.", + "type": "weapon", + "title": "Dart", + "description": "1,1d4,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Light Crossbow", + "value": "A ranged weapon that fires piercing bolts at long range which can slow an opponent. It requires two hands to load and fire.", + "type": "weapon", + "title": "Light Crossbow", + "description": "25,1d8,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Shortbow", + "value": "A ranged weapon that fire piercing arrows at long range which gives you advantage on your next attack. It requires two hands to fire.", + "type": "weapon", + "title": "Shortbow", + "description": "25,1d6,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Sling", + "value": "A ranged, bludgeoning weapon that launches sling bullets at medium range. These slow the opponent", + "type": "weapon", + "title": "Sling", + "description": "1,1d4,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Battleaxe", + "value": "A versatile weapon that causes slashing damage. It's capable of toppling opponents.", + "type": "weapon", + "title": "Battleaxe", + "description": "10,1d10,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Glaive", + "value": "A heavy melee weapon that requires two hands to wield slashing damage. It has long reach and guarantees some damage on its target.", + "type": "weapon", + "title": "Glaive", + "description": "20,1d10,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Flail", + "value": "A capable melee weapon that can bludgeon opponents and temporarily weakens the attacks of opponents.", + "type": "weapon", + "title": "Flail", + "description": "10,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Greataxe", + "value": "A heavy, two-handed weapon that slashes an opponent and damages a nearby enemy for lesser damage.", + "type": "weapon", + "title": "Greataxe", + "description": "30,1d12,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Greatsword", + "value": "A heavy, two-handed weapon that deals slashing damage and guarantees some damage even if it misses.", + "type": "weapon", + "title": "Greatsword", + "description": "50,2d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Halberd", + "value": "A heavy, two-handed weapon with incredible reach that slashes an opponent and damages a nearby enemy for lesser damage.", + "type": "weapon", + "title": "Halberd", + "description": "20,1d10,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Lance", + "value": "A heavy, twon-handed weapon with incredible reach that pierces an opponent and can topple them over.", + "type": "weapon", + "title": "Lance", + "description": "10,1d10,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Longsword", + "value": "A versatile weapon that can temporarily weaken the attack of an opponent with slashing damage.", + "type": "weapon", + "title": "Longsword", + "description": "15,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Maul", + "value": "A heavy, two-handed weapon capable of bludgeoning enemies and toppling them over.", + "type": "weapon", + "title": "Maul", + "description": "10,2d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Morningstar", + "value": "A piercing weapon that can temporarily weaken the attack of an opponent.", + "type": "weapon", + "title": "Morningstar", + "description": "15,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Pike", + "value": "A heavy, two-handed weapon with incredible reach that pierces enemies and pushes them back.", + "type": "weapon", + "title": "Pike", + "description": "5,1d10,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Rapier", + "value": "A weapon that can be wielded with precision or brute strength and pierces enemies to give advantage on further attacks.", + "type": "weapon", + "title": "Rapier", + "description": "25,1d8,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Scimitar", + "value": "A light weapon that can be dual-wielded with precision or brute strength and slashes enemies.", + "type": "weapon", + "title": "Scimitar", + "description": "25,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Shortsword", + "value": "A light weapon that can be wielded with precision or brute strength and pierces enemies to give advantage on further attacks.", + "type": "weapon", + "title": "Shortsword", + "description": "10,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Trident", + "value": "A versatile piercing weapon that can be thrown at short range to topple enemies.", + "type": "weapon", + "title": "Trident", + "description": "5,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Warhammer", + "value": "A versatile bludgeoning weapon that can push enemies.", + "type": "weapon", + "title": "Warhammer", + "description": "15,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "War Pick", + "value": "A versatile piercing weapon that can temporarily weaken an opponents attack.", + "type": "weapon", + "title": "War Pick", + "description": "5,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Whip", + "value": "A slashing weapon with incredible reach that can slow an enemy's approach.", + "type": "weapon", + "title": "Whip", + "description": "2,1d4,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Blowgun", + "value": "An unwieldly ranged weapon that can shoot a dart at a target to give advantage on the next attack.", + "type": "weapon", + "title": "Blowgun", + "description": "10,1,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Hand Crossbow", + "value": "A medium ranged weapon that can pierce enemies to give advantage on the next attack.", + "type": "weapon", + "title": "Hand Crossbow", + "description": "75,1d6,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Heavy Crossbow", + "value": "A two-handed long ranged weapon that can pierce enemies to push them back.", + "type": "weapon", + "title": "Heavy Crossbow", + "description": "50,1d10,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Longbow", + "value": "A two-handed extreme long ranged weapon that can pierce enemies to slow them.", + "type": "weapon", + "title": "Longbow", + "description": "50,1d8,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Musket", + "value": "A powerful, two-handed medium ranged weapon that can slow enemies.", + "type": "weapon", + "title": "Musket", + "description": "500,1d12,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Pistol", + "value": "A powerful, short ranged weapon that can give the attacker advantage on their next strike.", + "type": "weapon", + "title": "Pistol", + "description": "250,1d10,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Padded Armor", + "value": "A basic light armor that is not stealthy.", + "type": "armor", + "title": "Padded Armor", + "description": "5,11+d", + "useForCharacterCreation": false + }, + { + "keys": "Leather Armor", + "value": "A basic light armor.", + "type": "armor", + "title": "Leather Armor", + "description": "10,11+d", + "useForCharacterCreation": false + }, + { + "keys": "Studded Leather Armor", + "value": "An advanced light armor.", + "type": "armor", + "title": "Studded Leather Armor", + "description": "45,12+d", + "useForCharacterCreation": false + }, + { + "keys": "Hide Armor", + "value": "A basic medium armor that slightly restricts dodging.", + "type": "armor", + "title": "Hide Armor", + "description": "10,12+dmax2", + "useForCharacterCreation": false + }, + { + "keys": "Chain Shirt", + "value": "A moderate medium armor that slightly restricts dodging.", + "type": "armor", + "title": "Chain Shirt", + "description": "50,13+dmax2", + "useForCharacterCreation": false + }, + { + "keys": "Scale Mail", + "value": "An advanced medium armor that slightly restricts dodging and is not stealthy.", + "type": "armor", + "title": "Scale Mail", + "description": "50,14+dmax2", + "useForCharacterCreation": false + }, + { + "keys": "Breastplate", + "value": "An advanced medium armor that slightly restricts dodging.", + "type": "armor", + "title": "Breastplate", + "description": "400,14+dmax2", + "useForCharacterCreation": false + }, + { + "keys": "Half Plate Armor", + "value": "An excellent medium armor that slightly restricts dodging.", + "type": "armor", + "title": "Half Plate Armor", + "description": "750,15+dmax2", + "useForCharacterCreation": false + }, + { + "keys": "Ring Mail", + "value": "A basic heavy armor that restricts dodging and is not stealthy.", + "type": "armor", + "title": "Ring Mail", + "description": "30,14", + "useForCharacterCreation": false + }, + { + "keys": "Chain Mail", + "value": "A moderate heavy armor that requires strength to wear, restricts dodging, and is not stealthy.", + "type": "armor", + "title": "Chain Mail", + "description": "75,16", + "useForCharacterCreation": false + }, + { + "keys": "Splint Armor", + "value": "An advanced heavy armor that requires a lot of strengthh to wear, restricts dodging, and is not stealthy.", + "type": "armor", + "title": "Splint Armor", + "description": "200,17", + "useForCharacterCreation": false + }, + { + "keys": "Plate Armor", + "value": "An excellent heavy armor that requires a lot of strength to wear, restricts dodging, and is not stealthy.", + "type": "armor", + "title": "Plate Armor", + "description": "1500,18", "useForCharacterCreation": false }, { "keys": "Shield", - "value": "The shield spell creates an invisible barrier that protects the user from some physical damage.", - "type": "spell", + "value": "Adds additional armor to the wearer.", + "type": "armor", "title": "Shield", + "description": "10,+2", + "useForCharacterCreation": false + }, + { + "keys": "Alchemist's Supplies", + "value": "Can be used to identify a substance or start a fire. Could craft Alchhemist Fire, Component Pouch, Oil, Paper, or Perfume.", + "type": "item", + "title": "Alchemist's Supplies", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Brewer's Supplies", + "value": "Can detect if a drink is poisoned and identify alcohol. It can craft antitoxin.", + "type": "item", + "title": "Brewer's Supplies", + "description": "20", + "useForCharacterCreation": false + }, + { + "keys": "Calligrapher's Supplies", + "value": "Writes text that guards against forgery. It can craft ink and spell scrolls.", + "type": "item", + "title": "Calligrapher's Supplies", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Carpenter's Tools", + "value": "Seal a door or container and craft wooden weapons and equipment: club, greatclub, quarterstaff, barrel, chest, ladder, pole, portable ram, torch.", + "type": "item", + "title": "Carpenter's Tools", + "description": "8", + "useForCharacterCreation": false + }, + { + "keys": "Cartographer's Tools", + "value": "Draft a map of a small area. Used to craft maps.", + "type": "item", + "title": "Cartographer's Tools", + "description": "15", + "useForCharacterCreation": false + }, + { + "keys": "Cobbler's Tools", + "value": "Modify footwear to give the wearer an advantage at acrobatics. Craft a climber's kit.", + "type": "item", + "title": "Cobbler's Tools", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Cook's Utensils", + "value": "Improve food taste or detect poisoned food. Creates rations.", + "type": "item", + "title": "Cook's Utensils", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Glassblower's Tools", + "value": "Determine what was inside a glass object and craft glass bottle, magnifying glass, spyglass, and vial.", + "type": "item", + "title": "Glassblower's Tools", + "description": "30", + "useForCharacterCreation": false + }, + { + "keys": "Jeweler's Tools", + "value": "Determine the value of a gem or craft an arcane/holyh focus.", + "type": "item", + "title": "Jeweler's Tools", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Leatherworker's Tools", + "value": "Add a design to a leather item or craft a sling, whip, leather armors, backpack, bolt case, map/scroll case, parchment, pouch, quiver, waterskin.", + "type": "item", + "title": "Leatherworker's Tools", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Mason's Tools", + "value": "Chisel a symbol or hole into stone. Create a block and tackle.", + "type": "item", + "title": "Mason's Tools", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Painter's Supplies", + "value": "Paint an image of something that has been seen or craft a druidic/holy focus.", + "type": "item", + "title": "Painter's Supplies", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Potter's Tools", + "value": "Determine what was inside a pottery or create a jug/lamp.", + "type": "item", + "title": "Potter's Tools", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Smith's Tools", + "value": "Pry open a door or container. Create a metal melee weapon, medium/heavy armor, ball bearings, bucket, caltrops, chain, crowbar, bullets, grappling hook, iron pot, spikes.", + "type": "item", + "title": "Smith's Tools", + "description": "20", + "useForCharacterCreation": false + }, + { + "keys": "Tinker's Tools", + "value": "Assemble a tiny item made out of scrap or craft a musket, pistol, bell, lantern, flask, hunting trap, lock, manacles, mirror, shovel, signal whistle, tinderbox.", + "type": "item", + "title": "Tinker's Tools", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Weaver's Tools", + "value": "Fix a tear in clothhing or sew a tiny design. Craft padded armor, basket, bedroll, blanket, fine clothes, net, robe, rope, sack, string, tent, traveler's clothes.", + "type": "item", + "title": "Weaver's Tools", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Woodcarver's Tools", + "value": "Carve a pattern into wood or craft a club, greatclub, quarterstaff, ranged weapons, arcane/druidic focuses, arrows, bolts, ink pen, needles.", + "type": "item", + "title": "Woodcarver's Tools", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Disguise Kit", + "value": "Apply makeup or craft a costume.", + "type": "item", + "title": "Disguise Kit", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Forgery Kit", + "value": "Mimic up to 10 words of someone else's handwriting or duplicate a wax seal.", + "type": "item", + "title": "Forgery Kit", + "description": "15", + "useForCharacterCreation": false + }, + { + "keys": "Gaming Set, Dice, Dragonchess, Playing Cards, Three-Dragon Ante", + "value": "Play a game to win or discern if someone is cheating.", + "type": "item", + "title": "Gaming Set", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Herbalism Kit", + "value": "Identify a plant or craft an antitoxin, candle, healer's kit, or a potion of healing.", + "type": "item", + "title": "Herbalism Kit", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Musical Instrument, Bagpipes, Drum, Dulcimer, Flute, Horn, Lute, Lyre, Pan Flute, Shawm, Viol", + "value": "Play a known tune or improvise a song.", + "type": "item", + "title": "Musiical Instrument", + "description": "6", + "useForCharacterCreation": false + }, + { + "keys": "Navigator's Tools", + "value": "Plot a course or determine position by stargazing.", + "type": "item", + "title": "Navigator's Tools", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Poisoner's Kit", + "value": "Detect a poisoned object or craft a basic poison.", + "type": "item", + "title": "Poisoner's Kit", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Thieves' Tools", + "value": "Pick a lock or disarm a trap.", + "type": "item", + "title": "Thieves' Tools", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Acid", + "value": "A vial of acid that can be thrown at an object causing acid damage.", + "type": "item", + "title": "Acid", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Alchemist's Fire", + "value": "Thrown bottle that will cause the target to take fire damage and begin to burn.", + "type": "item", + "title": "Alchemist's Fire", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Ammunition", + "value": "Arrows, bolts, bullets, or needles necessary for ranged weapons.", + "type": "item", + "title": "Ammunition", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Antitoxin", + "value": "Drinking the antitoxin will cure poisoned creatures and avoid becoming poisoned.", + "type": "item", + "title": "Antitoxin", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Arcane Focus", + "value": "A crystal, orb, rod, staff, or wand that is needed to cast magic through.", + "type": "item", + "title": "Arcane Focus", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Backpack", + "value": "A carrying pack that can hold up to 30 pounds.", + "type": "item", + "title": "Backpack", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Ball Bearings", + "value": "Metal balls that can be spread on the floor that can drop creatures prone when they step on them.", + "type": "item", + "title": "Ball Bearings", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Barrel", + "value": "Carries up to 40 gallons of liquid in 4 cubic feet.", + "type": "item", + "title": "Barrel", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Basket", + "value": "Carries up to 40 pounds within 2 cubic feet.", + "type": "item", + "title": "Basket", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Bedroll", + "value": "Allows sleep during extreme cold.", + "type": "item", + "title": "Bedroll", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Bell", + "value": "Ringing the bell produces a sound that can be heard up to 60 feet away.", + "type": "item", + "title": "Bell", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Blanket", + "value": "Helps mitigate extreme cold.", + "type": "item", + "title": "Blanket", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Block and Tackle", + "value": "Allows the user to hoist up to four times the weight they can normally lift.", + "type": "item", + "title": "Block and Tackle", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Book", + "value": "A fiction or non-fiction book. Non-fiction books can be consulted for additional information on a subject.", + "type": "item", + "title": "Book", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Glass Bottle", + "value": "Can contain up to 1.5 pints of liquid.", + "type": "item", + "title": "Glass Bottle", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Bucket", + "value": "Can hold up to half a cubic foot of contents.", + "type": "item", + "title": "Bucket", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Burglar's Pack", + "value": "Contains a backpack, ball bearings, a bell, 10 candles, a crowbar, a hooded lantern, 7 flasks of oil, 5 days of rations, a rope, a tinderbox, and a waterskin.", + "type": "item", + "title": "Burglar's Pack", + "description": "16", + "useForCharacterCreation": false + }, + { + "keys": "Caltrops", + "value": "Can be spread out to cause piercing damage and slow down any creature that steps on it.", + "type": "item", + "title": "Caltrops", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Candle", + "value": "Sheds bright light for 5 feet and dim light for 5 feet past that.", + "type": "item", + "title": "Candle", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Crossbow Bolt Case", + "value": "Carries 20 bolts.", + "type": "item", + "title": "Crossbow Bolt Case", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Map Case", + "value": "Carries 10 sheets of paper or 5 sheets of parchment.", + "type": "item", + "title": "Map Case", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Scroll Case", + "value": "Carries 10 sheets of paper or 5 sheets of parchment.", + "type": "item", + "title": "Scroll Case", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Chain", + "value": "Can be wrapped around a creature to restrain it.", + "type": "item", + "title": "Chain", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Chest", + "value": "Holds up to 12 cubic feet of contents.", + "type": "item", + "title": "Chest", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Climber's Kit", + "value": "When utilized, the user will not fall more than 25 feet when climbing.", + "type": "item", + "title": "Climber's Kit", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Fine Clothes", + "value": "Clothes made of expensive fabrics.", + "type": "item", + "title": "Fine Clothes", + "description": "15", + "useForCharacterCreation": false + }, + { + "keys": "Traveler's Clothes", + "value": "Basic garments designed for travel.", + "type": "item", + "title": "Traveler's Clothes", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Component Pouch", + "value": "Contains all the basic components needed for spells.", + "type": "item", + "title": "Component Pouch", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Costume", + "value": "Gives the wearer the ability to blend in or impersonate an individual.", + "type": "item", + "title": "Costume", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Crowbar", + "value": "Assists with tasks that require strength and leverage.", + "type": "item", + "title": "Crowbar", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Diplomat's Pack", + "value": "Contains chest, fine clothes, ink, 5 ink pens, lamp, 2 maps/scroll scases, 4 flasks of oil, 5 sheets of paper, 5 sheets of parchment, perfume, and tinderbox.", + "type": "item", + "title": "Diplomat's Pack", + "description": "39", + "useForCharacterCreation": false + }, + { + "keys": "Druidic Focus", + "value": "A sprig of mistletoe, a wooden staff, or a yew wand necessary for casting nature spells.", + "type": "item", + "title": "Druidic Focus", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Dungeoneer's Pack", + "value": "Contains backpack, caltrops, crowbar, 2 flasks of oil, 10 days of rations, rope, tinderbox, 10 torches, and a waterskin.", + "type": "item", + "title": "Dungeoneer's Pack", + "description": "12", + "useForCharacterCreation": false + }, + { + "keys": "Entertainer's Pack", + "value": "Contains a backpack, bedroll, bell, bullseye lantern, 3 costumes, mirror, 8 flasks of oil, 9 days of rations, tinderbox, and waterskin.", + "type": "item", + "title": "Entertainer's Pack", + "description": "40", + "useForCharacterCreation": false + }, + { + "keys": "Explorer's Pack", + "value": "Contains backpack, bedroll, 2 flasks of oil, 10 days of rations, rope, tinderbox, 10 torches, and waterskin.", + "type": "item", + "title": "Explorer's Pack", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Flask", + "value": "Holds up to 1 pint of liquid.", + "type": "item", + "title": "Flask", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Grappling Hook", + "value": "Thrown with a range of 50 feet, it can catch on a ledge and allow creatures to climb up it.", + "type": "item", + "title": "Grappling Hook", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Healer's Kit", + "value": "Can be used to stabilize on unconcious creature.", + "type": "item", + "title": "Healer's Kit", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Holy Symbol", + "value": "An amulet, emblem, or reliquary used to cast divine magic.", + "type": "item", + "title": "Holy Symbol", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Holy Water", + "value": "Throwing holy water on a target such as fiends or undead creatures will cause radiant damage.", + "type": "item", + "title": "Holy Water", + "description": "25", + "useForCharacterCreation": false + }, + { + "keys": "Hunting Trap", + "value": "Can be prepared to damage and ensare any creature that steps on it. They'll have to use their strength to escape or continue to suffer its effects.", + "type": "item", + "title": "Hunting Trap", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Ink", + "value": "A bottle that provides enough ink to write about 500 pages.", + "type": "item", + "title": "Ink", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Ink Pen", + "value": "A pen used to write or draw.", + "type": "item", + "title": "Ink Pen", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Jug", + "value": "Can contain up to 1 gallon of liquid.", + "type": "item", + "title": "Jug", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Ladder", + "value": "Climbable up to 10 feet.", + "type": "item", + "title": "Ladder", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Lamp", + "value": "Burns oil to create bright light in a 15 foot radius and dim light for an additional 30 feet.", + "type": "item", + "title": "Lamp", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Bullseye Lantern", + "value": "Burns oil to cast bright light in a 60 foot cone and dim light in an additional 60 feet.", + "type": "item", + "title": "Bullseye Lantern", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Hooded Lantern", + "value": "Burns oil to cast bright light in a 30 foot radius and dim light for an additional 30 feet. The hood can be lowered to decrease the dim light to 5 feet.", + "type": "item", + "title": "Hooded Lantern", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Lock", + "value": "Must be unlocked with its matching key. It may also be picked with a thieve's tools.", + "type": "item", + "title": "Lock", + "description": "10", + "useForCharacterCreation": false + }, + { + "keys": "Magnifying Glass", + "value": "Can appraise an item or inspect a highly detailed item. It can also be used to focus sunlight to cause a fire.", + "type": "item", + "title": "Magnifying Glass", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Manacles", + "value": "Can be used to restrain a creature.", + "type": "item", + "title": "Manacles", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Map", + "value": "Can be used to navigate easier in the area depicted in the map.", + "type": "item", + "title": "Map", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Mirror", + "value": "Can be used to help apply cosmetics and also peek around corners and reflect light.", + "type": "item", + "title": "Mirror", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Net", + "value": "Can be thrown on a creature to restrain it.", + "type": "item", + "title": "Net", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Oil", + "value": "Used as fuel for lanterns for 6 hours. It can be doused onto a creature or space so that it can be lit on fire.", + "type": "item", + "title": "Oil", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Paper", + "value": "Can hold up to 250 handwritten words.", + "type": "item", + "title": "Paper", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Parchment", + "value": "Can hold up to 250 handwritten words.", + "type": "item", + "title": "Parchment", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Perfume", + "value": "The scent can increase the user's charisma for an hour.", + "type": "item", + "title": "Perfume", + "description": "5", + "useForCharacterCreation": false + }, + { + "keys": "Basic Poison", + "value": "Can be applied to a weapon to add extra poison damage.", + "type": "item", + "title": "Basic Poison", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Pole", + "value": "Can be used to touch something out of reach or use it to vault over something.", + "type": "item", + "title": "Pole", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Iron Pot", + "value": "Can hold up to 1 gallon of liquid for cooking.", + "type": "item", + "title": "Iron Pot", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Healing", + "value": "Heals a minor amount of health.", + "type": "item", + "title": "Potion of Healing", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Greater Healing", + "value": "Heals a minor amount of health.", + "type": "item", + "title": "Potion of Healing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Superior Healing", + "value": "Heals a minor amount of health.", + "type": "item", + "title": "Potion of Healing", + "description": "500", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Supreme Healing", + "value": "Heals a minor amount of health.", + "type": "item", + "title": "Potion of Healing", + "description": "5000", + "useForCharacterCreation": false + }, + { + "keys": "Pouch", + "value": "Holds up to 6 pounds within one-fifth of a cubic foot.", + "type": "item", + "title": "Pouch", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Priest's Pack", + "value": "Contains backpack, blanket, holy water, lamp, 7 days of rations, robe, and tinderbox", + "type": "item", + "title": "Priest's Pack", + "description": "33", + "useForCharacterCreation": false + }, + { + "keys": "Quiver", + "value": "Holds up to 20 arrows.", + "type": "item", + "title": "Quiver", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Portable Ram", + "value": "Can break down doors.", + "type": "item", + "title": "Portable Ram", + "description": "4", + "useForCharacterCreation": false + }, + { + "keys": "Rations", + "value": "Travel-ready food like jerky, dried fruit, hardtack, and nuts.", + "type": "item", + "title": "Rations", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Robe", + "value": "Clothing typical of vocational or ceremonial significance.", + "type": "item", + "title": "Robe", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Rope", + "value": "Can be tied in a knot around a creature or object.", + "type": "item", + "title": "Rope", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Sack", + "value": "Holds 30 pounds withn 1 cubic foot.", + "type": "item", + "title": "Sack", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Scholar's Pack", + "value": "Contains backpack, book, ink, ink pen, lamp, 10 lasks of oil, 10 sheets of parchment, and Tinderbox.", + "type": "item", + "title": "Scholar's Pack", + "description": "40", + "useForCharacterCreation": false + }, + { + "keys": "Shovel", + "value": "Can dig a 5 foot hole in an hour.", + "type": "item", + "title": "Shovel", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Signal Whistle", + "value": "Produces sound that can be hear up to 600 feet away.", + "type": "item", + "title": "Signal Whistle", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Spell Scroll", + "value": "Contains the magic of a single spell that can be cast when it's read. It disinigrates if it is targeted at a creature.", + "type": "item", + "title": "Spell Scroll", + "description": "30", + "useForCharacterCreation": false + }, + { + "keys": "Iron Spikes", + "value": "Can be hammered into the ground as a spike, used to jam a door shut, or tie a rope/chain to.", + "type": "item", + "title": "Iron Spikes", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Spyglass", + "value": "Magnifies objects viewed at a distance through it.", + "type": "item", + "title": "Spyglass", + "description": "1000", + "useForCharacterCreation": false + }, + { + "keys": "String", + "value": "A ten foot length that could be tied in a knot.", + "type": "item", + "title": "String", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Tent", + "value": "Can be used to house 2-3 creatures to sleep in.", + "type": "item", + "title": "Tent", + "description": "2", + "useForCharacterCreation": false + }, + { + "keys": "Tinderbox", + "value": "Can be used to light a fire if the target has exposed fuel, otherwise it takes 1 minute to initiate a fire.", + "type": "item", + "title": "Tinderbox", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Torch", + "value": "Casts bright light in a 20 foot radius and dim light for an additional 20 feet. It burns for an hour. It can be used as a makeshift weapon causing fire damage.", + "type": "item", + "title": "Torch", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Vial", + "value": "Can hold up to 4 ounces of liquid.", + "type": "item", + "title": "Vial", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "Waterskin", + "value": "Holds up to 4 pints of liquid.", + "type": "item", + "title": "Waterskin", + "description": "1", + "useForCharacterCreation": false + }, + { + "keys": "of Gleaming", + "value": "An armor that never gets dirty.", + "type": "armor", + "title": "Armor of Gleaming", + "description": "100,18", + "useForCharacterCreation": false + }, + { + "keys": "Bead of Nourishment", + "value": "Dissolves on the tongue and provides enough nourishment for one day.", + "type": "item", + "title": "Bead of Nourishment", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Bead of Refreshment", + "value": "Dissolves into liquid, transforming it into cold drinking water.", + "type": "item", + "title": "Bead of Refreshment", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Boots of False Tracks", + "value": "Leaves tracks of any kind of humanoid as choosen by the wearer.", + "type": "item", + "title": "Boots of False Tracks", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Candle of the Deep", + "value": "A candle that continues to work while underwater.", + "type": "item", + "title": "Candle of the Deep", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Cast-Off Armor", + "value": "Armor that can be removed instantly through magic.", + "type": "armor", + "title": "Cast-Off Armor", + "description": "100,17", + "useForCharacterCreation": false + }, + { + "keys": "Charlatan's Die", + "value": "A six sided die where the number rolled can be chosen.", + "type": "item", + "title": "Charlatan's Die", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Billowing", + "value": "Can be commanded to billow dramatically.", + "type": "item", + "title": "Cloak of Billowing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Many Fashions", + "value": "Can magically change style to anything desired but it must remain as a cloak.", + "type": "item", + "title": "Cloak of Many Fashions", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Clockwork Amulet", + "value": "Once per day, the ticking, geared amulet can be used to make any attack have basic accuracy.", + "type": "item", + "title": "Clockwork Amulet", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Clothes of Mending", + "value": "Automatically repairs itself as a result of normal wear and tear.", + "type": "item", + "title": "Clothes of Mending", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Dark Shard Amulet", + "value": "Once per day, the amulet can be used to cast a warlock cantrip. It requires intelligence to wield.", + "type": "item", + "title": "Dark Shard Amulet", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Dread Helm", + "value": "A fearsome helmet that disguises the wearer's face in shadow and turns their eyes glowing red.", + "type": "item", + "title": "Dread Helm", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Ear Horn of Hearing", + "value": "Suppresses the deafened condition when held to the ear.", + "type": "item", + "title": "Ear Horn of Hearing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Enduring Spellbook", + "value": "A spellbook that cannot be damaged by fire, water, or age.", + "type": "item", + "title": "Enduring Spellbook", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Ersatz Eye", + "value": "Magical replacement for a missing eye that works as well as a real one.", + "type": "item", + "title": "Ersatz Eye", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Hat of Vermin", + "value": "Can summmon a bat, frog, or rat. However, it is not under the control of the caster and only lasts an hour. It can be used 3 times a day.", + "type": "item", + "title": "Hat of Vermin", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Hat of Wizardry", + "value": "Once per day, the hat can be used to cast a wizard cantrip. It requires intelligence to wield.", + "type": "item", + "title": "Hat of Wizardry", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Heward's Handy Spice Pouch", + "value": "Can be used up to 10 times to summon nonmagical seasoning at will. It regains some of its charges every day.", + "type": "item", + "title": "Heward's Handy Spice Pouch", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Horn of Silent Alarm", + "value": "When blown, only a single chosen creature will hear the sound of this horn within 600 feet. It can be used up to 4 times a day.", + "type": "item", + "title": "Horn of Silent Alarm", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Instrument of Illusions", + "value": "While being played, this instrument creates harmless, illusory effects around it.", + "type": "item", + "title": "Instrument of Illusions", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Instrument of Scribing", + "value": "While being played, this instrument can magically inscribe a few words into a surface or object. It fades after a day. It can be used 3 times per day.", + "type": "item", + "title": "Instrument of Scribing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Lock of Trickery", + "value": "Otherwise like an ordinary lock, this device magically adjusts to thwart burglars and makes lockpicking much more difficult.", + "type": "item", + "title": "Lock of Trickery", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Moon-Touched Sword", + "value": "Creates bright light in a 15 foot radius and dim light for an additional 15 feet when unsheathed.", + "type": "weapon", + "title": "Moon-Touched Sword", + "description": "100,2d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Mystery Key", + "value": "It has a small chance of unlocking any lock that it is inserted to. Once it does so, it disappears.", + "type": "item", + "title": "Mystery Key", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Orb of Direction", + "value": "Can be used to determine which way is magnetic north.", + "type": "item", + "title": "Orb of Direction", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Orb of Time", + "value": "Can be used to determine if its morning, afternoon, evening, or nighttime.", + "type": "item", + "title": "Orb of Time", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Perfume of Bewitching", + "value": "Magical perfume that enhances the ability to influence creatures nearby. It can be used only once.", + "type": "item", + "title": "Perfume of Bewitching", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Pipe of Smoke Monsters", + "value": "Can be used to create a puff of smoke in the shape of any creature. It dissapates after a few seconds.", + "type": "item", + "title": "Pipe of Smoke Monsters", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Pole of Angling", + "value": "A basic pole that can be magically transformed into a fishing pole with a hook, line, and reel. It can be transformed back into a regular pole.", + "type": "item", + "title": "Pole of Angling", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Pole of Collapsing", + "value": "Can be transformed from a pole to a rod and back again.", + "type": "item", + "title": "Pole of Collapsing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Climbing", + "value": "Gain the ability to climb surfaces with relative ease.", + "type": "item", + "title": "Potion of Climbing", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Comprehension", + "value": "When drank, it allows the caster to understand any language seen or heard. The caster must be touching the surface of a written word to understand it. It does not decode cipher.", + "type": "item", + "title": "Potion of Comprehension", + "description": "50", + "useForCharacterCreation": false + }, + { + "keys": "Pot of Awakening", + "value": "Planting an ordinary shrub inside this pot will transform it into an Awakened Shrub that will obey your commands. The pot is broken when this happens.", + "type": "item", + "title": "Pot of Awakening", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Prosthetic Limb", + "value": "A magic item that can replace a lost limb. It functions identically to a normal limb.", + "type": "item", + "title": "Prosthetic Limb", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Rival Coin", + "value": "When tossed, a target within 60 feet has a 50% chance of taking psychic damage. Otherwise, the tosser will recieve some psychic damage. It can be used once a day.", + "type": "item", + "title": "Rival Coin", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Rope of Mending", + "value": "A 50 foot rope that can be cut up and be connected back together through magic.", + "type": "item", + "title": "Rope of Mending", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Ruby of the War Mage", + "value": "Allows a weapon to be used as a spell-casting focus.", + "type": "item", + "title": "Ruby of the War Mage", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Shield of Expression", + "value": "The shield bears a face which can be altered magically to have a different expression.", + "type": "armor", + "title": "Shield of Expression", + "description": "100,+2", + "useForCharacterCreation": false + }, + { + "keys": "Silvered Weapon", + "value": "Does additional damage to shape-shifted creatures.", + "type": "weapon", + "title": "Silvered Weapon", + "description": "10,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Smoldering Armor", + "value": "The armor has wisps of odorless smoke rise from it.", + "type": "armor", + "title": "Smoldering Armor", + "description": "100,17", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Adornment", + "value": "Allows up to three small items to be held hovering at the tip of the staff.", + "type": "weapon", + "title": "Staff of Adornment", + "description": "100,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Birdcalls", + "value": "Can imitate the sound of several types of birds. It can be used up to 10 times and recharges daily.", + "type": "weapon", + "title": "Staff of Birdcalls", + "description": "100,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Flowers", + "value": "Can cause a flower to grow and bloom from a patch of earth or soil. It can be used up to 10 times and recharges daily.", + "type": "weapon", + "title": "Staff of Flowers", + "description": "100,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Talking Doll", + "value": "A doll that can be taught to speak up to 6 different short phrases. It will speak these based on conditions set by the owner.", + "type": "item", + "title": "Talking Doll", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Tankard of Sobriety", + "value": "Drinking from this tankard prevents the drinker from getting drunk.", + "type": "item", + "title": "Tankard of Sobriety", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Veteran's Cane", + "value": "Can be transformed into a long sword and back again.", + "type": "item", + "title": "Veteran's Cane", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Walloping Ammunition", + "value": "When hit by this ammunition, the target may be knocked prone.", + "type": "item", + "title": "Walloping Ammunition", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Conducting", + "value": "While waving thhe wand around, the sound of orchestral music can be heard within 120 feet. It can be used up to 3 times per day.", + "type": "item", + "title": "Wand of Conducting", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Pyrotechnics", + "value": "Creates a harmless burst of multicolored light within 120 feet and a crackling noise within 300 feet. It can be used up to 7 times with recharges daily.", + "type": "item", + "title": "Wand of Pyrotechnics", + "description": "100", + "useForCharacterCreation": false + }, + { + "keys": "Adamantine Armor", + "value": "Made of one of the hardest materials in existence, it prevents critical damage.", + "type": "armor", + "title": "Adamantine Armor", + "description": "400,18", + "useForCharacterCreation": false + }, + { + "keys": "Adamantine Weapon", + "value": "Made of one of the hardest materials in existence, it causes critical damage against objects.", + "type": "weapon", + "title": "Adamantine Weapon", + "description": "400,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Alchemy Jug", + "value": "Can be used to magically produce a liquid of the user's choice (acid, poison, beer, honey, mayonnaise, oil, vinegar, water, salt water, wine). It can't produce a different kind of liquid until the next day.", + "type": "item", + "title": "Alchemy Jug", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ammunition +1", + "value": "Magical bonus damage and accuracy.", + "type": "item", + "title": "Ammunition +1", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Amulet of Proof against Detection and Location", + "value": "The wearer can not be targeted by divination spells.", + "type": "item", + "title": "Amulet of Proof against Detection and Location", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Baba Yaga's Dancing Broom", + "value": "Can be transformed into an animated broom that can follow commands.", + "type": "item", + "title": "Baba Yaga's Dancing Broom", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Bag of Holding", + "value": "A magical container that can hold up to 500 pounds inside a 64 cubic foot space. The bag will only feel like it weighs 5 pounds. If it is damaged, it will explode in the astral plane, losing all contents.", + "type": "item", + "title": "Bag of Holding", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Bag of Tricks", + "value": "Allows the user to pull out a fuzzy object that transforms into a creature that they can command (Weasel, rat, badger, boar, panther, giant badger, dire wolf, giant elk. It can be used up to three times per day.", + "type": "item", + "title": "Bag of Tricks", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Boots of Elvenkind", + "value": "The wearer makes no sound while taking steps with these boots. The wearer becomes really good at stealth.", + "type": "item", + "title": "Boots of Elvenkind", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Boots of Striding and Springing", + "value": "The wearer has greater speed and isn't hindered by carrying weight or wearing heavy armor. Can jump up to 30 feet with relative ease.", + "type": "item", + "title": "Boots of Striding and Springing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Boots of the Winterlands", + "value": "Warm and snug boots that grant cold resistance to the user and they can walk through difficult terrain of ice and snow.", + "type": "item", + "title": "Boots of the Winterlands", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Bracers of Archery", + "value": "Grants proficiency with the use of bows and increases their damage.", + "type": "item", + "title": "Bracers of Archery", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Brooch of Shielding", + "value": "Grants resistance to force damage and the magic missile spell has no effect on the user.", + "type": "item", + "title": "Brooch of Shielding", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Broom of Flying", + "value": "When commanded, the broom can be ridden in the air and fly up to 50 feet per 6 seconds.", + "type": "item", + "title": "Broom of Flying", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Cap of Water Breathing", + "value": "Creates a bubble of air around the head of the user, allowing them to breathe normally while underwater.", + "type": "item", + "title": "Cap of Water Breathing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Circlet of Blasting", + "value": "Allows casting Scorching Ray once a day.", + "type": "item", + "title": "Circlet of Blasting", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Elvenkind", + "value": "Makes the user harder to spot while stealthing.", + "type": "item", + "title": "Cloak of Elvenkind", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Protection", + "value": "Increases the defense of the person who dons this cloak.", + "type": "item", + "title": "Cloak of Protection", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of the Manta Ray", + "value": "Allows the user to breathe underwater and swim at incredible speeds.", + "type": "item", + "title": "Cloak of the Manta Ray", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Decanter of Endless Water", + "value": "Can produce fresh or salt water at will. It can be a gallon, five gallons, or shoot out like a geyser. It can be aimed to knock enemies prone.", + "type": "item", + "title": "Decanter of Endless Water", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Deck of Illusions", + "value": "A magic deck of cards that can produce an illusion based on the depicted creature illustrated on a randomly drawn card. Touching the illusion reveals it to be a fake. Once used, the card can't be reused.", + "type": "item", + "title": "Deck of Illusions", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Driftglobe", + "value": "A floating orb that can be commanded to cast light. It can cast the spell Daylight once a day. It follows the user as they walk around.", + "type": "item", + "title": "Driftglobe", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Dust of Disappearance", + "value": "When thrown in the air, it makes every creature within 10 feet invisible for several minutes.", + "type": "item", + "title": "Dust of Disappearance", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Dust of Dryness", + "value": "A dust that can be poured on water which will transform up to a 15 foot cube of it into a marble-sized pellet. The pellet can then be thrown to release its water", + "type": "item", + "title": "Dust of Dryness", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Dust of Sneezing and Choking", + "value": "A small container of powder that can be used once. Thrown in the air, it causes all creatures to sneeze uncontrollably in a 30 foot radius.", + "type": "item", + "title": "Dust of Sneezing and Choking", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Elemntal Gem", + "value": "A gem containing elemental energy which unleashes an elemental construct upon crushing it.", + "type": "item", + "title": "Elemntal Gem", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Armor Uncommon", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "armor", + "title": "Enspelled Armor Uncommon", + "description": "400,15+d2", + "useForCharacterCreation": false + }, + { + "keys": "Uncommon Enspelled Staff", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Uncommon Enspelled Staff", + "description": "400,1d6,strength", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Weapon Uncommon", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Enspelled Weapon Uncommon", + "description": "400,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Eversmoking Bottle", + "value": "Opening this bottle unleashes a cloud of smoke that grows up to a maximum of 120 feet wide. The smoke heavily obscures vision through it.", + "type": "item", + "title": "Eversmoking Bottle", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Eyes of Charming", + "value": "Lenses that can be worn over the eyes. They can be used to charm a person up to 3 times a day. It recharges completely every day.", + "type": "item", + "title": "Eyes of Charming", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Eyes of Minute Seeing", + "value": "Lenses that can be worn over the eyes. They grant the wearer Darkvision and better vision up to 1 foot.", + "type": "item", + "title": "Eyes of Minute Seeing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Eyes of the Eagle", + "value": "Lenses that can be worn over the eyes. It grants the user the ability to see clearly over great distances and ", + "type": "item", + "title": "Eyes of the Eagle", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Silver Raven Figurine of Wondrous Power", + "value": "Transforms into a raven for up to 12 hours. It can only be used again after 2 days.", + "type": "item", + "title": "Silver Raven Figurine of Wondrous Power", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Gauntlets of Ogre Power", + "value": "Increases the strength of the user to the near peak of ability.", + "type": "item", + "title": "Gauntlets of Ogre Power", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Gem of Brightness", + "value": "Can be used to cast bright light, blind a creature, or blind all creatures in a cone. It has 50 charges which are expelled at a rate depending on the intensity of the light.", + "type": "item", + "title": "Gem of Brightness", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Gloves of Missile Snaring", + "value": "Mitigates the damage of ranged or thrown attacks at the user with the chance of catching the ammunition.", + "type": "item", + "title": "Gloves of Missile Snaring", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Gloves of Swimming and Climbing", + "value": "Increases the ability to swim and climb.", + "type": "item", + "title": "Gloves of Swimming and Climbing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Gloves of Thievery", + "value": "Increases the sleight of hand ability. These gloves are invisible while they are worn.", + "type": "item", + "title": "Gloves of Thievery", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Goggles of Night", + "value": "Lenses that can be worn over the eyes. It allows the user to have dark vision to 60 feet.", + "type": "item", + "title": "Goggles of Night", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Hag Eye", + "value": "Can be used to cast darkvision or see invisibility up to 3 times per day. It allows the hag who created the eye to see through it if they concentrate.", + "type": "item", + "title": "Hag Eye", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Hat of Disguise", + "value": "Allows the user to cast the spell Disguise Self. It only has an effect while it's worn.", + "type": "item", + "title": "Hat of Disguise", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Headband of Intellect", + "value": "Grants the user an intelligence nearly at the peak of human ability.", + "type": "item", + "title": "Headband of Intellect", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Helm of Comprehending Languages", + "value": "Allows the user to cast the spell comprehend languages at will.", + "type": "item", + "title": "Helm of Comprehending Languages", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Helm of Telepathy", + "value": "Grants the user the ability to speak telepathically or cast detect thoughts or suggestion from the helm. Once a spell is used, it can't cast another spell until the next day.", + "type": "item", + "title": "Helm of Telepathy", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Immovable Rod", + "value": "Pressing a button on the rod fixes the rod firmly in space, allowing it to hold up to 8000 pounds of weight, defying gravity.", + "type": "item", + "title": "Immovable Rod", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Doss Lute", + "value": "A magical instrument that can cast fly, invisibility, levitate, protection from good and evil, animal friendship, protection from energy (fire damage), and protection from poison each once a day.", + "type": "item", + "title": "Doss Lute", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Fochlucan Bandore", + "value": "A magical instrument that can cast fly, invisibility, levitate, protection from good and evil, entangle, faerie fire, shillelagh, and speak with animals each once a day.", + "type": "item", + "title": "Fochlucan Bandore", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Mac-Fuirmidh Cittern", + "value": "A magical instrument that can cast fly, invisibility, levitate, protection from good and evil, barkskin, cure wounds, and fog cloud each once a day.", + "type": "item", + "title": "Mac-Fuirmidh Cittern", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Javelin of Lightning", + "value": "Can deal lightning damage instead of piercing damage. It can also be transformed into a bolt of lightning that will damage enemies in a line.", + "type": "weapon", + "title": "Javelin of Lightning", + "description": "400,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Keoghtom's Ointment", + "value": "A thick aloe mixture that can heal and removes the poisoned condition.", + "type": "item", + "title": "Keoghtom's Ointment", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Lantern of Revealing", + "value": "Functions as a regular lantern and also reveals invisible creatures while they are in bright light.", + "type": "item", + "title": "Lantern of Revealing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Mariner's Armor", + "value": "Increases the ability to swim and automatically brings you back to life if submerged under water once per day.", + "type": "armor", + "title": "Mariner's Armor", + "description": "400,12+d2", + "useForCharacterCreation": false + }, + { + "keys": "Medallion of Thoughts", + "value": "Can be used to cast detect thoughts up to 5 times and recharges slowly each day.", + "type": "item", + "title": "Medallion of Thoughts", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Nature's Mantle", + "value": "Allows the user to blend into their surroundings and easily hide. It changes color and texture as needed. They can hide even if directly observed if they are lightly obscured.", + "type": "item", + "title": "Nature's Mantle", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Necklace of Adaptation", + "value": "Allows the user to breathe normally in any environment and is hard to poison.", + "type": "item", + "title": "Necklace of Adaptation", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Oil of Slipperiness", + "value": "Grants the effect of Freedom of Movement spell to the user if applied to the body. If poured on the ground, it has the effect of the grease spell.", + "type": "item", + "title": "Oil of Slipperiness", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Pearl of Power", + "value": "Allows the user to regain a spell slot of level 3 or lower once a day.", + "type": "item", + "title": "Pearl of Power", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Periapt of Health", + "value": "Allows the user to heal a moderate amount once a day. It also helps prevent the poisoned condition.", + "type": "item", + "title": "Periapt of Health", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Periapt of Wound Closure", + "value": "Increases the chances of surviving death and increases the amount of health regained while resting.", + "type": "item", + "title": "Periapt of Wound Closure", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Philter of Love", + "value": "A liquid that causes a creature to be charmed by the next creature it sees after it is drunk.", + "type": "item", + "title": "Philter of Love", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Pipes of Haunting", + "value": "Playing these pipes creates an eerie, spellbinding tune that causes creatures that hear it a chance to become frightened for a minute.", + "type": "item", + "title": "Pipes of Haunting", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Pipes of the Sewers", + "value": "Whoever holds this is seen as indifferent by ordinary and giant rats unless harmed. Playing the pipes calls forth a swarm of rats that follows the users commands.", + "type": "item", + "title": "Pipes of the Sewers", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Animal Friendship", + "value": "When the potion is drunk, the user can cast animal friendship.", + "type": "item", + "title": "Potion of Animal Friendship", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Fire Breath", + "value": "Allows the user to breathe out damaging fire at a target.", + "type": "item", + "title": "Potion of Fire Breath", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Hill Giant Strength", + "value": "Drinking this potion grants the user strength that just surpasses normal human limits.", + "type": "item", + "title": "Potion of Hill Giant Strength", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Growth", + "value": "Causes the drinker of this potion to receive the effect of the enlarge spell for 10 minutes.", + "type": "item", + "title": "Potion of Growth", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Poison", + "value": "Looks like a potion of healing, but actually causes the drinker to become poisoned.", + "type": "item", + "title": "Potion of Poison", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Puglism", + "value": "Drinking this potion causes the user to deal extra force damage when using unarmed strikes.", + "type": "item", + "title": "Potion of Puglism", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Resistance", + "value": "Drinking this potion grants resistance to a specific type of damage for 1 hour.", + "type": "item", + "title": "Potion of Resistance", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Water Breathing", + "value": "Allows the user to breathe underwater for 24 hours after drinking.", + "type": "item", + "title": "Potion of Water Breathing", + "description": "200", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Anchor", + "value": "A feather shaped token that prevents a vessel from moving for 24 hours when the token is pressed against it. It can only be used once.", + "type": "item", + "title": "Quaal's Feather Token Uncommon", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Fan", + "value": "Tossing the token into the air creates a strong wind that can push a ship or vessel to go an extra 5 miles per hour for 8 hours.", + "type": "item", + "title": "Quaal's Feather Token Uncommon", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Tree", + "value": "Touching the token to unoccupied space on the ground outside causes a non-magical oak tree to grow instantly at that spot.", + "type": "item", + "title": "Quaal's Feather Token Uncommon", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Quiver of Ehlonna", + "value": "A quiver with 3 compartments that can hold ammunition in extradimensional space with no added weight. 60 arrows/bolts, 18 javelins, 6 staves/bows", + "type": "item", + "title": "Quiver of Ehlonna", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Jumping", + "value": "Allows the user to cast jump on themselves at will.", + "type": "item", + "title": "Ring of Jumping", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Mind Shielding", + "value": "Prevents the user from reading their thoughts, determining if they're lying, learning their alignment, or understanding their creature type. If they die while wearing the ring, the user's soul may enter it instead of going to the afterlife. It can then communicate to the next creature that wears it telepathically.", + "type": "item", + "title": "Ring of Mind Shielding", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Swimming", + "value": "Increases the swimming speed of the wearer to an incredible amount.", + "type": "item", + "title": "Ring of Swimming", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Warmth", + "value": "Reduces the amount of damage from cold attacks. The user is unharmed by temperatures of 0 degrees Fahrenheit or lower.", + "type": "item", + "title": "Ring of Warmth", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Water Walking", + "value": "Allows the user to cast water walk on themselves.", + "type": "item", + "title": "Ring of Water Walking", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Robe of Useful Items", + "value": "A robe with many patches of items sewn onto it. These patches can be removed which magically creates that item. It has patches of lanterns, daggers, mirrors, poles, ropes, sacks. It can have bags of gold, a silver coffer, an iron door, gems, a wooden ladder, riding horse, an open pit, healing potions, a rowboat, spell scrolls, mastiffs, a window, and a portable ram.", + "type": "item", + "title": "Robe of Useful Items", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Rod of the Pact Keeper +1", + "value": "Gives a bonus to spell attacks and allows regaining a spell slot once a day.", + "type": "item", + "title": "Rod of the Pact Keeper +1", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Rope of Climbing", + "value": "A rope that can be magically commanded to move and attach wherever desired within 60 feet.", + "type": "item", + "title": "Rope of Climbing", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Saddle of the Cavalier", + "value": "Magically protects the mount against direct attack and the user cannot be dismounted against their will.", + "type": "item", + "title": "Saddle of the Cavalier", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Sending Stones", + "value": "Comes in pairs. Allows the user to cast sending to the user of the other stone. It can be used once a day.", + "type": "item", + "title": "Sending Stones", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Sentinel Shield", + "value": "A magical shield emblazoned with an eye on its face. It makes the user more perceptive and have greater initiative.", + "type": "armor", + "title": "Sentinel Shield", + "description": "400,+2", + "useForCharacterCreation": false + }, + { + "keys": "Shield +1", + "value": "A magically enhanced shield that makes the user harder to hit.", + "type": "armor", + "title": "Shield +1", + "description": "400,+3", + "useForCharacterCreation": false + }, + { + "keys": "Slippers of Spider Climbining", + "value": "Allows the user to walk over walls and ceilings with relative ease. It doesn't work on surfaces of ice or oil.", + "type": "item", + "title": "Slippers of Spider Climbining", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Staff of the Adder", + "value": "A staff that can transform into a snake. The user can command the snake to move and attack with piercing and poison damage.", + "type": "weapon", + "title": "Staff of the Adder", + "description": "400,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of the Python", + "value": "When thrown on the ground, it can be transformed into a giant constrictor snake. The user can command the snake to move or attack. It can be reverted back to staff form to regain its health.", + "type": "weapon", + "title": "Staff of the Python", + "description": "400,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Stone of Good Luck", + "value": "The luck stone grants the user a bonus to their abilities and the chances of mitigating harmful effects.", + "type": "item", + "title": "Stone of Good Luck", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Sword of Vengeance", + "value": "Magically enhanced to be better at landing attacks as well as damaging opponents. It's a cursed weapon possessed by an angry spirit. The user has disadvantage on attacks with other weapons and is compelled to strike back at a foe that causes damage until they or the user is struck down.", + "type": "weapon", + "title": "Sword of Vengeance", + "description": "400,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Trident of Fish Command", + "value": "Can be used to cast dominate beast on any swimming creature up to 3 times daily.", + "type": "weapon", + "title": "Trident of Fish Command", + "description": "400,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Magic Detection", + "value": "Can be used to cast detect magic up to three times daily.", + "type": "item", + "title": "Wand of Magic Detection", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Magic Missiles", + "value": "Grants the user up to 7 charges of magic missile. Up to three can be used at a time with a number of charges regained every day.", + "type": "item", + "title": "Wand of Magic Missiles", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Secrets", + "value": "Reveals the direction to a secret door or trap up to three times with some charges regained daily.", + "type": "item", + "title": "Wand of Secrets", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Wand of the War Mage +1", + "value": "Grants the user additional chance to hit with magic spells. It ignores cover.", + "type": "item", + "title": "Wand of the War Mage +1", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Web", + "value": "Allows the user to cast web up to 7 times. It regains a number of charges daily. If the last charge is used, it has a chance of crumbling to dust and becoming unusable.", + "type": "item", + "title": "Wand of Web", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Weapon +1", + "value": "A weapon with a magically enhanced chance to hit and increased damage.", + "type": "weapon", + "title": "Weapon +1", + "description": "400,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Weapon of Warning", + "value": "Gives the user and their allies the ability to wake up instantly when combat is initiated. Each ally also has increased initiative.", + "type": "weapon", + "title": "Weapon of Warning", + "description": "400,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Wind Fan", + "value": "Allows the user to cast a gust of wind. Each time it is used, it has a small chance of tearing and being able to be used again.", + "type": "item", + "title": "Wind Fan", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Winged Boots", + "value": "Allows the user to fly for an hour. When the hour elapses the user can still descend gracefully until they land onto the ground.", + "type": "item", + "title": "Winged Boots", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Wraps of Unarmed Power +1", + "value": "Increases the accuracy and damage of unarmed strikes. They can cause force damage or normal damage.", + "type": "item", + "title": "Wraps of Unarmed Power +1", + "description": "400", + "useForCharacterCreation": false + }, + { + "keys": "Ammunition +2", + "value": "A bundle of ten pieces of magically enhanced ammunition. Has greater accuracy and deals additional damage.", + "type": "item", + "title": "Ammunition +2", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Amulet of Health", + "value": "Increases the constitution of the user to nearly the peak of human ability.", + "type": "item", + "title": "Amulet of Health", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Armor +1", + "value": "Magically enhanced armor that increases the user's ability to avoid damage.", + "type": "armor", + "title": "Armor +1", + "description": "4000,19", + "useForCharacterCreation": false + }, + { + "keys": "Armor of Resistance", + "value": "Gives magical resistance to a specific type of damage.", + "type": "armor", + "title": "Armor of Resistance", + "description": "4000,17", + "useForCharacterCreation": false + }, + { + "keys": "Armor of Vulnerability", + "value": "A cursed armor that gives resistance to one of bludgeoning, piercing, or slashing types of damage, but vulnerablility to the other two.", + "type": "armor", + "title": "Armor of Vulnerability", + "description": "4000,14+d2", + "useForCharacterCreation": false + }, + { + "keys": "Arrow-Catching Shield", + "value": "A shield that gives extra defense against ranged attacks. The user can also react to a nearby ally being targeted by a ranged attack and become the target.", + "type": "item", + "title": "Arrow-Catching Shield", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Bag of Beans", + "value": "A pouch of a handful of magical beans that have various effects when planted. Create poisonous toadstools,geyser of drink, good/evil treant, mean talking statue, green campfire, shrieker fungi, transforming pink toads, bulette, fruit tree, nest of good/bad eggs, mummy lord, giant beanstalk", + "type": "item", + "title": "Bag of Beans", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Dwarvenkind", + "value": "A magical belt that allows the wearer to speak dwarvish, befriend dwarves easily, increases toughness, adds dark vision, and adds resilience to poison damage.", + "type": "item", + "title": "Belt of Dwarvenkind", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Hill Giant Strength", + "value": "Increases strength to just above peak human ability.", + "type": "item", + "title": "Belt of Hill Giant Strength", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Berserker Axe", + "value": "An axe with increased accuracy and damage and increases the user's overall health. It is cursed and the user is unwilling to part with it. Becoming damaged sends the user into beserker state.", + "type": "weapon", + "title": "Berserker Axe", + "description": "4000,1d12+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Boots of Levitation", + "value": "Allows the user to cast levitate on themselves.", + "type": "item", + "title": "Boots of Levitation", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Boots of Speed", + "value": "Allows the user to double their speed and avoid opportunity attacks. Use up to 10 minutes before having to rest to recharge them.", + "type": "item", + "title": "Boots of Speed", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Bowl of Commanding Water Elementals", + "value": "Creates a water elemental that follows the user's commands and lasts for an hour. Used once per day.", + "type": "item", + "title": "Bowl of Commanding Water Elementals", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Bracers of Defense", + "value": "Increases the ability to dodge attacks when not wearing armor and not using a shield.", + "type": "item", + "title": "Bracers of Defense", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Brazier of Commanding Fire Elementals", + "value": "Creates a fire elemental that follows the user's commands and lasts for an hour. Used once per day.", + "type": "item", + "title": "Brazier of Commanding Fire Elementals", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cape of the Mountebank", + "value": "Smells like brimstone. Allows the user to cast dimension door once a day, leaving a cloud of smoke behind them. It lightly obscures view through it.", + "type": "item", + "title": "Cape of the Mountebank", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Censer of Controlling Air Elementals", + "value": "Creates an air elemental that follows the user's commands and lasts for an hour. Used once per day.", + "type": "item", + "title": "Censer of Controlling Air Elementals", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Chime of Opening", + "value": "A hollow metal tube that allows the user to cast knock. It makes a ringing tone of a chime when it works. It can be used up to 10 times.", + "type": "item", + "title": "Chime of Opening", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Displacement", + "value": "A magic cloak that makes the user appear to be standing close by to their location. This causes attackers to have disadvantage when attacking.", + "type": "item", + "title": "Cloak of Displacement", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of the Bat", + "value": "Allows the user to fly a fair distance if they grip the edges of the cloak. It also allows the user to polymorph in to a bat. It only works in dim light or darkness.", + "type": "item", + "title": "Cloak of the Bat", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cube of Force", + "value": "A 6 sided cube that allows the user to cast an associated spell: mage armor, shield, leomund's tiny hut, mordenkainen's private sanctum, otiluke's resilient sphere, wall of force. The more powerful the spell, the more charges it uses. 10 charges, slowly charging per day.", + "type": "item", + "title": "Cube of Force", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cube of Summoning", + "value": "A jack in the box type of magic object that creates a tune when it's cranked. When the lid pops open, a creature is summoned: an aberration, beast, construct, dragon, elemental, fey.", + "type": "item", + "title": "Cube of Summoning", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Daern's Instant Fortress", + "value": "A statuette that, when placed on the ground, transforms into 30 foot high tower with an internal ramp/ladder/staircase with a trapdoor at the top. It is only vulnerable to siege equipment.", + "type": "item", + "title": "Daern's Instant Fortress", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Dagger of Venom", + "value": "A dagger with increased accuracy and damage. It can be magically coated with poison once a day for additional damage.", + "type": "weapon", + "title": "Dagger of Venom", + "description": "4000,1d4+1,+1,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Dimensional Shackles", + "value": "Shackles that prevent the bound creature from teleporting. Only the user and those they delegate can open the shackles. The bound creature can try to break free once every 30 days, but it's nearly impossible.", + "type": "item", + "title": "Dimensional Shackles", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Dragon Slayer", + "value": "A weapon that has increased accuracy and damage. It deals even more damage when the target is a dragon.", + "type": "weapon", + "title": "Dragon Slayer", + "description": "4000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Elixir of Health", + "value": "A clear, red liquid with tiny bubbles of light that cures all magical contagions, blindness, deafened, paralyzed, and poisoned states when drunk. ", + "type": "item", + "title": "Elixir of Health", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Elven Chain", + "value": "Increases the chance to avoid an attack and can be used by those who don't even know how to wear armor of this weight.", + "type": "item", + "title": "Elven Chain", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Armor Rare", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "armor", + "title": "Enspelled Armor Rare", + "description": "4000,15+d2", + "useForCharacterCreation": false + }, + { + "keys": "Rare Enspelled Staff", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Rare Enspelled Staff", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Weapon Rare", + "value": "Allows the user to cast a basic spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Enspelled Weapon Rare", + "description": "4000,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Bronze Griffon Figurine of Wondrous Power", + "value": "A figurine that can be transformed into a griffon for up to 6 hours. Used once every 5 days.", + "type": "item", + "title": "Bronze Griffon Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ebony Fly Figurine of Wondrous Power", + "value": "Can be transformed into a giant fly and can be ridden as a mount for 12 hours. Used once every 12 days.", + "type": "item", + "title": "Ebony Fly Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Golden Lions Figurine of Wondrous Power", + "value": "A pair of lion figurines that can be transformed into one or two lions for 1 hour. Used once every 7 days.", + "type": "item", + "title": "Golden Lions Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ivory goats Figurine of Wondrous Power", + "value": "A set of three goat figurines that can be tranformed into three unique goats: goat of terror (giant goat that is frightening), goat of traveling (large goat for riding), goat of travail (giant goat).", + "type": "item", + "title": "Ivory goats Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Onyx Dog Figurine of Wondrous Power", + "value": "Can be transformed into a mastiff that speaks common for 6 hours. Used once every 7 days.", + "type": "item", + "title": "Onyx Dog Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Marble Elephant Figurine of Wondrous Power", + "value": "Can be transformed into an elephhant for 24 hours. Used once every 7 days.", + "type": "item", + "title": "Marble Elephant Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Serpentine Owl Figurine of Wondrous Power", + "value": "Can be transformed into a giant owl for 8 hours. It can communicate telepathically. It can be used once every 2 days.", + "type": "item", + "title": "Serpentine Owl Figurine of Wondrous Power", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Flame Tongue", + "value": "A weapon that can be commanded to ignite into flame. It deals extra fire damage when it lands a blow.", + "type": "weapon", + "title": "Flame Tongue", + "description": "4000,1d8+7,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Folding Boat", + "value": "A wooden box that can store things or fold out into a rowboat, keelboat, or back into a box.", + "type": "item", + "title": "Folding Boat", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Gem of Seeing", + "value": "Grants the user truesight for up to 120 feet when the gem is peered through for 10 minutes. It has 3 charges and regains charges slowly every day.", + "type": "item", + "title": "Gem of Seeing", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Giant Slayer", + "value": "A weapon with greater accuracy and damage. This weapon does additional damage to giants.", + "type": "weapon", + "title": "Giant Slayer", + "description": "4000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Glamoured Studded Leather", + "value": "Enchanted to provide additional protection against attacks. It can be made to look like a normal set of clothing of the user's choosing.", + "type": "armor", + "title": "Glamoured Studded Leather", + "description": "4000,12+d", + "useForCharacterCreation": false + }, + { + "keys": "Helm of Teleportation", + "value": "Allows the user to cast teleport up to 3 times. It slowly regains charges every day.", + "type": "item", + "title": "Helm of Teleportation", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Heward's Handy Haversack", + "value": "Has pouches of extradimensional space holding a total of 700 pounds of material. When the reaches in, they can think of what they want to pull out.", + "type": "item", + "title": "Heward's Handy Haversack", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Horn of Blasting", + "value": "Blowing through this horn causes a thunderous blast in a 30 foot cone. The sound can be heard from 600 feet. It severely damages glass and crystal. Each time it is used, it has a small chance of explodinng, destroying the horn and damaging the user.", + "type": "item", + "title": "Horn of Blasting", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Silver Horn of Valhalla", + "value": "Blowing the horn summons 2 berserker warrior spirits for 1 hour that follow your commands. It can be used once every 7 days.", + "type": "item", + "title": "Silver Horn of Valhalla", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Brass Horn of Valhalla", + "value": "Blowing the horn summons 3 berserker warrior spirits for 1 hour that follow your commands only if you are proficient with simple weapons. It can be used once every 7 days.", + "type": "item", + "title": "Brass Horn of Valhalla", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Horseshoes of Speed", + "value": "Horseshoes that can be magically affixed to a horse by touching it. It increases the speed of the horse.", + "type": "item", + "title": "Horseshoes of Speed", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Canaith Mandolin", + "value": "A magical instrument that can cast fly, invisibility, levitate, protection from good and evil, cure wounds, dispel magic, protection from energy (lightning) each once a day.", + "type": "item", + "title": "Canaith Mandolin", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Cli Lyre", + "value": "A magical instrument that can cast fly, invisibility, levitate, protection from good and evil, stone shape, wall of fire, wind wall each once a day.", + "type": "item", + "title": "Cli Lyre", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Awareness", + "value": "A magic stone tossed in the air that orbits the user's head and makes them have greater initiative and perception.", + "type": "item", + "title": "Ioun Stone of Awareness", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Protection", + "value": "A magic stone tossed in the air that orbits the user's head and grants the user the ability to dodge attacks a little better.", + "type": "item", + "title": "Ioun Stone of Protection", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Reserve", + "value": "A magic stone tossed in the air that orbits the user's head and can hold up to 4 levels of spells inside of it. It can be used to cast the spell(s) at a later time.", + "type": "item", + "title": "Ioun Stone of Reserve", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Sustenance", + "value": "A magic stone tossed in the air that orbits the user's head and negates the need of the user to eat or drink.", + "type": "item", + "title": "Ioun Stone of Sustenance", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Iron Bands of Bilarro", + "value": "A sphere, when thrown, opens up into a tangle of metal bands that will restrain the target. If the throw misses, the bands return into a small sphere.", + "type": "item", + "title": "Iron Bands of Bilarro", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Mace of Disruption", + "value": "Fiends or an undead hit with this mace receive extra radiant damage. When such a target is close to being killed, it has a chance to simply be destroyed outright. If it doesn't, it is frightened. The mace emits bright light.", + "type": "weapon", + "title": "Mace of Disruption", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Mace of Smiting", + "value": "Magically enchanted to be more accurate and damaging. It does additonal damage to constructs.", + "type": "weapon", + "title": "Mace of Smiting", + "description": "4000,1d6+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Mace of Terror", + "value": "The mace has 3 charges used to unleash a wave of terror. It has a chance of frightening opponents and driving them away from the user. It recharges slowly every day.", + "type": "weapon", + "title": "Mace of Terror", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Mantle of Spell Resistance", + "value": "Grants the user resistance to spell effects.", + "type": "item", + "title": "Mantle of Spell Resistance", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Necklace of Fireballs", + "value": "The necklace has up to 9 beads attached to it. When a bead is thrown, it is detonated as a fireball on contact with a target or surface.", + "type": "item", + "title": "Necklace of Fireballs", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Necklace of Prayer Beads", + "value": "The necklace has up to 6 beads attached to it. Each colored bead has a spell effect: bless, cure wounds, greater restoration, shining smite, guardian of faith, wind walk. They can be used once a day and regain their use by the next dawn.", + "type": "item", + "title": "Necklace of Prayer Beads", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Oil of Etherealness", + "value": "If covered on a creature, the creature will have the effect of etherealness on them. The creature has to be medium or smaller.", + "type": "item", + "title": "Oil of Etherealness", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Periapt of Proof against Poison", + "value": "Regains health once per day while this pendant is worn. It is harder to be poisoned while wearing this.", + "type": "item", + "title": "Periapt of Proof against Poison", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Portable Hole", + "value": "A fine black cloth folded like a hankerchief. It can fold out to a circular extra-dimensional hole 10 feet deep. If closed, any creatures inside will be trapped.", + "type": "item", + "title": "Portable Hole", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Clairvoyance", + "value": "When drunk, gives the user the effect of the clairvoyance spell.", + "type": "item", + "title": "Potion of Clairvoyance", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Diminution", + "value": "The drinker gains the effect of the reduce spell for a few hours.", + "type": "item", + "title": "Potion of Diminution", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Gaseous Form", + "value": "The drinker gains the effect of the gaseous form spell.", + "type": "item", + "title": "Potion of Gaseous Form", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Frost Giant Strength", + "value": "Grants the drinker a strength greater than that of peak human ability for one hour.", + "type": "item", + "title": "Potion of Frost Giant Strength", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Stone Giant Strength", + "value": "Grants the drinker a strength greater that that of peak human ability for one hour.", + "type": "item", + "title": "Potion of Stone Giant Strength", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Fire Giant Strength", + "value": "Grants the drinker a strength significantly greater that that of peak human ability for one hour.", + "type": "item", + "title": "Potion of Fire Giant Strength", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Heroism", + "value": "Gives the drinker an additional amount of temporary health. It also grants them the effect of the bless spell.", + "type": "item", + "title": "Potion of Heroism", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Invisibility", + "value": "Gives the drinker the invisible condition for an hour. It ends early if they attack, deal damage, or cast a spell.", + "type": "item", + "title": "Potion of Invisibility", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Invulnerability", + "value": "Gives the drinker resistance to all damage for 1 minute.", + "type": "item", + "title": "Potion of Invulnerability", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Mind Reading", + "value": "Gives the drinker the effect of detect thoughts for 10 minutes.", + "type": "item", + "title": "Potion of Mind Reading", + "description": "2000", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Bird", + "value": "When thrown, spawns a bird like a Roc that can't attack. It can fly and carry a weight of 500 pounds at a max speed of 144 miles per day. It can carry 1000 pounds at half the speed. It disappears after the flight distance is complete.", + "type": "item", + "title": "Quaal's Feather Token of Bird", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Swan Boat", + "value": "Touching a body of water with this feather creates a 50 foot long boat in the shape of a swan. It is self propelled and can be commanded to move and turn. It disappears after 24 hours.", + "type": "item", + "title": "Quaal's Feather Token of Swan Boat", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Quaal's Feather Token of Whip", + "value": "Thrown at a point within 10 feet creates a floating whip that can attack creatures with moderate force damage.", + "type": "item", + "title": "Quaal's Feather Token of Whip", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Animal Influence", + "value": "Can cast animal friendship, fear, or speak with animals. It has 3 charges which are regained slowly every day.", + "type": "item", + "title": "Ring of Animal Influence", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Evasion", + "value": "When the user would otherwise fail at evading an effect with dexterity, a charge can be used to automatically pass. Three charges that are regained slowly every day.", + "type": "item", + "title": "Ring of Evasion", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Feather Falling", + "value": "Whenever the user falls, they only fall at 60 feet per second and don't take fall damage.", + "type": "item", + "title": "Ring of Feather Falling", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Free Action", + "value": "Difficult terrain doesn't affect the wearer of this ring. They cannnot be affect by magic that slows them down, restrains, or paralyzes them.", + "type": "item", + "title": "Ring of Free Action", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Protection", + "value": "The wearer is harder to hit and is more likely succeed in evading effects of all kinds.", + "type": "item", + "title": "Ring of Protection", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Resistance", + "value": "Grants resistance to a specific type of damage.", + "type": "item", + "title": "Ring of Resistance", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Spell Storing", + "value": "Can store up to 5 levels of magic at a time. The user can then cast them at a later time.", + "type": "item", + "title": "Ring of Spell Storing", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of the Ram", + "value": "Can attack a target with force damage in the shape of a ram's head. It can also be used to break a nonmagical object that is not being worn. It has 3 charges that are regained slowly every day.", + "type": "item", + "title": "Ring of the Ram", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of X-ray Vision", + "value": "Grants the user x-ray vision within a range of 30 feet for one minute. Objects appear transparent and don't prevent light from traveling through them. Only lead or thicker substances can block it. Using it more than once a day causes exhaustion.", + "type": "item", + "title": "Ring of X-ray Vision", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Robe of Eyes", + "value": "A robe that grants added perception, darkvision, truesight for 120 feet. Casting light or daylight on the robe blinds the user for 1 minute.", + "type": "item", + "title": "Robe of Eyes", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Rulership", + "value": "Has a high chance of charming a victim. If charmed, the target views you as a trusted leader for 8 hours. Used once per day.", + "type": "item", + "title": "Rod of Rulership", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of the Pact Keeper +2", + "value": "Gives a bonus to spell attacks and allows regaining a spell slot once a day.", + "type": "item", + "title": "Rod of the Pact Keeper +2", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Rope of Entanglement", + "value": "A 30 foot long rope that can dart forward and entangle a target within 20 feet. It can escape with strength or dexterity.", + "type": "item", + "title": "Rope of Entanglement", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Scroll of Protection", + "value": "Protects the user from taking damage from a specific type of creature.", + "type": "item", + "title": "Scroll of Protection", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Shield +2", + "value": "A magically enhanced shield that makes the user harder to hit.", + "type": "armor", + "title": "Shield +2", + "description": "400,+4", + "useForCharacterCreation": false + }, + { + "keys": "Shield of Missile Attraction", + "value": "Gives the user resistance to ranged attacks. It is cursed and makes any creature within 10 feet targeted with a ranged attack instead makes you the target.", + "type": "armor", + "title": "Shield of Missile Attraction", + "description": "4000,+2", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Charming", + "value": "Can be used to cast charm person, command, or comprehend at one charge. Reflect an enchantment spell back at the caster at one charge. It has 10 charges, recharging most charges each day. Once a day it can be used to resist an enchantment.", + "type": "weapon", + "title": "Staff of Charming", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Swarming Insects", + "value": "Can be used to create a swarm of insects in a 30 foot emanation at one charge. It obscures vision. Cast giant insect at four charges, insect plague at 5 charges. It has 10 charges, recharging most charges each day.", + "type": "weapon", + "title": "Staff of Swarming Insects", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of the Woodlands", + "value": "If weilded by a druid, adds magical accuracy and damage to attacks. Spells are more likely to strike their target. It allows the user to cast animal friendship, awaken, barkskin, locate animals or plants, pass without trace, speak with animals, speak with plants, wall of thorns. The staff can be planted which it will grow into a tree. It can then be converted back. Each use uses a charge, 6 total. Regains slowly every day.", + "type": "weapon", + "title": "Staff of the Woodlands", + "description": "4000,1d6+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Withering", + "value": "Using a charge, the staff can deal extra necrotic damage with a chance of causing the target to be at a disadvantage for an hour. 3 charges slowly regained every day.", + "type": "weapon", + "title": "Staff of Withering", + "description": "4000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Stone of Controlling Earth Elementals", + "value": "Can be used to summon an earth elemental when touched to the ground within 30 feet. It obeys the user's commands", + "type": "item", + "title": "Stone of Controlling Earth Elementals", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Sun Blade", + "value": "A sword hilt. When grasped, a blade of pure radiance emerges. It functions as a long sword with increased accuracy and damage. Deals additional damage to the undead. It emits bright light in 15 feet.", + "type": "weapon", + "title": "Sun Blade", + "description": "4000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Sword of Life Stealing", + "value": "If the target is not undead or a construct, the sword deals extra necrotic damage. It gives this extra damage as temporary health to the user.", + "type": "weapon", + "title": "Sword of Life Stealing", + "description": "4000,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Tentacle Rod", + "value": "Allows the user to direct three rubbery tentacles outward as an attack with psychic damage on hit and restraining the target. It continues to take damage until it breaks free.", + "type": "item", + "title": "Tentacle Rod", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Vicious Weapon", + "value": "Deals extra damage to any creature it hits.", + "type": "weapon", + "title": "Vicious Weapon", + "description": "4000,1d8+7,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Binding", + "value": "Can cast spells hold monster (5 charges) or hold person (2 charges). With 7 charges that are regained slowly every day.", + "type": "item", + "title": "Wand of Binding", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Enemy Detection", + "value": "Allows the user to use a charge to notice the direction of where the closest creature that is hostile to you is. With 7 charges that are regained slowly every day.", + "type": "item", + "title": "Wand of Enemy Detection", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Fear", + "value": "Can cast spells command (1 charge) and fear (3 charges). With 7 charges that are regained slowly every day.", + "type": "item", + "title": "Wand of Fear", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Fireballs", + "value": "Used to cast the spell fireball. With 7 charges that regained slowly every day, the user can upcast with up to three charges to increase the spell's level.", + "type": "item", + "title": "Wand of Fireballs", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Lightning Bolts", + "value": "Used to cast the spell Lightning Bolt. With 7 charges that regained slowly every day, the user can upcast with up to three charges to increase the spell's level.", + "type": "item", + "title": "Wand of Lightning Bolts", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Paralysis", + "value": "Fires a thin, blue ray that can cause the paralyzed condition on a target for one minute. The effect remains until the target breaks free. It has 7 charges which are regained slowly every day.", + "type": "item", + "title": "Wand of Paralysis", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Wonder", + "value": "Wand with 7 charges that can be used for various random effects: darkness, faerie fire, fireball, slow, stinking cloud, gust of wind, the user is stunned, the user takes psychic damage, butterflies, lightning bolt, enlarge/reduce, grass, make a rat/elephant/rhinoceros, an object disappears, leaves grow on the target, blinding, invisibility, a stream of gems, polymorph, restrained. Recharges slowly every day.", + "type": "item", + "title": "Wand of Wonder", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Weapon +2", + "value": "A weapon with greater accuracy and damage.", + "type": "weapon", + "title": "Weapon +2", + "description": "4000,1d8+2,+2,strength", + "useForCharacterCreation": false + }, + { + "keys": "Wings of Flying", + "value": "Can be transformed into wings while being worn. They last for 1 hour and allow the user to fly. The user falls if the wings disappear. It takes up to half a day to recharge again.", + "type": "item", + "title": "Wings of Flying", + "description": "4000", + "useForCharacterCreation": false + }, + { + "keys": "Ammunition +3", + "value": "A bundle of ten pieces of magically enhanced ammunition. Has greater accuracy and deals additional damage.", + "type": "item", + "title": "Ammunition +3", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ammunition of Slaying", + "value": "Ammunition that does incredible damage to a specific type of enemy.", + "type": "item", + "title": "Ammunition of Slaying", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Amulet of the Planes", + "value": "Allows the user to plane shift to another known plane. There is a chance that it sends the user and others in 15 feet to a random location in a random plane.", + "type": "item", + "title": "Amulet of the Planes", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Animated Shield", + "value": "When activated, it leaps into the air and hovers to protect the user.", + "type": "armor", + "title": "Animated Shield", + "description": "40000,+2", + "useForCharacterCreation": false + }, + { + "keys": "Armor +2", + "value": "Magically enhanced armor that increases the user's ability to avoid damage.", + "type": "armor", + "title": "Armor +2", + "description": "40000,20", + "useForCharacterCreation": false + }, + { + "keys": "Bag of Devouring", + "value": "Like a bag of holding, but eats animal or vegetable matter when placed inside. There is a 50 percent chance when someone reaches inside that they are pulled into the bag. They need incredible strength to escape or become devoured by the bag.", + "type": "item", + "title": "Bag of Devouring", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Frost Giant Strength", + "value": "Increases strength to above peak human ability.", + "type": "item", + "title": "Belt of Frost Giant Strength", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Stone Giant Strength", + "value": "Increases strength to above peak human ability.", + "type": "item", + "title": "Belt of Stone Giant Strength", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Fire Giant Strength", + "value": "Increases strength to well above peak human ability.", + "type": "item", + "title": "Belt of Fire Giant Strength", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Candle of Invocation", + "value": "Provides dim light and gives advantage with all abilities to creatures within 30 feet. It allows clerics and druids to use level 1 spells without using spell slots. Burns for up to 4 hours. It can instead be used to cast gate to another plane of existence.", + "type": "item", + "title": "Candle of Invocation", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Carpet of Flying", + "value": "A command word activates the carpet allowing whoever/whatever is on top of it to fly and hover.", + "type": "item", + "title": "Carpet of Flying", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Cauldron of Rebirth", + "value": "Allows the user to brew a healing potion once a day. Alternatively, it can be grown to size and have a dead body placed inside. After soaking is salt for 8 hours, the spell raise dead is cast upon it. This can only be done once every 7 days.", + "type": "item", + "title": "Cauldron of Rebirth", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Arachnida", + "value": "Grants the user poison resistance, spider climb, can't be caught in spider webs, and allows the casting of the web spell once a day.", + "type": "item", + "title": "Cloak of Arachnida", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Crystal Ball", + "value": "Allows the casting of the scrying spell.", + "type": "item", + "title": "Crystal Ball", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Dancing Sword", + "value": "Tossing the weapon in the air activates its ability to hover and attack enemies up to four times. It then returns to the hand of the user.", + "type": "weapon", + "title": "Dancing Sword", + "description": "40000,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Demon Armor", + "value": "Gives the user increased ability to avoid attack and speak abyssal. It has clawed gauntlets that allows the user to make damaging unarmed slashing attacks. It is cursed and cannot be removed without a magical spell to undo it. Disadvantage against attacking demons and sustaining their magical effects.", + "type": "armor", + "title": "Demon Armor", + "description": "40000,19", + "useForCharacterCreation": false + }, + { + "keys": "Dragon Scale Mail", + "value": "Made from the scales of a dragon, it is harder to hit the wearer of such armor and added protection against breath attacks from dragons. It has resistance against the type of damage that the dragon scales came from. It allows the user to determine the direction of the closest dragon of this type within 30 miles.", + "type": "armor", + "title": "Dragon Scale Mail", + "description": "40000,19", + "useForCharacterCreation": false + }, + { + "keys": "Dwarven Plate", + "value": "The wearer is harder to hit. Any affect that will move the user against their will can be reduced to 10 feet.", + "type": "armor", + "title": "Dwarven Plate", + "description": "40000,20", + "useForCharacterCreation": false + }, + { + "keys": "Dwarven Thrower", + "value": "A hammer with incredible accuracy and damage. Can be thrown up to 20-60 feet. It has extra damage upon impact from a throw and more so if the target is a giant. The hammer returns to the user's hand.", + "type": "weapon", + "title": "Dwarven Thrower", + "description": "40000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Efreeti Bottle", + "value": "When the stopper is removed, a cloud of thick smoke emerges. It disappears in a puff of fire and an efreeti appears. It may attack the user and disappear, obey your commands for an hour (reusable 3 times), or cast wish once", + "type": "item", + "title": "Efreeti Bottle", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Energy Longbow", + "value": "Magically accurate and does additional damage. It has no string. Making a firing motion with your arm creates a magical arrow of golden energy. The arrow emits bright light in 20 feet. Instead of dealing damage, it can try to restrain the target, teleport the target within 10 feet of you, or produce magical rungs of a ladder in a wall to be climbed.", + "type": "weapon", + "title": "Energy Longbow", + "description": "40000,1d8+1,+1,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Energy Shortbow", + "value": "Magically accurate and does additional damage. It has no string. Making a firing motion with your arm creates a magical arrow of golden energy. The arrow emits bright light in 20 feet. Instead of dealing damage, it can try to restrain the target, teleport the target within 10 feet of you, or produce magical rungs of a ladder in a wall to be climbed.", + "type": "weapon", + "title": "Energy Shortbow", + "description": "40000,1d6+1,+1,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Armor Very Rare", + "value": "Allows the user to cast an advanced spell up to 6 times. It slowly regains charges every day.", + "type": "armor", + "title": "Enspelled Armor Very Rare", + "description": "40000,15+d2", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Weapon Very Rare", + "value": "Allows the user to cast an advanced spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Enspelled Weapon Very Rare", + "description": "40000,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Executioner's Axe", + "value": "Magically conditioned to hit with more accuracy and damage. It deals exceptional damage to humanoids which is then added as extra health to the user.", + "type": "weapon", + "title": "Executioner's Axe", + "description": "40000,1d12+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Obsidian Steed Figurine of Wondrous Power", + "value": "Can be transformed into a Nightmare for 24 hours. It only fights to defend itself. It has a small chance to disobey the users orders. If it is forced to be mounted at such a time, the nightmare reverts to figurine form and both it and the user are transported to hades. It can be used once per 5 days.", + "type": "item", + "title": "Obsidian Steed Figurine of Wondrous Power", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Frost Brand", + "value": "Causes the target to take extra cold damage. Fire has less of an effect on the user. When the temperature is freezing, it emits bright light for 10 feet. Once every hour, brandishing the weapon can be used to extinguish all flames within 30 feet.", + "type": "weapon", + "title": "Frost Brand", + "description": "40000,1d8+4,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Hat of Many Spells", + "value": "Can be used to cast spells by grabbing the spell from within the hat. It can also be used to cast spells the user does not already know. It has a chance to fail causing one of the following effects: casts a random basic spell, or stuns the user, creates a swarm of butterflies, or pulls out a random non-magic item, a gem, or suffer magic sickness, petrified, a friendly creature, swarm of bats, a portal to another plane, or a magic item.", + "type": "item", + "title": "Hat of Many Spells", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Helm of Brilliance", + "value": "A helm adorned with diamonds, opals, and rubies. It emits a 30 foot dim light that damages the undead, causes a weapon to burst into flames that damage enemies, and grants the user resistance to fire damage. Removing a gem allows the user to cast a spell: daylight (opal), Fireball (fire opal), prismatic spray (diamond), or wall of fire (ruby).", + "type": "item", + "title": "Helm of Brilliance", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Bronze Horn of Valhalla", + "value": "Blowing the horn summons 4 berserker warrior spirits for 1 hour that follow your commands only if you are trained with all medium armor. It can be used once every 7 days.", + "type": "item", + "title": "Bronze Horn of Valhalla", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Horseshoes of a Zephyr", + "value": "Can be magically attached to the feet of a horse. This allows the horse to float 4 inches above the surface allowing them to traverse nonsolid or unstable surfaces. It leaves no tracks. It can travel 12 hours a day without exhaustion.", + "type": "item", + "title": "Horseshoes of a Zephyr", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Absorption", + "value": "A magic stone tossed in the air that orbits the user's head and can be used to cancel a spell cast by another creature. It can be used up to 20 times.", + "type": "item", + "title": "Ioun Stone of Absorption", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Agility", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's dexterity.", + "type": "item", + "title": "Ioun Stone of Agility", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Fortitude", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's constitution.", + "type": "item", + "title": "Ioun Stone of Fortitude", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Insight", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's wisdom.", + "type": "item", + "title": "Ioun Stone of Insight", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Intellect", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's intelligence.", + "type": "item", + "title": "Ioun Stone of Intellect", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Leadership", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's charisma.", + "type": "item", + "title": "Ioun Stone of Leadership", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Strength", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's strength.", + "type": "item", + "title": "Ioun Stone of Strength", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Lute of Thunderous Thumping", + "value": "Can be wielded as a club that inflicts thunder damage. A bard can use their charisma to determine how well they hit with this item, but must sing or hum while striking.", + "type": "item", + "title": "Lute of Thunderous Thumping", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Manual of Bodily Health", + "value": "A book of health and nutrition tips which will allow the user to increase their constitution slightly with a maximum beyond normal limits. It loses its magic afterwards. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Manual of Bodily Health", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Manual of Gainful Exercise", + "value": "A book describing fitness exercises which will allow the user to increase their strength slightly with a maximum beyond normal limits. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Manual of Gainful Exercise", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Manual of Golems", + "value": "Describes how to make a golem. It can only be deciphered by a spellcaster or takes psychic damage while reading it. With constant work the reader can make a clay golem in 30 days, flesh golem in 60 days, iron golem in 120 days, and stone goem in 90 days. The manual is consumed in fire to make the golem.", + "type": "item", + "title": "Manual of Golems", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Manual of Quickness of Action", + "value": "A book describing coordination and balance exercises which will allow the user to increase their dexterity slightly with a maximum beyond normal limits. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Manual of Quickness of Action", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Mirror of Life Trapping", + "value": "Once activated by a command word, any creature who sees its own reflection has a chance to be trapped in its extra dimensional cells. They can escape if they have spells that can send them to another plane. Otherwise, the mirror has to be broken in order to free them. The owner can name the creature or cell to call them into the image of the mirror to speak to them or release them.", + "type": "item", + "title": "Mirror of Life Trapping", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Nine Lives Stealer", + "value": "A weapon with increased accuracy and damage. When a critical hit is struck on a weakened foe, it can use a charge to kill them instantly. It can do this up to 9 times.", + "type": "item", + "title": "Nine Lives Stealer", + "description": "40000,1d8+2,+2", + "useForCharacterCreation": false + }, + { + "keys": "Nolzur's Marvelous Pigments", + "value": "Contains a brush and pigments that can be used to paint objects and surfaces within a 20 foot cube. The painting then becomes real. Drawing a door on a wall makes a real door. The items must not be worth more than 25 gold each.", + "type": "item", + "title": "Nolzur's Marvelous Pigments", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Oathbow", + "value": "The bow whispers the phrase \"Swift defeat to my enemies\" when an arrow is knocked. If the phrase \"Swift death tto you who haved wronged me\" is said, it allows the user to mark an enemy as a sworn enemy, giving them a greater chance to hit them and adds additonal damage. When they die, the sworn enemy can be switched to another enemy. All other weapons are at a disadvantage until the enemy is killed or 7 days has passed.", + "type": "weapon", + "title": "Oathbow", + "description": "40000,1d8,0,dexterity", + "useForCharacterCreation": false + }, + { + "keys": "Oil of Sharpness", + "value": "An oil that can coat a melee weapon or ammunition. It makes them have incredible accuracy and damage.", + "type": "item", + "title": "Oil of Sharpness", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Flying", + "value": "Allows the drinker to fly and hover for an hour.", + "type": "item", + "title": "Potion of Flying", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Cloud Giant Strength", + "value": "Increases strength to incredible levels.", + "type": "item", + "title": "Potion of Cloud Giant Strength", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Greater Invisibility", + "value": "Gives the drinker invisiblity for 1 hour.", + "type": "item", + "title": "Potion of Greater Invisibility", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Longevity", + "value": "When drunk, the user's physical age is reduced by 7 to 12 years to a minimum of 13 years old. Each time it is drunk afterward, it has a chance to instead age the person by that amount.", + "type": "item", + "title": "Potion of Longevity", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Speed", + "value": "Gives the drinker the effect of the haste spell without the effect of lethargy at the end of it.", + "type": "item", + "title": "Potion of Speed", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Vitality", + "value": "When drunk, removes all exhaustion and poisoned states. Healing is at its maximum for the next 24 hours.", + "type": "item", + "title": "Potion of Vitality", + "description": "20000", + "useForCharacterCreation": false + }, + { + "keys": "Quarterstaff of the Acrobat", + "value": "A quarterstaff that is magically more accurate and damaging. It can emit dim light for 10 feet. It can extend to a 10 foot pole. The user is more acrobatic. It can deflect an attack once per short rest. It can be used as a thrown weapon and it will immediately return to the user's hand.", + "type": "weapon", + "title": "Quarterstaff of the Acrobat", + "description": "40000,1d6+2,+2,strength", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Regeneration", + "value": "Heals the user by a tiny amount every 10 minutes and even regrows lost limbs.", + "type": "item", + "title": "Ring of Regeneration", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Shooting Stars", + "value": "Casts dancing lights or light. It has 6 charges that can be used to cast faerie fire (1 charge), lightning spheres (2 charges), or shooting stars (1-3 charges)", + "type": "item", + "title": "Ring of Shooting Stars", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Telekenisis", + "value": "Allows the user to cast telekinesis.", + "type": "item", + "title": "Ring of Telekenisis", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Robe of Scintillating Colors", + "value": "Has 3 charges that can be used to activate a dazzling display of lights that creates a 30 foot range of bright light. It can stun enemies.", + "type": "item", + "title": "Robe of Scintillating Colors", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Robe of Stars", + "value": "It gives the user greater ability to avoid effects. It has six large stars that can be removed to cast magic missile. Some of the stars return every day. It allows the user to enter the astral plane and return at will.", + "type": "item", + "title": "Robe of Stars", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Absorption", + "value": "Can absorb a spell attack targeted at the user. The spell is cancelled and the energy is stored in the rod. This energy can be used to cast a spell.", + "type": "item", + "title": "Rod of Absorption", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Alertness", + "value": "Makes the user have greater perception and have more initiative. It can cast detect evil and good, detect magic, detect poison and disease, see invisibility. When planted in the ground, it gives extra protection to those in its glowing radius for 10 minutes once a day.", + "type": "item", + "title": "Rod of Alertness", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Security", + "value": "Activating the rod transports the user and up to 199 other creatures to a demiplane in any form the user chooses (palace, tavern, garden, etc.). Each visitor regains health automatically. The effect lasts up to 200 days. The rod can't be used again until 10 days from then.", + "type": "item", + "title": "Rod of Security", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Rod of the Pact Keeper +3", + "value": "Gives a bonus to spell attacks and allows regaining a spell slot once a day.", + "type": "item", + "title": "Rod of the Pact Keeper +3", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Scimitar of Speed", + "value": "Magically enhanced to be more accurate and damaging. It can make an additional attack every turn.", + "type": "weapon", + "title": "Scimitar of Speed", + "description": "40000,1d6+2,+2,strength", + "useForCharacterCreation": false + }, + { + "keys": "Shield +3", + "value": "Gives an incredible bonus to the user's ability to avoid attack.", + "type": "armor", + "title": "Shield +3", + "description": "40000,+5", + "useForCharacterCreation": false + }, + { + "keys": "Shield of the Cavalier", + "value": "Gives an additional bonus to avoiding attacks. It can be used to make a forceful bash that damages opponents and pushes them away or knocks it prone. It can also create a protective field that prevents attacks and enemies from entering the space around the user or an ally.", + "type": "armor", + "title": "Shield of the Cavalier", + "description": "40000,+4", + "useForCharacterCreation": false + }, + { + "keys": "Spellguard Shield", + "value": "Gives advantage on defending against magical effects and spell attacks.", + "type": "armor", + "title": "Spellguard Shield", + "description": "40000,+2", + "useForCharacterCreation": false + }, + { + "keys": "Spirit Board", + "value": "A wooden board with letters and the words \"yes\", \"no\", \"weal\", \"woe\" written on it. It comes with a heart shaped planchette. It has three charges to cast the spells augury (1 charge) or commune (3 charges). The spirits of the dead answer by guiding the planchette. Recharges slowly every day.", + "type": "item", + "title": "Spirit Board", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Fire", + "value": "Gives the user resistance to fire damage. It has 10 charges to cast the spells burning hands (1 charge), fireball (3 charges), wall of fire (4 charges). The staff regains charges quickly every day. Druid, sorcerer, warlock, or wizard only.", + "type": "weapon", + "title": "Staff of Fire", + "description": "40000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Frost", + "value": "Gives the user resistance to cold damage. It has 10 charges to cast the spells cone of cold (5 charges), fog cloud (1 charge), ice storm (4 charges), wall of ice (4 charges). The staff regains charges quickly every day. Druid, sorcerer, warlock, or wizard only.", + "type": "weapon", + "title": "Staff of Frost", + "description": "40000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Power", + "value": "Quarterstaff with extra damage and accuracy. It improves the ability to avoid being hit. With 20 charges it can cast cone of cold (5) fireball (5) globe of invulnerability (6) hold monster (5) levitate (2) lightning bolt (5) magic missile (1) ray of enfeeblement (1), wall of force (5). Breaking the staff creates an incredible explosion with a chance of transporting the user away to avoid injury. It regains charges quickly every day. Sorcerer, warlock, wizard only.", + "type": "weapon", + "title": "Staff of Power", + "description": "40000,1d6+2,+2,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Striking", + "value": "Quarterstaff with incredible accuracy and damage. With 10 charges, up to 3 can be used to give extra force damage. It quickly regains charges every day.", + "type": "weapon", + "title": "Staff of Striking", + "description": "40000,1d6+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Staff of Thunder and Lightning", + "value": "Strikes with magical accuracy and damage and hits with extra lightning damage or stuns the enemy once per day. It can create a lightning strike that does severe damage in a line once per day. It can defean creatures within 60 feet and cause thunder damage once per day.", + "type": "weapon", + "title": "Staff of Thunder and Lightning", + "description": "40000,1d6+9,+2,strength", + "useForCharacterCreation": false + }, + { + "keys": "Sword of Sharpness", + "value": "Inflicts maximum damage against objects. On a critical hit, this weapon does even more slashing damage and causes the target to be exhausted.", + "type": "weapon", + "title": "Sword of Sharpness", + "description": "40000,1d8,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Thunderous Greatclub", + "value": "Increases the strength of the user to 20. It deals extra thunder damage to creatures and extreme damage to objects. It can create a thunderclap creating a 30 foot cone of thunder damage. Once per day, can be struck on the ground to create a seismic disturbance, causing incredible damage to structures and knocking creatures prone. Creates a 30 foot dep fissure.", + "type": "weapon", + "title": "Thunderous Greatclub", + "description": "40000,1d8+5,strength", + "useForCharacterCreation": false + }, + { + "keys": "Tome of Clear Thought", + "value": "A book of memory and logic exercises which will allow the user to increase their intelligence slightly with a maximum beyond normal limits. It loses its magic afterwards. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Tome of Clear Thought", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Tome of Leadership and Influence", + "value": "A book with guidelines for influencing and charming others which will allow the user to increase their charisma slightly with a maximum beyond normal limits. It loses its magic afterwards. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Tome of Leadership and Influence", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Tome of Understanding", + "value": "A book of intuition and insight exercises which will allow the user to increase their wisdom slightly with a maximum beyond normal limits. It loses its magic afterwards. The user must spend 48 hours over a period of 6 days or fewer to gain the benefit.", + "type": "item", + "title": "Tome of Understanding", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Polymorph", + "value": "It has 7 charges that can be used to cast polymorph. It regains these charges quickly every day.", + "type": "item", + "title": "Wand of Polymorph", + "description": "40000", + "useForCharacterCreation": false + }, + { + "keys": "Weapon +3", + "value": "A weapon with incredible accuracy and damage.", + "type": "weapon", + "title": "Weapon +3", + "description": "40000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Apparatus of Kwalish", + "value": "A large vehicle with various levers that control its movements. It has mechanical legs, claws, shutters, eyes that emit light, a hatch. It can grapple, attack, swim, walk.", + "type": "item", + "title": "Apparatus of Kwalish", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Armor +3", + "value": "Magically enhanced armor that causes the user to be incredibly hard to hit.", + "type": "armor", + "title": "Armor +3", + "description": "200000,21", + "useForCharacterCreation": false + }, + { + "keys": "Armor of Invulnerability", + "value": "Resists bludgeoning, piercing, and slashing damage. Once a day, it can form a metal shell for 10 minutes that gives immunity to these damage types.", + "type": "armor", + "title": "Armor of Invulnerability", + "description": "200000,18", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Cloud Giant Strength", + "value": "Increases strength to near impossible levels.", + "type": "item", + "title": "Belt of Cloud Giant Strength", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Belt of Storm Giant Strength", + "value": "Increases strength to impossible levels.", + "type": "item", + "title": "Belt of Storm Giant Strength", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Cloak of Invisibility", + "value": "The cloak has 3 charges which can be used to give the invisible condition for 1 hour. It regains the charges slowly each day.", + "type": "item", + "title": "Cloak of Invisibility", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Crystal Ball of Mind Reading", + "value": "Allows casting of the scrying spell. Allows the user to cast detect thoughts on targets within 30 feet of the spell's sensor.", + "type": "item", + "title": "Crystal Ball of Mind Reading", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Crystal Ball of Telepathy", + "value": "Allows casting of the scrying spell. Allows telepathic communication or cast suggestion with targets within 30 feet of the spell's sensor.", + "type": "item", + "title": "Crystal Ball of Telepathy", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Crystal Ball of True Seeing", + "value": "Allows casting of the scrying spell. Grants truesight with a range of 120 feet centered on the spell's sensor.", + "type": "item", + "title": "Crystal Ball of True Seeing", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Cubic Gate", + "value": "3 inch cube with each side keyed to a different plane. With 3 charges it can cast gate or plane shift to the associated plane pressed.", + "type": "item", + "title": "Cubic Gate", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Deck of Many Things", + "value": "A deck of cards that unleashes wonderful or terrible magical effects when randomly drawn: balance, comet, donjon, euryale, fates, flames, fool, gem, jester, key, knight, moon, puzzle, rogue, ruin, sage, skull, star, sun, talons, throne, void.", + "type": "item", + "title": "Deck of Many Things", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Defender", + "value": "Has incredible accuracy and damage. Instead, that bonus can be used to have a better chance of avoiding damage at the user's discretion.", + "type": "weapon", + "title": "Defender", + "description": "200000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Efreeti Chain", + "value": "Has incredible bonus to avoiding attacks. Has immunity to fire damage and the user can speak primordial. Allows the user to stand and walk across molten lava.", + "type": "armor", + "title": "Efreeti Chain", + "description": "200000,19", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Armor Legendary", + "value": "Allows the user to cast a highly advanced spell up to 6 times. It slowly regains charges every day.", + "type": "armor", + "title": "Enspelled Armor Legendary", + "description": "200000,15+d2", + "useForCharacterCreation": false + }, + { + "keys": "Legendary Enspelled Staff", + "value": "Allows the user to cast a highly advanced spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Legendary Enspelled Staff", + "description": "200000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Enspelled Weapon Legendary", + "value": "Allows the user to cast a highly advanced spell up to 6 times. It slowly regains charges every day.", + "type": "weapon", + "title": "Enspelled Weapon Legendary", + "description": "200000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Hammer of Thunderbolts", + "value": "Has enhanced damage and accuracy. With 5 charges, it can be thrown expending a charge to possibly stun all creatures within 30 feet of the target, then flies back to the user's hand. It has synergy with the belt of giant strength or gauntlets of ogre power. Critical hits on a giant can possibly kill it instantly. The strength bestowed increases by 4.", + "type": "weapon", + "title": "Hammer of Thunderbolts", + "description": "200000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Holy Avenger", + "value": "Must be used by a paladin, has incredible damage and accuracy. Deals extra radiant damage to the undead. Allies within 10 feet have extra protection against magical effects.", + "type": "weapon", + "title": "Holy Avenger", + "description": "200000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Greater Absorption", + "value": "A magic stone tossed in the air that orbits the user's head and can cancel a spell cast by a creature that can be seen by the user. It can only do this 20 times.", + "type": "item", + "title": "Ioun Stone of Greater Absorption", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Mastery", + "value": "A magic stone tossed in the air that orbits the user's head and increases the user's proficiency.", + "type": "item", + "title": "Ioun Stone of Mastery", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ioun Stone of Regeneration", + "value": "A magic stone tossed in the air that orbits the user's head and regain health every hour.", + "type": "item", + "title": "Ioun Stone of Regeneration", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Iron Flask", + "value": "If a creature is not native to the plane it's on, it can potentially be captured within this flask wihin 60 feet. Removing the stopper of the flask unleashes the creature. It obeys the user's commands for an hour. It may already contain a creature when discovered.", + "type": "item", + "title": "Iron Flask", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Iron Horn of Valhalla", + "value": "Blowing the horn summons 5 berserker warrior spirits for 1 hour that follow your commands only if you are proficient with all martial weapons. It can be used once every 7 days.", + "type": "item", + "title": "Iron Horn of Valhalla", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Luck Blade", + "value": "Has additional accuracy and damage. It also has added ability to avoid magical effects. It gives the user another chance when they would have otherwise failed a task. It can use this once per day. It can grant up to 3 wishes, one per day.", + "type": "weapon", + "title": "Luck Blade", + "description": "200000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Moonblade", + "value": "Weapon that chooses a wielder whose goals align with its own, otherwise cursing the user. Each potential rune on it gives a magical property: increased accuracy/damage, deals extra force damage, can be thrown, easier critical hits, flashes to cause blindness, can store spells, and can summon a shadowy elf. It has sentience and personality.", + "type": "weapon", + "title": "Moonblade", + "description": "200000,1d8+1,+1,strength", + "useForCharacterCreation": false + }, + { + "keys": "Plate Armor of Etherealness", + "value": "Can cast the etherealness spell once a day.", + "type": "armor", + "title": "Plate Armor of Etherealness", + "description": "200000,18", + "useForCharacterCreation": false + }, + { + "keys": "Potion of Storm Giant Strength", + "value": "Increases strength to impossible levels.", + "type": "item", + "title": "Potion of Storm Giant Strength", + "description": "100000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Djinni Summoning", + "value": "Can summon a djinni for up to an hour. It obeys the user's commands. It can be summoned once a day unless it dies.", + "type": "item", + "title": "Ring of Djinni Summoning", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Elemental Command", + "value": "Linked to a single elemental force (air, earth, fire, water). Gives advantage against elementals of that type. It can potentially compell elementals of that type to follow your command. It has 5 charges used to cast elemental spells. It also has additional elemental properties.", + "type": "item", + "title": "Ring of Elemental Command", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Invisibility", + "value": "Can make the user invisible at will.", + "type": "item", + "title": "Ring of Invisibility", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Spell Turning", + "value": "Creates magical protection against spell effects. It can possibly reflect spells back at the user that casted them if they only target you.", + "type": "item", + "title": "Ring of Spell Turning", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Ring of Three Wishes", + "value": "Can cast up to three wishes where it will then cease to be magical.", + "type": "item", + "title": "Ring of Three Wishes", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Robe of the Archmagi", + "value": "Must be used by a sorcerer, warlock, or wizard. Gives magical armor, resists magical effects, and increases the difficulty of having the user's effects resisted.", + "type": "armor", + "title": "Robe of the Archmagi", + "description": "200000,15+d", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Lordly Might", + "value": "Functions as a mace with incredible damage and accuracy and has a series of buttons that transform it: fiery blade, great axe, spear, climbing pole, battering ram, back to a mace that indicates magnetic north. It can drain life, paralyze its targets, or terrify them.", + "type": "weapon", + "title": "Rod of Lordly Might", + "description": "200000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Rod of Resurrection", + "value": "With 5 charges to cast heal (1 charge), or resurrection (5 charges). It slowly regains charges every day.", + "type": "item", + "title": "Rod of Resurrection", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Scarab of Protection", + "value": "It gives the user additional ability to avoid attacks, it has 12 charges to prevent the effects of necromancy, it has added ability to avoid spell effects.", + "type": "item", + "title": "Scarab of Protection", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Scroll of Titan Summoning", + "value": "Reading this scroll summons a titan in a position the user can see in 1 mile. It can be animal lord, blob of annihilation, colossus, elemental cataclysm, empyrean, kraken, or tarrasque. It is hostile to all creatures.", + "type": "item", + "title": "Scroll of Titan Summoning", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Sovereign Glue", + "value": "Can form a permanent bond between two objects with one ounce of liquid. It is contained in a vial coated with oil of slipperiness. It takes a minute to cure. The bond can only be broken with universal solvent, oil of etherealness, or the wish spell. Contains up to 7 ounces.", + "type": "item", + "title": "Sovereign Glue", + "description": "100000", + "useForCharacterCreation": false + }, + { + "keys": "Sphere of Annihilation", + "value": "Obliterates all matter that passes through it except artifacts. Causes extreme damage to larger objects or creatures that touch it. Only one who has exceptional arana ability has a chance of commanding it to move. Failure to control it causes it to move toward the person. Passing the sphere through a planar portal can possibly destroy it or send all creatures within 180 feet to a random plane.", + "type": "item", + "title": "Sphere of Annihilation", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Staff of the Magi", + "value": "Must be used by a sorcerer, warlock, or wizard. It has 50 charges that can be used to cast arcane lock, conjure elemental, detect magic, dispel magic. enlarge, reduce, fireball, flaming sphere4, ice storm, invisibility, knock, light, lightning bolt, mage hand, passwall, plane shift, protection from evil and good, telekinesis, wall of fire, web. It has a chance of cancelling other creature's spells and absorbing its power. Absorbing too much power may make it explode.", + "type": "weapon", + "title": "Staff of the Magi", + "description": "200000,1d6,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Sword of Answering", + "value": "Weapon has incredible accuracy and damage. If an enemy attacks the user, the user can achieve a free attack against that enemy. The special attack ignores immunity or resistance.", + "type": "weapon", + "title": "Sword of Answering", + "description": "200000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Talisman of Pure Good", + "value": "Must be used by a cleric or paladin. If a fiend or undead touches the talisman, they take incredible radiant damage and continue to take damage as long as they hold it. It increases the accuracy of spells. With 7 charges, it can open a fissure underneath a creature on the ground. If they fall in, they are destroyed forever. If not, they take psychic damage. Using the last charge destroys the talisman.", + "type": "item", + "title": "Talisman of Pure Good", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Talisman of the Sphere", + "value": "Gives the user advantage on arcana checks when attempting to control the sphere of annihilation. It allows movement of the sphere an additional distance based on the user's intelligence.", + "type": "item", + "title": "Talisman of the Sphere", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Talisman of Ultimate Evil", + "value": "Represents pure evil. Anyone holding it who is not a fiend or undead will take incredible damage until it releases it. It increases the accuracy of spells. It has 6 charges that can be used to open a flaming fissure underneath a target. If they fall in, they are destroyed forever. Otherwise they take psychic damage. Using the last charge destroys the talisman.", + "type": "item", + "title": "Talisman of Ultimate Evil", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Tome of the Stilled Tongue", + "value": "An evil book with a tongue pinned to the front. The first few pages are filled with indecipherable scrawls. Once a day, any spells written in the rest of the pages can be cast for free and with relative ease. Removing the tongue erases the spells. Cryptic messages are sometimes written on the page and are erased as soon as they are read.", + "type": "item", + "title": "Tome of the Stilled Tongue", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Universal Solvent", + "value": "A strong alcohol smelling milky liquid that contains up to 7 ounces of fluid. It instantly dissolves any adhesive including sovereign glue.", + "type": "item", + "title": "Universal Solvent", + "description": "100000", + "useForCharacterCreation": false + }, + { + "keys": "Well of Many Worlds", + "value": "Folded like a hankerchief, it can be unfolded and placed as a two way portal into another world or plane of existence. It can be closed but can't be used again until up to 8 hours later.", + "type": "item", + "title": "Well of Many Worlds", + "description": "200000", + "useForCharacterCreation": false + }, + { + "keys": "Axe of the Dwarvish Lords", + "value": "An artifact once known to bring peace to the dwarvish people. It has incredible damage and accuracy. Critical hits do additional slashing damage. Throwing the axe deals additional force damage and more so on giants. It returns to the user's hand. It grants darkvision, increases constitution, grants proficiency in brewing/masonry/smithing, immunity to poison, resistance to fire, destroy objects easily, teleport through stone once every 3 days. The user can summon an earth elemental once a day.", + "type": "weapon", + "title": "Axe of the Dwarvish Lords", + "description": "5000000,1d12+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Blackrazor", + "value": "A great sword with incredible damage and accuracy. Hitting an undead harms the user and heals the undead. Cannot be charmed or frightened. Blindsight for 30 feet. Killing a creature grants the user additional health equal to that of the enemy slain. It is sentient and may choose to cast haste on the user. It demands the devouring of souls at least every 3 days or it will raise conflict with the user.", + "type": "weapon", + "title": "Blackrazor", + "description": "5000000,2d6+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Book of Exalted Deeds", + "value": "A guide on what is true virtue. The user must read and study 80 hours to fully absorb its contents. Fiends and the undead are harmed by reading this book. It grants immunity to the charmed and frightened conditions. Resistance to psychic damage. Wisdom increases slightly. Spells are casted at one higher level. Emit bright light as a halo within 10 feet. This gives advantage when persuading people. Fiends and undead have trouble hitting the user. These benefits only last if the user strives to do good.", + "type": "item", + "title": "Book of Exalted Deeds", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Book of Vile Darkness", + "value": "Even reading the pages of this vile book invite doom. Whenever it is read by a creature other than an undead or fiend, there is a chance that they are transformed into a larva which can only be undone by the wish spell. Reading and studying it for a total of 80 hours unlocks its power and must be maintained by doing evil acts. It increases a user's ability by taking away from another. Immunity to exhaustion. Can cast animate dead, circle of death, dominate monster, finger of death. It's a reference for any aspect of evil. Reading words aloud damages the reader as well as other creatures in the area with psychic damage.", + "type": "item", + "title": "Book of Vile Darkness", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Demonomicon of Iggwilv", + "value": "An accounting of the abyss and demons. It contains demonic secrets and contains a part of the abyss that keeps it up to date. The first ten pages are blank which can be used to capture a fiend trapped in a magic circle spell once a day. Magic circle is cast at its highest level when used against fiends. Spells against fiends do the most damage possible. With 8 charges, it can cast magic circle, magic jar, planar ally, planar binding, plane shift to the abyss, summon fiend, tasha's hideous laughter. ", + "type": "item", + "title": "Demonomicon of Iggwilv", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Eye of Vecna", + "value": "An artifact which consists of what is left behind by the evil lich Vecna. It must be applied to the user's empty eye socket which can never be removed or the user will die. It grants true sight and has 8 charges to cast clairvoyance(2) crown of madness(1) disintegrate(4) dominate monster(5) eyebite (4). The charges are regained quickly every day but each spell has a chance of devouring the user's soul. xray vision for 1 minute.", + "type": "item", + "title": "Eye of Vecna", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Hand of Vecna", + "value": "An artifact which consists of what is left behind by the evil lich Vecna. It must be applied to the user's stump where the left hand was. It increases strength to the peak of human ability. Melee attacks and spells deal extra cold damage. It has 8 charges to cast finger of death(5) sleep(1) slow(2) teleport(3). It regains charges daily, but using such spells may cause it to cast suggest to make the user do an evil deed. It grants greater initiative, can reduce targets to slime, immunity to poison, and regenerates health. Can cast wish once every 30 days.", + "type": "item", + "title": "Hand of Vecna", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Orb of Dragonkind", + "value": "Forged to defeat dragons, the orb contains the essence of an evil dragon. The user must have incredible force of personality or be controlled by it. It has 7 charges that allows the user to cast cure wounds(4) daylight(1) death ward(2) detect magic(0) scrying(3). It recharges slowly daily. It can issue a telepathic call to chromatic dragons in 40 miles and it will compell them to come to the user.", + "type": "item", + "title": "Orb of Dragonkind", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Sword of Kas", + "value": "Once used to defeat the lich Vecna, it grants the user a bloodthirst that must be satiated within 1 minute of the weapon being drawn. If not, the user may take psychic damage or become dominated by the sword until the blood lust is satiated. It has incredible accuracy and damage. Critical hits deal extra damage to undead. It can cast call lightning, divine word, finger of death once a day. Grants greater initiative, could be used to give greater defense, resistance to necrotic damage. It's sentient and chaotic evil. However, its goal is to destroy Vecna and all his works.", + "type": "weapon", + "title": "Sword of Kas", + "description": "5000000,2d6+3,0,strength", + "useForCharacterCreation": false + }, + { + "keys": "Wand of Orcus", + "value": "Crafted by Orcus, demon prince of the abyss. Trying to attune to the wand causes incredible necrotic damage or simply becomes a zombie. It can wielded as a mace with excellent accuracy and damage and deals extra necrotic damage. It provides additional ability to avoid attacks. It has 7 charges to cast spells: animate dead(1) blight(2) circle of death(3) finger of death(3) power word kill(4) speak with dead(1). It regains charges slowly every day. It can conjure 15 skeletons and 15 zombies once per day. It is sentient and desires to slay everything in the multiverse.", + "type": "item", + "title": "Wand of Orcus", + "description": "5000000", + "useForCharacterCreation": false + }, + { + "keys": "Wave", + "value": "Has incredible accuracy and damage. The target takes extra necrotic damage on hit. It gives the user extra initiative and a buble of air to breathe while underwater. It has three charges to cast dominate beast on swimming creatures. It regains charges slowly every day. It can cast globe of invulnerability once every day. It's sentient and wants its users to embrace sea gods.", + "type": "weapon", + "title": "Wave", + "description": "5000000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Whelm", + "value": "A warhammer that has incredible damage and accuracy. It can be hurled to cause extra force damage and even more so against constructs, elementals, or giants. It flies back to the user's hand. Once a day, it can create a shock wave when struck on the ground causing creatures within 60 feet to be stunned. It alerts its user of secret or concealed doors within 30 feet. It can caast detect evil and good or locate object once a day. It is sentient and longs to returned to its dwarf clan.", + "type": "weapon", + "title": "Whelm", + "description": "5000000,1d8+3,+3,strength", + "useForCharacterCreation": false + }, + { + "keys": "Acid Splash", + "value": "Spell that creates a five foot acid bubble within fifty feet. It explodes, causing acid damage.", + "type": "spell", + "title": "Acid Splash", "description": "", "useForCharacterCreation": false }, { - "keys": "Ray of Frost", - "value": "Casts a beam of blue-white light that causes cold damage on a given target within range. The target is then slowed for a short period of time.", + "keys": "Aid spell, cast Aid", + "value": "Spell that increases the health and maximum health of up to 3 different creatures.", "type": "spell", - "title": "Ray of Frost", + "title": "Aid", "description": "", "useForCharacterCreation": false }, { - "keys": "Magic Missile", - "value": "Summon a number of darts comprised of pure magical force that can each target a creature that you can see within range, striking all at once.", + "keys": "Alarm spell, cast Alarm", + "value": "Spell that creates a magical alarm that alerts the caster that there is an intrusion in a twenty foot area.", "type": "spell", - "title": "Magic Missile", + "title": "Alarm", "description": "", "useForCharacterCreation": false }, { - "keys": "Mage Hand", - "value": "Creates an ethereal hand within range that will obey the commands of its caster. It must remain within range or it will disappear.", + "keys": "Alter Self", + "value": "Spell that changes the caster to be able to breath underwater, change their appearance, or grow natural weapons like claws or fangs.", "type": "spell", - "title": "Mage Hand", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Cure Wounds", - "value": "Heals a creature you can touch.", - "type": "spell", - "title": "Cure Wounds", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Fire Bolt", - "value": "Summons a mote of fire and casts it at a creature within range. It causes a moderate amount of fire damage.", - "type": "spell", - "title": "Fire Bolt", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Divine Favor", - "value": "A prayer that calls on divinity and gives the caster divine radiance. Subsequent attacks will cause radiant damage.", - "type": "spell", - "title": "Divine Favor", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Thunderous Smite", - "value": "Adds lightning damage to your attack and amplifies the attack's effectiveness.", - "type": "spell", - "title": "Thunderous Smite", + "title": "Alter Self", "description": "", "useForCharacterCreation": false }, { "keys": "Animal Friendship", - "value": "Compels a beast to see that you mean no harm to it. The spell is ended if the beast is harmed.", + "value": "Spell that compels a beast to see that you mean no harm to it. The spell is ended if the beast is harmed.", "type": "spell", "title": "Animal Friendship", "description": "", "useForCharacterCreation": false }, { - "keys": "Druidcraft", - "value": "Allows the caster to predict the weather, make plants bloom, or create a harmless sensory effect like a puff of wind or the smell of a dead animal.", + "keys": "Animal Messenger", + "value": "Spell that allows the caster to choose a tiny beast that will travel and communicate a simple message to a person of their choosing.", "type": "spell", - "title": "Druidcraft", + "title": "Animal Messenger", "description": "", "useForCharacterCreation": false }, { - "keys": "Healing Word", - "value": "Heal a creature within range.", + "keys": "Animal Shapes", + "value": "Spell that transforms any willing creatures into beasts of the caster's choosing.", "type": "spell", - "title": "Healing Word", + "title": "Animal Shapes", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Animate Dead", + "value": "Spell that tranforms a dead corpse or pile of bones into an undead creature. This could be a zombie or skeleton, respectively.", + "type": "spell", + "title": "Animate Dead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Animate Objects", + "value": "Spell that transforms an inanimate object into a living creature that follows your commands.", + "type": "spell", + "title": "Animate Objects", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Antilife Shell", + "value": "Spell that prevents living creatures from entering a 10 foot sphere emanating from the caster. It has no affect on constructs or undead.", + "type": "spell", + "title": "Antilife Shell", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Antimagic Field", + "value": "Spell that creates a ten foot antimagic emanation from the caster which prevents magic effects from working inside it.", + "type": "spell", + "title": "Antimagic Field", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Antipathy", + "value": "Spell that causes a creature to be frightened. It must move as far away from the caster as possible.", + "type": "spell", + "title": "Antipathy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Antipathy/Sympathy", + "value": "Spell that causes a creature to be frightened must move as far away from the caster as possible or to be charmed and must move as close as possible to the caster.", + "type": "spell", + "title": "Antipathy/Sympathy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Arcane Eye", + "value": "Spell creates an invisible eye which allows the caster to see from that eye in every direction. The eye can be moved.", + "type": "spell", + "title": "Arcane Eye", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Arcane Gate", + "value": "Spell creates two connecting portals. One within ten feet and the other up to five hundred feet within visual range. Misty glowing rings floating above ground.", + "type": "spell", + "title": "Arcane Gate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Arcane Lock", + "value": "Spell creates a magic lock that cannot be lockpicked or opened in any nonmagical way.", + "type": "spell", + "title": "Arcane Lock", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Arcane Vigor", + "value": "Spell allows the caster to heal a minor amount of health.", + "type": "spell", + "title": "Arcane Vigor", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Armor of Agathys", + "value": "Spell creates magical frost armor for the caster. Any melee attackers will take cold damage.", + "type": "spell", + "title": "Armor of Agathys", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Arms of Hadar", + "value": "Spell creates tendrils emanating from the caster up to ten feet in length. Each tendril causes necrotic damage.", + "type": "spell", + "title": "Arms of Hadar", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Astral Projection", + "value": "Spell projects the caster and other companions to the astral plane. Their real bodies are in a state of suspended animation.", + "type": "spell", + "title": "Astral Projection", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Augury", + "value": "Spell allows caster to receive an omen from an otherworldly entity about an action you are about to take within thirty minutes.", + "type": "spell", + "title": "Augury", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Aura of Life", + "value": "Spell creates a thirty foot emanation that gives resistance to necrotic damage.", + "type": "spell", + "title": "Aura of Life", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Aura of Purity", + "value": "Spell creates a thirty foot emanation that gives resistance to poison damage.", + "type": "spell", + "title": "Aura of Purity", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Aura of Vitality", + "value": "Spell creates a thirty foot emanation that heals an ally for a minor amount repeatedly for a period of time.", + "type": "spell", + "title": "Aura of Vitality", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Awaken spell, cast Awaken", + "value": "Spell that can be casted on a beast or a plant which gives it intelligence enough to speak and move. The target is charmed towards the caster.", + "type": "spell", + "title": "Awaken", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Bane spell, cast Bane", + "value": "Spell that causes the target to miss their attacks more often.", + "type": "spell", + "title": "Bane", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Banishing Smite", + "value": "Spell that causes an attack to cause extra force damage. A sufficiently damaged target is sent to a demiplane where they are incapacitated for a time.", + "type": "spell", + "title": "Banishing Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Banishment", + "value": "Spell that causes the target to be transported to a demiplane where they are incapacitated for a time.", + "type": "spell", + "title": "Banishment", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Barkskin", + "value": "Spell that causes the target's skin to transform into a barklike appearance. This increases their ability to prevent damage.", + "type": "spell", + "title": "Barkskin", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Beacon of Hope", + "value": "Spell that causes any number of targets in range to be wiser and more resiliant when fallen in battle.", + "type": "spell", + "title": "Beacon of Hope", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Beast Sense", + "value": "Spell that allows you to see through eyes of a beast you can touch and use any senses it has.", + "type": "spell", + "title": "Beast Sense", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Befuddlement", + "value": "Spell that causes the target is unable to cast spells and takes incredible psychic damage.", + "type": "spell", + "title": "Befuddlement", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Bestow Curse", + "value": "Spell when target is touched, it is cursed with disadvantage in one ability.", + "type": "spell", + "title": "Bestow Curse", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Bigby's Hand", + "value": "Spell that creates a magical hand that moves and attacks at your will. It is able to grab, push, punch, and provide cover.", + "type": "spell", + "title": "Bigby's Hand", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blade Barrier", + "value": "Spell that creates a wall of whirling blades that damage opponents that enter its space.", + "type": "spell", + "title": "Blade Barrier", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blade Ward", + "value": "Spell that causes attackers to have difficulty targeting the caster.", + "type": "spell", + "title": "Blade Ward", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Bless spell, cast Bless", + "value": "Spell that causes up to three targets have better ability to target enemies and resist the enemies' effects.", + "type": "spell", + "title": "Bless", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blight spell, cast Blight", + "value": "Spell that causes the target to take necrotic damage. Nonmagical plant targets instantly wither and die.", + "type": "spell", + "title": "Blight", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blinding Smite", + "value": "Spell that causes radiant damage and blinds the target.", + "type": "spell", + "title": "Blinding Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blindness spell, cast Blindness", + "value": "Spell that blinds the target.", + "type": "spell", + "title": "Blindness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blindness/Deafness", + "value": "Spell that blinds or deafens the target.", + "type": "spell", + "title": "Blindness/Deafness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blink spell, cast Blink", + "value": "Spell with a chance to cause the caster to enter the ethereal plane at the end of their turn, preventing them from being targeted by attacks.", + "type": "spell", + "title": "Blink", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Blur spell, cast Blur", + "value": "Spell that causes you to appear blurred to others. This helps prevent the caster from being targeted by attacks.", + "type": "spell", + "title": "Blur", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Burning Hands", + "value": "Spell that causes flames to shoot from your hands causing damage in a cone in front of the caster.", + "type": "spell", + "title": "Burning Hands", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Call Lightning", + "value": "Spell that causes a lighting bolt to strike within a 60 foot radius once at the end of every turn.", + "type": "spell", + "title": "Call Lightning", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Calm Emotions", + "value": "Spell that causes the target to no longer be hostile towards creatures of the caster's choosing.", + "type": "spell", + "title": "Calm Emotions", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Chain Lightning", + "value": "Spell that shoots a lightning bolt that travels from one target to another in close range.", + "type": "spell", + "title": "Chain Lightning", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Charm Monster", + "value": "Spell that causes any sort of creature to be friendly to you.", + "type": "spell", + "title": "Charm Monster", "description": "", "useForCharacterCreation": false }, { "keys": "Charm Person", - "value": "Enchant a humanoid within range to consider the caster as a friend for a time or until it is injured. After the spell ends, the target will know that it was charmed.", + "value": "Spell to enchant a humanoid within range to consider the caster as a friend for a time or until it is injured. After the spell ends, the target will know that it was charmed.", "type": "spell", "title": "Charm Person", "description": "", "useForCharacterCreation": false }, { - "keys": "Vicious Mockery", - "value": "Insult the target with a barrage of abusive language and subtle enchantments. If it can hear you, it will receive psychic damage and will be at a disadvantage for the next attack.", + "keys": "Chill Touch", + "value": "Spell casted when the caster takes a melee attack against a target. It causes necrotic damage.", "type": "spell", - "title": "Vicious Mockery", + "title": "Chill Touch", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Chromatic Orb", + "value": "Spell that launches a magical orb of acid, cold, fire, lightning, poison, or thunder damage at a target. It has a chance to leap to other targets in close range.", + "type": "spell", + "title": "Chromatic Orb", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Circle of Death", + "value": "Spell that causes necrotic damage in a sixty foot radius.", + "type": "spell", + "title": "Circle of Death", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Circle of Power", + "value": "Spell that causes allies to have advantage on all magical effects while within range.", + "type": "spell", + "title": "Circle of Power", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Clairvoyance spell, cast Clairvoyance", + "value": "Spell that creates an invisible sensor in a space. The target is able to see or hear through that sensor as if they were there.", + "type": "spell", + "title": "Clairvoyance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Clone spell, cast Clone", + "value": "Spell that creates a duplicate of another creature. It is grown in an expensive vessel and takes a long time to produce. If the target ever dies, their soul is transferred to the clone.", + "type": "spell", + "title": "Clone", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Cloudkill", + "value": "Spell that creates a sphere of yellowish green fog that causes poison damage. It moves away from the caster by 10 feet per turn.", + "type": "spell", + "title": "Cloudkill", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Cloud of Daggers", + "value": "Spell that creates a cloud of spinning daggers that causes slashing damage to any creatures that enter the space.", + "type": "spell", + "title": "Cloud of Daggers", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Color Spray", + "value": "Spell that creates a magical display of lights in a cone in front of the caster. It may blind enemies.", + "type": "spell", + "title": "Color Spray", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Command spell, cast Command", + "value": "Spell that causes the target to either approach, drop what they're holding, flee, grovel, or halt immediately.", + "type": "spell", + "title": "Command", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Commune spell, cast Commune", + "value": "Spell that allows the caster to speak to a divine entity to ask up to three questions.", + "type": "spell", + "title": "Commune", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Commune with Nature", + "value": "Spell that allows the caster to speak with the nature spirits to gain knowledge. Reveals locations, portals, prevalent plants beasts, bodies of water, and celestial, elemental, fey, fiend, or undead.", + "type": "spell", + "title": "Commune with Nature", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Compelled Duel", + "value": "Spell that causes enemy to target the caster specifically. It has disadvantage targeting anything else.", + "type": "spell", + "title": "Compelled Duel", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Comprehend Languages", + "value": "Spell that allows the caster to understand any language seen or heard. The caster must be touching the surface of a written word to understand it. It does not decode cipher.", + "type": "spell", + "title": "Comprehend Languages", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Compulsion spell, cast Compulsion", + "value": "Spell that charms a target, allowing the caster to compel them to move safely left or right.", + "type": "spell", + "title": "Compulsion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Cone of Cold", + "value": "Spell that causes cold damage in a cone in front of the caster.", + "type": "spell", + "title": "Cone of Cold", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Confusion spell, cast Confusion", + "value": "Spell that causes enemies in a ten foot area to do random things such as move in a random direction, stand still, or attack a random creature.", + "type": "spell", + "title": "Confusion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Animals", + "value": "Spell that creates an intangible pack of animals. Whenever an enemy enters this area they take slashing damage.", + "type": "spell", + "title": "Conjure Animals", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Barrage", + "value": "Spell that creates spectral weapons or ammunition that assaults creatures in a 60 foot cone with force damage.", + "type": "spell", + "title": "Conjure Barrage", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Celestial", + "value": "Spell that summons a celestial that appears as a pillar of light. It can heal allies or sear enemies with radiant damage.", + "type": "spell", + "title": "Conjure Celestial", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Elemental", + "value": "Spell that summons an elemental spirit. An enemy entering its space takes specific damage of lightning, thunder, fire, or water and is restrained.", + "type": "spell", + "title": "Conjure Elemental", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Fey", + "value": "Spell that summons a fey spirit. It can attack causing the target to take psychic damage and become frightened. It can teleport and attack another target.", + "type": "spell", + "title": "Conjure Fey", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Minor Elementals", + "value": "Spell that summons elemental spirits that surround the caster in a range of fifteen feet. They cause acid, cold, fire, or lightning damage to enemies in this area.", + "type": "spell", + "title": "Conjure Minor Elementals", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Volley", + "value": "Spell that creates spectral weapons or ammunition in a volley that causes force damage.", + "type": "spell", + "title": "Conjure Volley", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Conjure Woodland Beings", + "value": "Spell that summons nature spirits in a ten foot range that causes force damage as enemies enter the area.", + "type": "spell", + "title": "Conjure Woodland Beings", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Contact Other Plane", + "value": "Spell that allows the caster to speak to a demigod, long-dead sage, or an entity from another plane to ask five yes or no questions but at the risk of recieving psychic damage.", + "type": "spell", + "title": "Contact Other Plane", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Contagion spell, cast Contagion", + "value": "Spell that inflicts necrotic damage when the caster touches an enemy. The enemy is poisoned and is at a disadvantage.", + "type": "spell", + "title": "Contagion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Contingency spell, cast Contingency", + "value": "Spell that triggers another spell when certain conditions are met.", + "type": "spell", + "title": "Contingency", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Continual Flame", + "value": "Spell that ignites a flame from an object. That object then casts light. The flame consumes no fuel, creates no heat, and can't be smothered.", + "type": "spell", + "title": "Continual Flame", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Control Water", + "value": "Spell that allows the caster to control water allowing it to create a flood, part a path through the water, redirect the flow, or create a whirlpool.", + "type": "spell", + "title": "Control Water", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Control Weather", + "value": "Spell that allows the caster to change the weather, create clouds, fog, rain, hail, snow, or blizzard. The caster must be outdoors and it takes time to affect the climate.", + "type": "spell", + "title": "Control Weather", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Cordon of Arrows", + "value": "Spell that plants four arrows or bolts into the ground. As an enemy enters in a range of thirty feet, the arrow flies up and strikes it.", + "type": "spell", + "title": "Cordon of Arrows", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Counterspell", + "value": "Spell that attempts to interrupt a spell casted by an opponent.", + "type": "spell", + "title": "Counterspell", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Create Food and Water", + "value": "Spell that creates either 45 pounds of food or ten gallons of clean water.", + "type": "spell", + "title": "Create Food and Water", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Create Food", + "value": "Spell that creates 45 pounds of food. It is bland and spoils after 24 hours if uneaten.", + "type": "spell", + "title": "Create Food", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Create Water", + "value": "Spell that creates up to ten gallons of clean water in a container or by raining down in an area.", + "type": "spell", + "title": "Create or Destroy Water", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Create Undead", + "value": "Spell that can only be cast at night. The caster can transform up to three corpses into ghouls that they control that follow their orders. It takes time to cast this completely.", + "type": "spell", + "title": "Create Undead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Creation spell, cast Creation", + "value": "Spell that summons shadow material and creates a small object made out of vegetable or mineral matter like a rope or key. It dissapates into shadow when the spell ends.", + "type": "spell", + "title": "Creation", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Crown of Madness", + "value": "Spell that charms a creature. It must attack a creature of the caster's choosing each turn before it can move away.", + "type": "spell", + "title": "Crown of Madness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Crusader's Mantle", + "value": "Spell that creates a thirty foot magical emanation that causes the caster and their allies to cause additional radiant damage with their attacks.", + "type": "spell", + "title": "Crusader's Mantle", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Cure Wounds", + "value": "Spell that heals a creature the caster can touch.", + "type": "spell", + "title": "Cure Wounds", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dancing Lights", + "value": "Spell that creates four dancing lights that can be moved by the caster.", + "type": "spell", + "title": "Dancing Lights", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Darkness spell, cast Darkness", + "value": "Spell that creates a darkness emanation from a point or an object that can't be seen through and light cannot penetrate.", + "type": "spell", + "title": "Darkness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Darkvision spell, cast Darkvision", + "value": "Spell that grants the target the ability to see in dim light for up to 150 feet.", + "type": "spell", + "title": "Darkvision", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Daylight spell, cast Daylight", + "value": "Spell that creates bright light at a point or to emanate from an object.", + "type": "spell", + "title": "Daylight", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Death Ward", + "value": "Spell that grants the target protection from death. At the moment where damage would bring it completely to death, the target would instead have a single sliver of health.", + "type": "spell", + "title": "Death Ward", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Deafness spell, cast Deafness", + "value": "Spell that deafens the target.", + "type": "spell", + "title": "Deafness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Delayed Blast Fireball", + "value": "Spell that creates a glowing bead that eventually explodes with the force of a powerful fireball when it is dispelled.", + "type": "spell", + "title": "Delayed Blast Fireball", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Demiplane", + "value": "Spell that creates a magical doorway that leads to a demiplane where items and creatures can be stored.", + "type": "spell", + "title": "Demiplane", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Destroy Water", + "value": "Spell that destroys fog or up to ten gallons of water.", + "type": "spell", + "title": "Create or Destroy Water", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Destructive Wave", + "value": "Spell that causes necrotic or thunderous damage in a wave.", + "type": "spell", + "title": "Destructive Wave", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Detect Evil and Good", + "value": "Spell that allows the caster to sense celestials, elementals, fey, fiends, or undead and their location.", + "type": "spell", + "title": "Detect Evil and Good", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Detect Magic", + "value": "Spell that reveals if there are magical effects around the caster in a thirty foot range.", + "type": "spell", + "title": "Detect Magic", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Detect Poison and Disease", + "value": "Spell that detects if there are poisons or disease in the local area.", + "type": "spell", + "title": "Detect Poison and Disease", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Detect Thoughts", + "value": "Spell that allows the caster to sense creatures that have thoughts and allows the caster to read the thoughts of a single individual.", + "type": "spell", + "title": "Detect Thoughts", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dimension Door", + "value": "Spell that teleports the caster and one other willing target to a location in range.", + "type": "spell", + "title": "Dimension Door", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Disguise Self", + "value": "Spell that causes the caster to appear to look like somebody else.", + "type": "spell", + "title": "Disguise Self", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Disintegrate", + "value": "Spell that launches a green ray that causes incredible damage. If the attack kills, the target is distigrated into gray dust.", + "type": "spell", + "title": "Disintegrate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dispel Evil and Good", + "value": "Spell that breaks charmed and frightened enchantments. Celestials, elementals, fey, fiends, and the undead can be dismissed back to their home planes.", + "type": "spell", + "title": "Dispel Evil and Good", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dispel Magic", + "value": "Spell that dispels a magical effect within 120 feet.", + "type": "spell", + "title": "Dispel Magic", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dissonant Whispers", + "value": "Spell that causes the target to hear a melody in their mind which causes psychic damage.", + "type": "spell", + "title": "Dissonant Whispers", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Divination spell, cast Divination", + "value": "Spell that allows the caster to ask a question of a god or god's servant.", + "type": "spell", + "title": "Divination", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Divine Favor", + "value": "Spell that calls on divinity and gives the caster divine radiance. Subsequent attacks will cause radiant damage.", + "type": "spell", + "title": "Divine Favor", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Divine Smite", + "value": "Spell that adds radiant damage to an attack.", + "type": "spell", + "title": "Divine Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Divine Word", + "value": "Spell that causes a random effect including the death of the target and blindness, deafness, stunned conditions", + "type": "spell", + "title": "Divine Word", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dominate Beast", + "value": "Spell that has a chance of controlling a beast and commanding it to do the caster's will.", + "type": "spell", + "title": "Dominate Beast", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dominate Monster", + "value": "Spell that has a chance of controlling a creature and commanding it to do the caster's will.", + "type": "spell", + "title": "Dominate Monster", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dominate Person", + "value": "Spell that has a chance of controlling a humonoid and commanding it to do the caster's will.", + "type": "spell", + "title": "Dominate Person", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dragon's Breath", + "value": "Spell that grants the target that the caster can touch the ability to breathe fire.", + "type": "spell", + "title": "Dragon's Breath", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Drawmij's Instant Summons", + "value": "Spell that consumes an expensive saphire that will summon a marked object to the caster's hand.", + "type": "spell", + "title": "Drawmij's Instant Summons", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Dream spell, cast Dream", + "value": "Spell that causes the target and a designated dream messenger to enter a trance. They can enter the dreams of a target or make it a nightmare that causes psychic damage.", + "type": "spell", + "title": "Dream", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Druidcraft", + "value": "Spell that allows the caster to predict the weather, make plants bloom, or create a harmless sensory effect like a puff of wind or the smell of a dead animal.", + "type": "spell", + "title": "Druidcraft", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Earthquake", + "value": "Spell that creates tremors and rips the ground apart. It topples structures and creates fissures where creatures can fall in.", + "type": "spell", + "title": "Earthquake", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Eldritch Blast", + "value": "Spell that casts a beam of pure energy at a creature, causing magical force damage.", + "type": "spell", + "title": "Eldritch Blast", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Elementalism", + "value": "Spell that creates an elemental effect such as a breeze, dust, embers, mist. It can also shape elements into a figure that fits in a one foot cube.", + "type": "spell", + "title": "Elementalism", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Elemental Weapon", + "value": "Spell that causes a weapon touched by the caster to cause magical damage: acid, cold, fire, lightning, or thunder.", + "type": "spell", + "title": "Elemental Weapon", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Enhance Ability", + "value": "Spell that causes the target to have increased ability with Strength, Dexterity, Intelligence, Wisdom, or Charisma.", + "type": "spell", + "title": "Enhance Ability", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Enlarge spell, cast Enlarge", + "value": "Spell that increases the size of the target making them harder to move and increases their damage.", + "type": "spell", + "title": "Enlarge", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Enlarge/Reduce", + "value": "Spell that increases or decreases the size of the target which affects how easy they are to move and the damage they output.", + "type": "spell", + "title": "Enlarge/Reduce", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ensnaring Strike", + "value": "Spell that creates grasping vines around the target when it is hit with an attack that restrains the target.", + "type": "spell", + "title": "Ensnaring Strike", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Entangle spell, cast Entangle", + "value": "Spell that creates grasping plants on the ground. They are hard to traverse and any targets in the area are restrained.", + "type": "spell", + "title": "Entangle", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Enthrall", + "value": "Spell that implements a distracting string of words. They cause targets unable to perceive things well.", + "type": "spell", + "title": "Enthrall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Etherealness", + "value": "Spell that causes the caster to enter the Ethereal Plane. The caster can move in any direction. When the spell is ended, they are brought back in their new location.", + "type": "spell", + "title": "Etherealness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Evard's Black Tentacles", + "value": "Spell that creates squirming tentacles in a twenty foot square. These restrain and damage enemies in its area.", + "type": "spell", + "title": "Evard's Black Tentacles", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Expeditious Retreat", + "value": "Spell that can double the caster's dash distance.", + "type": "spell", + "title": "Expeditious Retreat", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Eyebite", + "value": "Spell that causes the target to sleep, panic, or feel sick. The caster can continue to target other creatures until the spell ends.", + "type": "spell", + "title": "Eyebite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fabricate spell, cast Fabricate", + "value": "Spell that can transform raw materials into a product such as ladders, ropes, clothes so long as they are not complicated.", + "type": "spell", + "title": "Fabricate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Faerie Fire", + "value": "Spell that creates a magical outline around any creatures in its area. They give off a dim light and cannot benefit from invisibility.", + "type": "spell", + "title": "Faerie Fire", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "False Life", + "value": "Adds temporary health to the target.", + "type": "spell", + "title": "False Life", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fear spell, cast Fear", + "value": "Spell that causes targets in a thirty foot cone to become frightened. They dash away at the sight of you.", + "type": "spell", + "title": "Fear", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Feather Fall", + "value": "Reactionary spell that prevents a target from taking fall damage. They lightly fall to the ground.", + "type": "spell", + "title": "Feather Fall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Feign Death", + "value": "Spell that causes a willing target to appear dead to others for up to an hour. They have resistance to all damage.", + "type": "spell", + "title": "Feign Death", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Find Familiar", + "value": "Spell that spawns a spirit animal that you have a telepathic connection to. It cannot attack, but can do other actions. Only one familiar at a time.", + "type": "spell", + "title": "Find Familiar", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Find Steed", + "value": "Spawns a large, rideable spirit animal that can attack, heal others, teleport, frighten enemies, and move quickly.", + "type": "spell", + "title": "Find Steed", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Find the Path", + "value": "Spell that magically finds the direct route to a location of the caster's choosing.", + "type": "spell", + "title": "Find the Path", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Find Traps", + "value": "Spell that detects the presence of traps within the line of sight. It reveals that a trap is present, but not where it is.", + "type": "spell", + "title": "Find Traps", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Finger of Death", + "value": "Spell that causes massive damage to a target that the caster can touch. If the touch kills, that target becomes a zombie.", + "type": "spell", + "title": "Finger of Death", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fireball", + "value": "Spell that creates a bright streak which explodes on collision with a target. It causes fire damage in a twenty foot radius.", + "type": "spell", + "title": "Fireball", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fire Bolt", + "value": "Spell that summons a mote of fire and casts it at a creature within range. It causes a moderate amount of fire damage.", + "type": "spell", + "title": "Fire Bolt", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fire Shield", + "value": "Spell that causes magical flames to protect the caster's body protecting from either cold or fire damage.", + "type": "spell", + "title": "Fire Shield", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fire Storm", + "value": "Spell that summons a fire storm that causes fire damage to creatures in an area of 100 feet.", + "type": "spell", + "title": "Fire Storm", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Flame Blade", + "value": "Spell that creates a fire blade in the caster's hand. It allows melee attacks that cause fire damage.", + "type": "spell", + "title": "Flame Blade", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Flame Strike", + "value": "Spell that summons a column of fire roaring down from above which causes radiant and fire damage.", + "type": "spell", + "title": "Flame Strike", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Flaming Sphere", + "value": "Spell that summons a sphere that causes fire damage to any creatures next to it. The sphere can be commanded to move by the caster.", + "type": "spell", + "title": "Flaming Sphere", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Flesh to Stone", + "value": "Spell that causes a target to turn to stone if they fail to will themselves away from its effects.", + "type": "spell", + "title": "Flesh to Stone", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fly spell, cast Fly", + "value": "Spell that allows the target to fly for a period of time.", + "type": "spell", + "title": "Fly", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fog Cloud", + "value": "Spell that creates a twenty foot sphere of fog which obstructs view through it.", + "type": "spell", + "title": "Fog Cloud", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Forbiddance", + "value": "Spell that prevents the magical use of telportation or portals within 40,000 square feet. It also damage aberrations, celestials, elementals, fey, fiends, and the undead.", + "type": "spell", + "title": "Forbiddance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Forcecage", + "value": "Spell that creates an invisible cube as a prison around a target.", + "type": "spell", + "title": "Forcecage", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Foresight", + "value": "Spell that gives the target the ability to see into the immediate future, giving an advantage to attacks and defense.", + "type": "spell", + "title": "Foresight", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Fount of Moonlight", + "value": "Spell that causes the caster to emit bright light which causes attacks to have extra radiant damage. The caster is also resistant to radiant damage.", + "type": "spell", + "title": "Fount of Moonlight", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Freedom of Movement", + "value": "Spell that causes the target to be able to deftly move over difficult terrain.", + "type": "spell", + "title": "Freedom of Movement", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Friends", + "value": "Spell that causes the target to be charmed. They are more likely to agree with the caster's requests, however they will know that they were charmed after the spell completes.", + "type": "spell", + "title": "Friends", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Gaseous Form", + "value": "Spell that transforms the caster or a willing target into a gaseous form that can fly and has resistance to physical damage.", + "type": "spell", + "title": "Gaseous Form", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Gate spell, cast Gate", + "value": "Spell that creates a portal that links a position the caster can see to another plane of existence. It can be used to summon a specific creature if its name is known.", + "type": "spell", + "title": "Gate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Geas", + "value": "Spell that charms a target and asks them to follow a verbal command.", + "type": "spell", + "title": "Geas", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Gentle Repose", + "value": "Spell that prevents a corpse from becoming undead and makes it not decay.", + "type": "spell", + "title": "Gentle Repose", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Giant Insect", + "value": "Spell that summons a giant centipede, spider, or wasp that follows your commands.", + "type": "spell", + "title": "Giant Insect", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Glibness", + "value": "Spell that makes it appear that you are telling the truth even when magical means are used to detect lies.", + "type": "spell", + "title": "Glibness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Globe of Invulnerability", + "value": "Spell that creates an impenetrable shell that prevents spells from affecting any targets inside it.", + "type": "spell", + "title": "Globe of Invulnerability", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Glyph of Warding", + "value": "Spell that allows you to inscribe a glyph that explodes or casts a spell based on a trigger specified by the caster.", + "type": "spell", + "title": "Glyph of Warding", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Goodberry spell, cast Goodberry", + "value": "Spell that creates berries that provide enough nourishment for the entire day.", + "type": "spell", + "title": "Goodberry", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Grasping Vine", + "value": "Spell that creates a magical vine that can do melee attacks and grapple enemies", + "type": "spell", + "title": "Grasping Vine", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Grease", + "value": "Spell that covers the ground with a nonflammable grease that can cause creatures to fall over prone.", + "type": "spell", + "title": "Grease", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Greater Invisibility", + "value": "Spell that allows a target that can be touched to become invisible. This invisibility is not disenchanted when the target takes actions.", + "type": "spell", + "title": "Greater Invisibility", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Greater Restoration", + "value": "Spell that removes exhaustion, curses, reduction to health or ability, charmed and petrified conditions.", + "type": "spell", + "title": "Greater Restoration", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Guardian of Faith", + "value": "Spell that summons a large, spectral guardian that hovers in a space. Any enemies that move within this space are attacked with radiant damage.", + "type": "spell", + "title": "Guardian of Faith", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Guards and Wards", + "value": "Spell that that fills corridors with fog, doors with with arcane locks, stairs filled with spider webs, and another magical affect including gust, stinking cloud, dancing lights, suggestion, or magic mouth.", + "type": "spell", + "title": "Guards and Wards", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Guidance", + "value": "Spell that adds a higher probability of success to any ability check.", + "type": "spell", + "title": "Guidance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Guiding Bolt", + "value": "Spell that casts a bolt of light at a creature that causes radiant damage. The next attack against this creature has advantage.", + "type": "spell", + "title": "Guiding Bolt", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Gust of Wind", + "value": "Spell that creates a continuous line of strong wind that can push creatures.", + "type": "spell", + "title": "Gust of Wind", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hail of Thorns", + "value": "Spell that is triggered after hitting an enemy with a ranged attack. Targets within 5 feet take piercing damage from a rain of thorns.", + "type": "spell", + "title": "Hail of Thorns", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hallow spell, cast Hallow", + "value": "Spell that prevents a choice of aberration, celestial, elemental, fey, fiend, or undead from entering the space. It can also create courage, darkness, daylight, peaceful rest, extradimensional interference, fear, resistance, silence, tongues, or vulnerability.", + "type": "spell", + "title": "Hallow", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hallucinatory Terrain", + "value": "Spell that makes terrain look like other terrain such as swamps, hills, crevasses, and difficult/impassible terrain.", + "type": "spell", + "title": "Hallucinatory Terrain", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Harm spell, cast Harm", + "value": "Spell that causes necrotic damage on a target that can be seen.", + "type": "spell", + "title": "Harm", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Haste spell, cast Haste", + "value": "Spell that causes a willing target to gain additional defense, become more dextrous, and gains an additional action every turn. When it is dispelled, the target becomes lethargic and must take a turn to recover.", + "type": "spell", + "title": "Haste", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Heal spell, cast Heal", + "value": "Spell that heals the target by a significant amount. They are also cured of blinded, deafened, and poisoned conditions.", + "type": "spell", + "title": "Heal", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Healing Word spell, cast Healing Word", + "value": "Spell that heals a creature within visible range.", + "type": "spell", + "title": "Healing Word", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Heat Metal", + "value": "Spell that causes a metal object to heat up and cause fire damage to the creature utilizing it.", + "type": "spell", + "title": "Heat Metal", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hellish Rebuke", + "value": "Spell that is cast as a reaction to taking damage. The target takes fire damage from green flames.", + "type": "spell", + "title": "Hellish Rebuke", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Heroes' Feast", + "value": "Spell that magically conjures a feast that allows anyone who eats from it to have resistance to poison damage and cannot be frightened or poisoned.", + "type": "spell", + "title": "Heroes' Feast", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Heroism spell, cast Heroism", + "value": "Spell that causes the target to not be frightened. It also adds temporary health.", + "type": "spell", + "title": "Heroism", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hex spell, cast Hex", + "value": "Spell that causes necrotic damage whenever the target is hit by an attack. It also has a disadvantage in a specified ability.", + "type": "spell", + "title": "Hex", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hold Monster", + "value": "Spell that can paralyze a monster for a duration.", + "type": "spell", + "title": "Hold Monster", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hold Person", + "value": "Spell that can paralyze a person for a duration.", + "type": "spell", + "title": "Hold Person", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Holy Aura", + "value": "Spell that creates an emanation where allies within it gain advantage. Any fiends or undead that attack may become blinded.", + "type": "spell", + "title": "Holy Aura", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hunger of Hadar", + "value": "Spell that creates an opening to another realm that is infested with unspeakable horrors. This darkness inflicts cold damage to anyone within in it and causes them to be blind.", + "type": "spell", + "title": "Hunger of Hadar", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hunter's Mark", + "value": "Spell that magically marks a creature. This causes extra force damage to the enemy when it is attacked by the caster and makes it easier to locate this creature.", + "type": "spell", + "title": "Hunter's Mark", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Hypnotic Pattern", + "value": "Spell that creates a pattern of colors that charms and incapacitates creatures that can see it.", + "type": "spell", + "title": "Hypnotic Pattern", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ice Knife", + "value": "Spell that creates a shard of ice that is flinged at a target. The target takes piercing damage. The shard explodes and causes additional damage.", + "type": "spell", + "title": "Ice Knife", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ice Storm", + "value": "Spell that creates hail which causes bludgeoning damage to enemies.", + "type": "spell", + "title": "Ice Storm", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Identify spell, cast Identify", + "value": "Spell that determines the nature of a magical object. It can also determine what spells are affecting a target.", + "type": "spell", + "title": "Identify", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Illusory Script", + "value": "Spell that makes the caster's writing on any sort of writing material to appear non-sensical to others except whoever the caster has designated.", + "type": "spell", + "title": "Illusory Script", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Imprisonment spell, cast Imprisonment", + "value": "Spell that creates magical restraints that hold a a target. The target can be buried, chained, or held in a demiplane/gemstone. It can also be put into a magical slumber.", + "type": "spell", + "title": "Imprisonment", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Incendiary Cloud", + "value": "Spell that creates a swirling cloud of embers and smoke. It cannot be seen through and causes fire damage to creatures inside its space.", + "type": "spell", + "title": "Incendiary Cloud", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Inflict Wounds", + "value": "Spell that causes minor necrotic damage.", + "type": "spell", + "title": "Inflict Wounds", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Insect Plague", + "value": "Spell that creates a swarm of locusts that causes piercing damage to creatures that are in range.", + "type": "spell", + "title": "Insect Plague", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Invisibility spell, cast Invisibility", + "value": "Spell that causes a willing target to become invisible. The spell ends if the target attacks or casts a spell.", + "type": "spell", + "title": "Invisibility", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Jallarzi's Storm of Radiance", + "value": "Spell that creates a storm of thunder and flashing light. Creatures in this area are blinded and deafened and can't speak. They take radiant damage.", + "type": "spell", + "title": "Jallarzi's Storm of Radiance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Jump spell, cast Jump", + "value": "Spell that causes a willing target to be able to jump up to thirty feet.", + "type": "spell", + "title": "Jump", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Knock", + "value": "Spell that unlocks any mundane or magical locks.", + "type": "spell", + "title": "Knock", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Legend Lore", + "value": "Spell that allows the caster to describe or name a popular person, place, or object. The caster then learns sinificant lore about that thing.", + "type": "spell", + "title": "Legend Lore", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Leomund's Secret Chest", + "value": "Spell that allows the caster to hide a chest in the ethereal plane. It can be recalled by touching the tiny replica of the chest.", + "type": "spell", + "title": "Leomund's Secret Chest", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Leomund's Tiny Hut", + "value": "Spell that bars any creatures outside of its emanation from entering. Inside is dry and comfortable and can't be seen through from the outside.", + "type": "spell", + "title": "Leomund's Tiny Hut", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Lesser Restoration", + "value": "Spell that ends blinded, deafened, paralyzed, or poisoned conditions.", + "type": "spell", + "title": "Lesser Restoration", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Levitate spell, cast Levitate", + "value": "Spell that causes the target to rise up twenty feet and stay there. The caster can then make the target rise up by twenty feet each turn.", + "type": "spell", + "title": "Levitate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Light spell, cast Light", + "value": "Spell that causes an object to emit bright light in a twenty foot radius", + "type": "spell", + "title": "Light", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Lightning Arrow", + "value": "Spell that transforms the ammunition of a ranged attack that just happened into a lightning bolt. It causes lightning damage.", + "type": "spell", + "title": "Lightning Arrow", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Lightning Bolt", + "value": "Spell that blasts a line of lightning damage from the caster.", + "type": "spell", + "title": "Lightning Bolt", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Locate Animals or Plants", + "value": "Spell that allows the caster to describe a beast or plant. The caster then learns the direction and distance to the closest creature or plant of that kind.", + "type": "spell", + "title": "Locate Animals or Plants", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Locate Creature", + "value": "Spell that allows the caster to describe or name a creature. The caster then learns the location of the target or the closest creature that matches that description.", + "type": "spell", + "title": "Locate Creature", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Locate Object", + "value": "Spell that allows the caster to describe an object. The caster will sense the location of an object of that description if it's within one thousand feet.", + "type": "spell", + "title": "Locate Object", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Longstrider", + "value": "Spell that increases the speed of a target that is touched.", + "type": "spell", + "title": "Longstrider", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mage Armor", + "value": "Spell that increases the defense of the touched target. This is only applicable if they are not already wearing armor.", + "type": "spell", + "title": "Mage Armor", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mage Hand", + "value": "Spell that creates an ethereal hand within range that will obey the commands of its caster. It must remain within range or it will disappear.", + "type": "spell", + "title": "Mage Hand", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Magic Circle", + "value": "Spell that creates a cylinder of magical energy. It may either block entry of creatures, the creatures inside are more susceptible to damage, or the creatures within cannot be frightened, charmed, or possessed.", + "type": "spell", + "title": "Magic Circle", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Magic Jar", + "value": "Spell that sends the caster's soul into a a container, leaving the caster's body in a catatonic state. The caster can then attempt to posess a humanoid body within 100 feet or return to their body.", + "type": "spell", + "title": "Magic Jar", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Magic Missile", + "value": "Spell that summons a number of darts comprised of pure magical force that can each target a creature that the caster can see within range, striking all at once.", + "type": "spell", + "title": "Magic Missile", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Magic Mouth", + "value": "Spell that implants a message into an object. When triggered, the mouth will speak the message.", + "type": "spell", + "title": "Magic Mouth", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Magic Weapon", + "value": "Spell that enchants a nonmagical weapon into becoming magical. It has a bonus to accuracy and damage.", + "type": "spell", + "title": "Magic Weapon", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Major Image", + "value": "Illusion spell that creates an image of an object, creature, or some visible phenomenon. It can be moved and appear naturally. Physical interaction will reveal that it is an illusion.", + "type": "spell", + "title": "Major Image", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mass Cure Wounds", + "value": "Spell that sends a wave of healing energy over six creatures.", + "type": "spell", + "title": "Mass Cure Wounds", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mass Heal", + "value": "Spell that sends a massive flood of healing energy into everyone around you.", + "type": "spell", + "title": "Mass Heal", "description": "", "useForCharacterCreation": false }, { "keys": "Mass Healing Word", - "value": "Heals up to six creatures, including yourself, within a given range.", + "value": "Spell that heals up to six creatures, including the caster, within a given range.", "type": "spell", "title": "Mass Healing Word", "description": "", "useForCharacterCreation": false }, + { + "keys": "Mass Suggestion", + "value": "Spell that causes up to twelve creatures to follow your command in 25 words or less.", + "type": "spell", + "title": "Mass Suggestion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Maze spell, cast Maze", + "value": "Spell that sends a creature into a demiplane that consists of a maze. They are forced to figure out how to escape it.", + "type": "spell", + "title": "Maze", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Meld into Stone", + "value": "Spell that allows the caster to meld into stone, making the caster undectable.", + "type": "spell", + "title": "Meld into Stone", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Melf's Acid Arrow", + "value": "Spell that spawns an arrow made of acid that is launched at a target.", + "type": "spell", + "title": "Melf's Acid Arrow", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mending spell, cast Mending", + "value": "Spell that repairs a simple break or tear in an item.", + "type": "spell", + "title": "Mending", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Message", + "value": "Spell that magically sends a whispered message to a target. The target can respond.", + "type": "spell", + "title": "Message", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Meteor Swarm", + "value": "Spell that sends explosive orbs down that cause fire and bludgeoning damage.", + "type": "spell", + "title": "Meteor Swarm", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mind Blank", + "value": "Spell that grants the target immunity to psychic damage and cannot be charmed.", + "type": "spell", + "title": "Mind Blank", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mind Sliver", + "value": "Spell that causes psychic damage to the target and makes them more susceptible to failing anything that tests their abilities.", + "type": "spell", + "title": "Mind Sliver", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mind Spike", + "value": "Spell that sends a spike of psionic energy into a target, causing psychic damage. The target can then not be hidden from the caster.", + "type": "spell", + "title": "Mind Spike", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Minor Illusion", + "value": "Spell that creates an illusion of a sound or image of an object within range. The spell is broken if a physical interaction with the item reveals it to be a hoax.", + "type": "spell", + "title": "Minor Illusion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mirage Arcane", + "value": "Illusion spell that can change the environment/buildings to seem like some other kind of terrain or look. It will look, smell, and feel as if it was like the real thing.", + "type": "spell", + "title": "Mirage Arcane", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mirror Image", + "value": "Spell that creates three duplicates of the caster. These make it hard for an enemy to target the caster, each taking damage for the caster.", + "type": "spell", + "title": "Mirror Image", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mislead spell, cast Mislead", + "value": "Spell that causes the caster to be invisible while a duplicate illusionary stand in appears in your place. It can move and behave as if it was you for the duration of the spell.", + "type": "spell", + "title": "Mislead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Misty Step", + "value": "Spell that allows the caster to teleport up to thirty feet away.", + "type": "spell", + "title": "Misty Step", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Modify Memory", + "value": "Spell that can charm a target. The target is incapacitated and a memory it has can be modified.", + "type": "spell", + "title": "Modify Memory", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Moonbeam", + "value": "Spell that shines a silvery beam of pale light in a cylinder at a specified point. It causes radiant damage.", + "type": "spell", + "title": "Moonbeam", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mordenkainen's Faithful Hound", + "value": "Spell that creates a watchdog that only the caster can see. It can bark and attack if a creature approaches without speaking a specified password.", + "type": "spell", + "title": "Mordenkainen's Faithful Hound", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mordenkainen's Magnificent Mansion", + "value": "Spell that creates a magical door that allows the caster and whoever they specify to enter an extradimensional mansion of any design. There are near transparent servants that do your bidding.", + "type": "spell", + "title": "Mordenkainen's Magnificent Mansion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mordenkainen's Private Sanctum", + "value": "Spell that creates a space that sound can't pass through, appears dark and foggy, divination can't work inside of it, or teleporting doesn't work.", + "type": "spell", + "title": "Mordenkainen's Private Sanctum", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Mordenkainen's Sword", + "value": "Spell that creates a hovering, spectral sword that can cause force damage on enemies that it attacks.", + "type": "spell", + "title": "Mordenkainen's Sword", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Move Earth", + "value": "Spell that can reshape terrain. It can form pillars and shapes. Change elevation. The ground moves slowly enough that it can't be used to damage anyone.", + "type": "spell", + "title": "Move Earth", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Nondetection", + "value": "Spell that hides the target from divination spells.", + "type": "spell", + "title": "Nondetection", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Nystul's Magic Aura", + "value": "Spell that makes the target or an object react to spells and other magical effects as if it was of another type. It can make an object appear magical or nonmagical.", + "type": "spell", + "title": "Nystul's Magic Aura", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Otiluke's Freezing Sphere", + "value": "Spell that creates a frigid globe that explodes with cold damage.", + "type": "spell", + "title": "Otiluke's Freezing Sphere", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Otiluke's Resilient Sphere", + "value": "Spell that creates a shimmering sphere. Nothing can pass through or damage the sphere. Anything inside is imprisoned, but can roll the sphere to move.", + "type": "spell", + "title": "Otiluke's Resilient Sphere", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Otto's Irresistible Dance", + "value": "Spell that causes the target to dance comically for a short time.", + "type": "spell", + "title": "Otto's Irresistible Dance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Passwall", + "value": "Spell that creates an opening on any specified wall, ceiling, or floor.", + "type": "spell", + "title": "Passwall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Pass without Trace", + "value": "Spell that creates an emanation around the caster that causes their allies to be more undetectable.", + "type": "spell", + "title": "Pass without Trace", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Phantasmal Force", + "value": "Spell that creates an illusion in the mind of a target. The illusion seems real and the target can even take psychic damage from it.", + "type": "spell", + "title": "Phantasmal Force", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Phantasmal Killer", + "value": "Spell that draws from the nightmares of a creature and causes psychic damage over a period of time.", + "type": "spell", + "title": "Phantasmal Killer", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Phantom Steed", + "value": "Spell that creates a large horselike creature. It has a saddle and can be ridden.", + "type": "spell", + "title": "Phantom Steed", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Planar Ally", + "value": "Spell that summons an otherworldly entity for aid. If it agrees, a celestial, elemental, or fiend ally is sent to aid the caster.", + "type": "spell", + "title": "Planar Ally", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Planar Binding", + "value": "Spell that attempts to bind a celestial, elemental, fey, or fiend into your service. The spell takes time and the target must be bound.", + "type": "spell", + "title": "Planar Binding", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Plane Shift", + "value": "Spell that allows the caster and allies to teleport to a different plane of existence.", + "type": "spell", + "title": "Plane Shift", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Plant Growth", + "value": "Spell that causes plants to grow. The plants can be overgrown and become difficult to pass through. The plants in the local area yield twice the amount of food.", + "type": "spell", + "title": "Plant Growth", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Poison Spray", + "value": "Spell that sprays a toxic mist that causes poison damage.", + "type": "spell", + "title": "Poison Spray", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Polymorph", + "value": "Spell that transforms a target into a beast of some kind. Upon death, the target turns back to their original form.", + "type": "spell", + "title": "Polymorph", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Power Word Fortify", + "value": "Spell that adds temporary health to specified targets.", + "type": "spell", + "title": "Power Word Fortify", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Power Word Heal", + "value": "Spell that causes a wave of healing energy to wash over a given target. It clears any negative conditions it faces.", + "type": "spell", + "title": "Power Word Heal", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Power Word Kill", + "value": "Spell that causes intense psychic damage and may even kill outright if the target is weak.", + "type": "spell", + "title": "Power Word Kill", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Power Word Stun", + "value": "Spell that causes the target to be stunned.", + "type": "spell", + "title": "Power Word Stun", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Prayer of Healing", + "value": "Spell that must take time to cast. Any targets within range are well rested and regain health points.", + "type": "spell", + "title": "Prayer of Healing", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Prestidigitation", + "value": "Spell that creates a sensory effect, ignite/snuff a fire, cleans/soils, chill/warm/flavor nonliving material, mark, or create an illusionary trinket.", + "type": "spell", + "title": "Prestidigitation", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Prismatic Spray", + "value": "Spell that creates an array of colored lights in a cone. Targets hit by the light accept a random effect including fire/acid/lightning/poison/cold damage, restraint, and blinding.", + "type": "spell", + "title": "Prismatic Spray", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Prismatic Wall", + "value": "Spell that creates a shimmering, impassible plane of light forms. It is comprised of seven layers, red, orange, yellow, green, blue, indigo, and violet. Each layer has different magical properties must take a different type of damage to be destroyed.", + "type": "spell", + "title": "Prismatic Wall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Produce Flame", + "value": "Spell that creates a flickering flame in the caster's hand. It emits dim light. It can be hurled to cause fire damage.", + "type": "spell", + "title": "Produce Flame", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Programmed Illusion", + "value": "Spell that creates an illusion that activates if a trigger is activated. It performs a preprogrammed performance.", + "type": "spell", + "title": "Programmed Illusion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Project Image", + "value": "Spell that creates an intangible, illusory image of the caster. It moves, speaks, and behaves exactly in the way that the caster desires.", + "type": "spell", + "title": "Project Image", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Protection from Energy", + "value": "Spell that causes the target to have resistance against a specified type of damage.", + "type": "spell", + "title": "Protection from Energy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Protection from Evil and Good", + "value": "Spell that protects the target from aberrations, celestials, elementals, fey, fiends, and the undead. The target cannot be possessed, charmed, or frightened.", + "type": "spell", + "title": "Protection from Evil and Good", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Protection from Poison", + "value": "Spell that prevents the target from being poisoned.", + "type": "spell", + "title": "Protection from Poison", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Purify Food and Drink", + "value": "Spell that removes poison from nonmagical food and drink.", + "type": "spell", + "title": "Purify Food and Drink", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Raise Dead", + "value": "Spell that revives a dead creature if it wasn't undead. It doesn't restore missing body parts. The target is not well until it has time to rest.", + "type": "spell", + "title": "Raise Dead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Rary's Telepathic Bond", + "value": "Spell that creates a telepathic bond with up to eight creatures. They can communicate through telepathy no matter what language they speak over any distance.", + "type": "spell", + "title": "Rary's Telepathic Bond", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ray of Enfeeblement", + "value": "Spell that casts a beam of energy. Targets affected by the beam will be at a disadvantage when they try to attack and inflict less damage.", + "type": "spell", + "title": "Ray of Enfeeblement", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ray of Frost", + "value": "Spell that casts a beam of blue-white light that causes cold damage on a given target within range. The target is then slowed for a short period of time.", + "type": "spell", + "title": "Ray of Frost", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Ray of Sickness", + "value": "Spell that casts a greenish ray that inflicts poison damage.", + "type": "spell", + "title": "Ray of Sickness", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Reduce spell, cast Reduce", + "value": "Spell that decreases the size of the target making them easier to move and decreases their damage.", + "type": "spell", + "title": "Reduce", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Regenerate spell, cast Regenerate", + "value": "Spell that heals a target and regrows any severed limbs.", + "type": "spell", + "title": "Regenerate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Reincarnate spell, cast Reincarnate", + "value": "Spell that can form a new body for a recently deceased humanoid (within 10 days). The new body is one of the following random species: aasimar, dragonborn, dwarf, elf, gnome, goliath, halfling, human, orc, or tiefling.", + "type": "spell", + "title": "Reincarnate", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Remove Curse", + "value": "Spell that removes all curses from a target.", + "type": "spell", + "title": "Remove Curse", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Resistance spell, cast Resistance", + "value": "Spell that causes the target to receive less damage from a specific type of damage.", + "type": "spell", + "title": "Resistance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Resurrection spell, cast Resurrection", + "value": "Spell that revives a dead creature. It cannot be an undead when it died. The target is not well until it has a long rest.", + "type": "spell", + "title": "Resurrection", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Reverse Gravity", + "value": "Spell that reverses gravity within a fifty foot radius.", + "type": "spell", + "title": "Reverse Gravity", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Revivify spell, cast Revivify", + "value": "Spell that can revive a creature that has died in the last minute. It does not restore missing body parts.", + "type": "spell", + "title": "Revivify", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Rope Trick", + "value": "Spell that levitates an end of a rope. The rope leads to a portal that opens into extradimensional space. Creatures can climb into this space and become immune to spells and attacks.", + "type": "spell", + "title": "Rope Trick", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sacred Flame", + "value": "Spell that creates a flame-like radiance around a creature that the caster can see. The target takes radiant damage.", + "type": "spell", + "title": "Sacred Flame", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sanctuary spell, cast Sanctuary", + "value": "Spell that wards a creature within range. Any attacks or spells targeted against this creature can be redirected at another opponent or be nullified completely.", + "type": "spell", + "title": "Sanctuary", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Scorching Ray", + "value": "Spell that shoots three friery rays at targets that cause fire damage.", + "type": "spell", + "title": "Scorching Ray", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Scrying", + "value": "Spell that allows the caster to see and hear any target on the same plane of existence. The effectiveness of the spell depends on how well the caster knows the target and if they have any personal affects of the target.", + "type": "spell", + "title": "Scrying", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Searing Smite", + "value": "Spell that causes the caster's attack to have additional fire damage.", + "type": "spell", + "title": "Searing Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "See Invisibility", + "value": "Spell that allows the caster to see invisible creatures and see into the ethereal plane.", + "type": "spell", + "title": "See Invisibility", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Seeming spell, cast Seeming", + "value": "Spell that changes the appearance of a number of creatures with illusion. The appearance must have the same basic arrangement of limbs, but can look like anything.", + "type": "spell", + "title": "Seeming", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sending spell, cast Sending", + "value": "Spell that sends a short message telepathically to a creature that the caster has met before.", + "type": "spell", + "title": "Sending", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sequester", + "value": "Spell that causes the target or an object to become invisible and can't be targeted by divination, detected by magic, or viewed remotely. The target is made unconcious and doesn't age or need food. This lasts until a trigger is called.", + "type": "spell", + "title": "Sequester", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shapechange", + "value": "Spell that allows the caster to shapeshift into another creature. The caster keeps their mind and the ability to talk.", + "type": "spell", + "title": "Shapechange", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shatter spell, cast Shatter", + "value": "Spell that creates a loud noise in a sphere. Creatures inside the sphere take thunder damage.", + "type": "spell", + "title": "Shatter", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shield spell, cast Shield", + "value": "Spell that creates an invisible barrier that protects the user from some physical damage.", + "type": "spell", + "title": "Shield", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shield of Faith", + "value": "Spell that creates a shimmering field which gives the target increased defense.", + "type": "spell", + "title": "Shield of Faith", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shillelagh", + "value": "Spell that allows the caster to use their magic ability instead of strength to attack with a weapon.", + "type": "spell", + "title": "Shillelagh", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shining Smite", + "value": "Spell that allows the caster to attack and add radiant damage to the blow.", + "type": "spell", + "title": "Shining Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Shocking Grasp", + "value": "Spell that causes lightning damage from the touch of the caster.", + "type": "spell", + "title": "Shocking Grasp", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Silence spell, cast Silence", + "value": "Spell that creates a spherical space where no words can be spoken and no sounds can be heard. This prevents magic that requires speech.", + "type": "spell", + "title": "Silence", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Silent Image", + "value": "Spell that creates an illusionary image. It can be moved, but it is not accompanied by an appropriate smell or sound.", + "type": "spell", + "title": "Silent Image", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Simulacrum", + "value": "Spell that makes a copy of a creature out of ice or snow. It then follows the caster's commands and is functionally like the original. The spell consumes an expensive ruby.", + "type": "spell", + "title": "Simulacrum", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sleep spell, cast Sleep", + "value": "Spell that causes the target to become unconcious. The condition ends if the target is damaged or awoken by another creature.", + "type": "spell", + "title": "Sleep", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sleet Storm", + "value": "Spell that conjures sleet in the area of a cylinder. The terrain becomes difficult to navigate and creatures are knocked prone.", + "type": "spell", + "title": "Sleet Storm", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Slow spell, cast Slow", + "value": "Spell that causes targets to move slower, resulting in targets being able to do less actions and be susceptible to attacks.", + "type": "spell", + "title": "Slow", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sorcerous Burst", + "value": "Spell that shoots energy at a creature. It takes damage of a type that the caster chooses. There is a chance that additional damage is added.", + "type": "spell", + "title": "Sorcerous Burst", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Spare the Dying", + "value": "Spell that stabilizes a creature that is on death's door.", + "type": "spell", + "title": "Spare the Dying", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Speak with Animals", + "value": "Spell that allows the caster to communicate fully with beasts.", + "type": "spell", + "title": "Speak with Animals", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Speak with Dead", + "value": "Spell that allows the caster to ask up to five questions to a corpse which will animate and respond according to what it knew in life.", + "type": "spell", + "title": "Speak with Dead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Speak with Plants", + "value": "Spell that allows the caster to ask simple questions to plants in a thirty foot range. The plants can also follow commands and even make impassible thicket easier to walk through.", + "type": "spell", + "title": "Speak with Plants", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Spider Climb", + "value": "Spell that allows a willing target to be able to move accross walls and ceilings without using their hands.", + "type": "spell", + "title": "Spider Climb", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Spike Growth", + "value": "Spell that sprouts spiky grow in a twenty foot range that is hard to traverse and causes piercing damage.", + "type": "spell", + "title": "Spike Growth", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Spirit Guardians", + "value": "Spell that conjures good or evil spirits that slows and causes radiant or necrotic damage to any creatures that enter a fifteen foot range.", + "type": "spell", + "title": "Spirit Guardians", + "description": "", + "useForCharacterCreation": false + }, { "keys": "Spiritual Weapon", - "value": "Creates a floating, spectral weapon that obeys the caster's commands. It can move independently and attack opponents. It can take the shape of any form of any melee weapon.", + "value": "Spell that creates a floating, spectral weapon that obeys the caster's commands. It can move independently and attack opponents. It can take the shape of any form of any melee weapon.", "type": "spell", "title": "Spiritual Weapon", "description": "", "useForCharacterCreation": false }, + { + "keys": "Staggering Smite", + "value": "Spell that adds additional psychic damage to an attack. It has a chance to stun an opponent.", + "type": "spell", + "title": "Staggering Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Starry Wisp", + "value": "Spell that launches a mote of light at a creature. It takes radiant damage and emits dim light.", + "type": "spell", + "title": "Starry Wisp", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Steel Wind Strike", + "value": "Spell that makes the caster vanish and then strike like the wind against up to five opponents. They each take force damage. Then the caster is teleported next to one of the opponents.", + "type": "spell", + "title": "Steel Wind Strike", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stinking Cloud", + "value": "Spell that creates a nauseating gas around a point. It causes creatures to be poisoned and not be able to take action for a short time.", + "type": "spell", + "title": "Stinking Cloud", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stone Shape", + "value": "Spell that can transform stone into any shape including weapons, passage ways, doors, or simple locks.", + "type": "spell", + "title": "Stone Shape", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stoneskin", + "value": "Spell that transforms the target's skin so that it has resistance to bludgeoning, piercing, and slashing damage.", + "type": "spell", + "title": "Stoneskin", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Storm of Vengeance", + "value": "Spell that creates a storm that causes thunder damage against creatures in its area. It progresses to rain acid, strike lightning, cast hailstones, and eventually blow freezing wind.", + "type": "spell", + "title": "Storm of Vengeance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Suggestion spell, cast Suggestion", + "value": "Spell that allows the caster to suggest a course of action that a creature will follow. It has to be short and achieveable.", + "type": "spell", + "title": "Suggestion", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Aberration", + "value": "Spell that allows the caster to summon an abberation that must follow the caster's commands.", + "type": "spell", + "title": "Summon Aberration", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Beast", + "value": "Spell that allows the caster to summon a beast that must follow the caster's commands.", + "type": "spell", + "title": "Summon Beast", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Celestial", + "value": "Spell that allows the caster to summon a celestial that must follow the caster's commands.", + "type": "spell", + "title": "Summon Celestial", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Construct", + "value": "Spell that creates a construct that must follow the caster's commands.", + "type": "spell", + "title": "Summon Construct", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Dragon", + "value": "Spell that allows the caster to summon a draconic spirit that must follow the caster's commands.", + "type": "spell", + "title": "Summon Dragon", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Elemental", + "value": "Spell that allows the caster to summon an elemental spirit that must follow the caster's commands.", + "type": "spell", + "title": "Summon Elemental", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Fey", + "value": "Spell that allows the caster to summon a fey spirit that must follow the caster's commands.", + "type": "spell", + "title": "Summon Fey", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Fiend", + "value": "Spell that allows the caster to summon a fiend that must follow the caster's commands.", + "type": "spell", + "title": "Summon Fiend", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Summon Undead", + "value": "Spell that allows the caster to summon an undead spirit that must follow the caster's commands.", + "type": "spell", + "title": "Summon Undead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sunbeam", + "value": "Spell that creates a beam of sunlight that blinds enemies and causes radiant damage. It can be summoned again while this spell is in effect. The caster has a light mote of brilliance while this spell is active.", + "type": "spell", + "title": "Sunbeam", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sunburst", + "value": "Spell that creates a brilliant blast of sunlight in a 60 foot radius. It blinds enemies and causes significant radiant damage.", + "type": "spell", + "title": "Sunburst", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Swift Quiver", + "value": "Spell that allows the caster to take additional attacks with a bow or crossbow.", + "type": "spell", + "title": "Swift Quiver", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Symbol spell, cast Symbol", + "value": "Spell that creates a harmful glyph that can be set off by a specified trigger as a trap. It can cause necrotic damage, cause targets to argue with themselves, frighten/incapacitate/sleep/stun them.", + "type": "spell", + "title": "Symbol", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Sympathy spell, cast Sympathy", + "value": "Spell that causes the creature to be charmed and must move as close as possible to the caster.", + "type": "spell", + "title": "Sympathy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Synaptic Static", + "value": "Spell that causes psychic damage. The target then has difficulty maintaining concentration.", + "type": "spell", + "title": "Synaptic Static", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tasha's Bubbling Cauldron", + "value": "Spell that creates a cauldron. The caster can then reach in to receive a common or uncommon potion.", + "type": "spell", + "title": "Tasha's Bubbling Cauldron", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tasha's Hideous Laughter", + "value": "Spell that causes the target to go prone and laugh uncontrollably.", + "type": "spell", + "title": "Tasha's Hideous Laughter", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Telekinesis spell, cast Telekinesis", + "value": "Spell that allows the caster to move objects or creatures with their mind.", + "type": "spell", + "title": "Telekinesis", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Telepathy spell, cast Telepathy", + "value": "Spell that allows the caster to speak telepathically with willing targets of their choosing, instantly sending images, words, sounds, and other sensory messages.", + "type": "spell", + "title": "Telepathy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Teleport spell, cast Teleport", + "value": "Spell that instantly transports the caster and up to eight other creatures at once. The target location must be known by the caster or it may result in a mishap of some kind.", + "type": "spell", + "title": "Teleport", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Teleportation Circle", + "value": "Spell that allows the caster to draw a circle on the ground. This circle is linked with a permanent circle created in another location.", + "type": "spell", + "title": "Teleportation Circle", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tenser's Floating Disk", + "value": "Spell that creates a simple floating disc that can move creatures or objects. It follows the caster.", + "type": "spell", + "title": "Tenser's Floating Disk", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Thaumaturgy", + "value": "Spell that causes the target to have altered eyes or a booming voice. It can also play with fire, open a door or window dramatically, create a sound, or cause harmless tremors.", + "type": "spell", + "title": "Thaumaturgy", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Thorn Whip", + "value": "Spell that creates a vine-like whip that causes piercing damage.", + "type": "spell", + "title": "Thorn Whip", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Thunderclap", + "value": "Spell that creates a loud boom that causes thunder damage that damages each creature within 5 feet.", + "type": "spell", + "title": "Thunderclap", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Thunderous Smite", + "value": "Spell that adds lightning damage to your attack and amplifies the attack's effectiveness.", + "type": "spell", + "title": "Thunderous Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Thunderwave", + "value": "Spell that creates a wave of force that thrusts everything within a short range in front of you away. This can push friend and foe alike, causing them to topple over.", + "type": "spell", + "title": "Thunderwave", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Time Stop", + "value": "Spell that stops the flow of time for everyone except the caster. The caster can take any actions during this short period, however any actions that affect another creature end the time stop.", + "type": "spell", + "title": "Time Stop", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Toll the Dead", + "value": "Spell that allows the caster to point at a creature which will then hear the chime of a bell. It will take necrotic damage and more so if it is already damage.", + "type": "spell", + "title": "Toll the Dead", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tongues spell, cast Tongues", + "value": "Spell that grants a creature that the caster can touch the ability to understand all spoken or signed languages. They can also be understood by any creatures that know a language.", + "type": "spell", + "title": "Tongues", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Transport via Plants", + "value": "Spell that allows creatures to enter a large plant and allow them to exit another plant elsewhere. The caster must have seen or touched this place before.", + "type": "spell", + "title": "Transport via Plants", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tree Stride", + "value": "Spell that allows the caster to enter a tree and exit a similar one within 500 feet.", + "type": "spell", + "title": "Tree Stride", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "True Polymorph", + "value": "Spell that allows the caster to select a target which will be transformed into another kind of creature. It can also transform objects into creatures and creatures into objects.", + "type": "spell", + "title": "True Polymorph", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "True Resurrection", + "value": "Spell that resurrects a creature that has been dead for no longer than 200 years. The spell fully heals, cures, and regrows missing limbs.", + "type": "spell", + "title": "True Resurrection", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "True Seeing", + "value": "Spell that gives the target true sight, which sees through illusion, darkness, and invisibility.", + "type": "spell", + "title": "True Seeing", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "True Strike", + "value": "Spell that allows the caster to attack with a weapon, but their accuracy and damage is based on their magical ability.", + "type": "spell", + "title": "True Strike", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Tsunami spell, cast Tsunami", + "value": "Spell that creates a wall of water that can up to 300 feet tall. It causes significant bludgeoning damage.", + "type": "spell", + "title": "Tsunami", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Unseen Servant", + "value": "Spell that creates an invisible force that performs simple tasks. It cannot attack or do any heavy lifting.", + "type": "spell", + "title": "Unseen Servant", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Vampiric Touch", + "value": "Spell that causes necrotic damage. The caster regains half of the damage as health.", + "type": "spell", + "title": "Vampiric Touch", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Vicious Mockery", + "value": "Spell that insults the target with a barrage of abusive language and subtle enchantments. If it can hear you, it will receive psychic damage and will be at a disadvantage for the next attack.", + "type": "spell", + "title": "Vicious Mockery", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Vitriolic Sphere", + "value": "Spell that launches a ball of acid at an enemy, causing incredible acid damage.", + "type": "spell", + "title": "Vitriolic Sphere", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wall of Fire", + "value": "Spell that creates a wall of fire that damages any enemies moving through it from a single side.", + "type": "spell", + "title": "Wall of Fire", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wall of Force", + "value": "Spell that creates an invisible wall of force that prevents creatures, projectiles, and spells from passing through it. It can only be destroyed by the disintegrate spell.", + "type": "spell", + "title": "Wall of Force", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wall of Ice", + "value": "Spell that creates a wall of ice that causes significant cold damage to enemies that touch it initially. It can be destroyed, but leaves behind frigid air that continues to damage enemies.", + "type": "spell", + "title": "Wall of Ice", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wall of Stone", + "value": "Spell that creates a wall of stone that can also be shaped to imprison creatures.", + "type": "spell", + "title": "Wall of Stone", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wall of Thorns", + "value": "Spell that creates a wall of tangled brush. It causes piercing and slashing damage to creatures trying to pass through it. It is difficult to pass through.", + "type": "spell", + "title": "Wall of Thorns", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Warding Bond", + "value": "Spell that creates a mystical connection between the caster and another creature. The creature has additional defense and resistance to all damage.", + "type": "spell", + "title": "Warding Bond", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Water Breathing", + "value": "Spell that allows a number of creatures to be able to breath underwater for a time.", + "type": "spell", + "title": "Water Breathing", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Water Walk", + "value": "Spell that allows a number of creatures to be able to walk over the surface of liquids harmlessly.", + "type": "spell", + "title": "Water Walk", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Web spell, cast Web", + "value": "Spell that shoots a mass of webs at a point, causing the terrain to be difficult to pass through. Any creatures caught in the web are restrained.", + "type": "spell", + "title": "Web", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Weird spell, cast Weird", + "value": "Spell that creates visions of terrible things in a target's mind. This affects targets within a 30 foot radius. They take extreme psychic damage and continues to take damage while they are frightened.", + "type": "spell", + "title": "Weird", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wind Walk", + "value": "Spell that allows a number of creatures transform into a gaseous form and traverse great distances with ease.", + "type": "spell", + "title": "Wind Walk", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wind Wall", + "value": "Spell that creates a vortex of wind in any shape that makes a continuous path. It causes bludgeoning damage to creatures and pushes light creatures and missiles away.", + "type": "spell", + "title": "Wind Wall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wish", + "value": "Spell that grants a single wish to caster. This is typically in the form of another spell casted for free. However, it can be casted to do anything, but with a chance of weakening the caster or removing their ability to ever make another wish again.", + "type": "spell", + "title": "Wish", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Witch Bolt", + "value": "Spell that creates a sustained arc of lightning between the caster and the target. It causes constant lightning damage, but requires the user to concentrate on the spell. The spell is broken when the caster is injured or loses concentration in any way.", + "type": "spell", + "title": "Witch Bolt", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Word of Radiance", + "value": "Spell that creates a burning radiance that causes radiant damage.", + "type": "spell", + "title": "Word of Radiance", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Word of Recall", + "value": "Spell that transports a number of creatures and the caster to a location previously designated as a sanctuary.", + "type": "spell", + "title": "Word of Recall", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Wrathful Smite", + "value": "Spell that causes additional necrotic damage to an attack.", + "type": "spell", + "title": "Wrathful Smite", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Yolande's Regal Presence", + "value": "Spell that causes psychic damage whenever a creature enters ten feet from the caster.", + "type": "spell", + "title": "Yolande's Regal Presence", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Zone of Truth", + "value": "Spell that creates a range of fifteen feet that any creatures within cannot speak a deliberate lie.", + "type": "spell", + "title": "Zone of Truth", + "description": "", + "useForCharacterCreation": false + }, { "keys": "Stragedy", "value": "A trading card game passed down through the generations. The magical cards are rare and have incredible value. Players use the cards to have friendly battles.", - "type": "item", + "type": "card", "title": "Stragedy Card Game", "description": "", "useForCharacterCreation": false @@ -162,7 +7730,7 @@ { "keys": "Stragedy Ace", "value": "Sends all cards that have the number that this card is played on to the discard pile.", - "type": "item", + "type": "card", "title": "Stragedy Ace Card", "description": "", "useForCharacterCreation": false @@ -170,57 +7738,65 @@ { "keys": "Stragedy Jack", "value": "Sends the targeted card back to the discard pile. The player can then draw a card from the discard pile and take another turn.", - "type": "item", + "type": "card", "title": "Stragedy Jack Card", "description": "", "useForCharacterCreation": false }, - { - "keys": "Stragedy Queen", - "value": "Removes the value of the numbered card that this is played on and adds it to the opponent.", - "type": "item", - "title": "Stragedy Queen Card", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Stragedy King", - "value": "Doubles the value of the numbered card that this is played on both sides of the battlefield.", - "type": "item", - "title": "Stragedy King Card", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Stragedy Joker", - "value": "Increases the total of the numbered card that this is placed on to get a score of 30. It cannot subtract points if you bust, however.", - "type": "item", - "title": "Stragedy Joker Card", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Stragedy Witch", - "value": "Forces the opponent to play a card directly from their draw pile or a random one chosen from their hand. If it can't be played, they must discard their hand.", - "type": "item", - "title": "Stragedy Witch Card", - "description": "", - "useForCharacterCreation": false - }, - { - "keys": "Stragedy Priest", - "value": "Prevents the numbered card that this is played on from causing you to bust. It subtracts down to 30.", - "type": "item", - "title": "Stragedy Priest Card", - "description": "", - "useForCharacterCreation": false - }, { "keys": "Stragedy Brigand", "value": "Your opponent must discard 5 cards from their draw pile.", - "type": "item", + "type": "card", "title": "Stragedy Brigand Card", "description": "", "useForCharacterCreation": false + }, + { + "keys": "Stragedy Joker", + "value": "Increases the total of the numbered card that this is placed on to get a score of 30. It cannot subtract points if you bust, however.", + "type": "card", + "title": "Stragedy Joker Card", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stragedy King", + "value": "Doubles the value of the numbered card that this is played on both sides of the battlefield.", + "type": "card", + "title": "Stragedy King Card", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stragedy Priest", + "value": "Prevents the numbered card that this is played on from causing you to bust. It subtracts down to 30.", + "type": "card", + "title": "Stragedy Priest Card", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stragedy Queen", + "value": "Removes the value of the numbered card that this is played on and adds it to the opponent.", + "type": "card", + "title": "Stragedy Queen Card", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Stragedy Witch", + "value": "Forces the opponent to play a card directly from their draw pile or a random one chosen from their hand. If it can't be played, they must discard their hand.", + "type": "card", + "title": "Stragedy Witch Card", + "description": "", + "useForCharacterCreation": false + }, + { + "keys": "Edit the entry above to adjust your story card automation settings", + "value": "> Auto-Cards automatically creates and updates plot-relevant story cards while you play. You may configure the following settings by replacing \"false\" with \"true\" (and vice versa) or by adjusting numbers for the appropriate settings.\n> Disable Auto-Cards: true\n> Show detailed guide: false\n> Delete all automatic story cards: false\n> Reset all config settings and prompts: false\n> Pin this config card near the top: true\n> Minimum turns cooldown for new cards: 22\n> New cards use a bulleted list format: true\n> Maximum entry length for new cards: 750\n> New cards perform memory updates: true\n> Card memory bank preferred length: 2750\n> Memory summary compression ratio: 25\n> Exclude all-caps from title detection: true\n> Also detect titles from player inputs: false\n> Minimum turns age for title detection: 5\n> Use Live Script Interface v2: false\n> Log debug data in a separate card: false", + "type": "class", + "title": "Configure \nAuto-Cards", + "description": "> Please visit my AI Dungeon profile @LewdLeah and read my bio for simple instructions on adding Auto-Cards to your scenarios! ā¤ļø\n\nhttps://play.aidungeon.com/profile/LewdLeah\n\n> Auto-Cards v1.0.0 is an open-source script for AI Dungeon made by LewdLeah. You have my full permission to use Auto-Cards within your personal or published scenarios!\n> You're also welcome to edit the AI prompts and title exclusions provided below. I sincerely hope you enjoy your adventures! 🄰\n——————————————————————————\n> AI prompt to generate new cards:\n-----\n\n\n# Stop the story and ignore previous instructions. Write a brief and coherent informational entry for %{title} following these instructions:\n- Write only third-person pure prose information about %{title} using complete sentences with correct punctuation\n- Avoid short-term temporary details or appearances, instead focus on plot-significant information\n- Prioritize story-relevant details about %{title} first to ensure seamless integration with the previous plot\n- Create new information based on the context and story direction\n- Mention %{title} in every sentence\n- Use semicolons if needed\n- Add additional details about %{title} beneath incomplete entries\n- Be concise and grounded\n- Imitate the story's writing style and infer the reader's preferences\n\nContinue the entry for %{title} below while avoiding repetition:\n%{entry}\n——————————————————————————\n> AI prompt to summarize card memories:\n-----\n\n\n# Stop the story and ignore previous instructions. Summarize and condense the given paragraph into a narrow and focused memory passage while following these guidelines:\n- Ensure the passage retains the core meaning and most essential details\n- Use the third-person perspective\n- Prioritize information-density, accuracy, and completeness\n- Remain brief and concise\n- Write firmly in the past tense\n- The paragraph below pertains to old events from far earlier in the story\n- Integrate %{title} naturally within the memory; however, only write about the events as they occurred\n- Only reference information present inside the paragraph itself, be specific\n\nWrite a summarized old memory passage for %{title} based only on the following paragraph:\n\"\"\"\n%{memory}\n\"\"\"\nSummarize below:\n——————————————————————————\n> Titles banned from new card creation:\nNorth, East, South, West, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, January, February, March, April, May, June, July, August, September, October, November, December", + "useForCharacterCreation": false } ] \ No newline at end of file