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 c1864f1..aba6921 100644
--- a/Input.js
+++ b/Input.js
@@ -1,4 +1,4 @@
-const version = "Hashtag DnD v0.6.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"]
@@ -70,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"]
@@ -112,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 }
@@ -177,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, lockpickSynonyms, memorySynonyms, 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) {
@@ -262,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)
@@ -302,6 +318,8 @@ const modifier = (text) => {
if (state.flavorText != null) text += state.flavorText
+ text = AutoCards("input", text);
+
return { text }
}
@@ -1186,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},
@@ -1240,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 = []
@@ -1249,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
@@ -1317,6 +1766,13 @@ 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) {
@@ -2494,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
@@ -2504,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]`
}
}
@@ -2568,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)
@@ -2577,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]`
}
}
@@ -2850,6 +3322,7 @@ function doAttack(command) {
}
var enemyString = ""
+ var allyString = ""
if (state.initiativeOrder.length > 0) {
var foundEnemy
@@ -2868,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)
@@ -2894,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
@@ -2901,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}!`
@@ -2914,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"
@@ -3252,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) {
@@ -3304,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) {
@@ -3317,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) {
@@ -3406,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")
@@ -3418,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"
@@ -3479,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
@@ -3519,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"
@@ -4265,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
@@ -4283,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)
@@ -4305,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"
@@ -4322,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`
@@ -4493,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 2aa5399..21f6d9f 100644
--- a/Library.js
+++ b/Library.js
@@ -344,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)
@@ -373,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`
@@ -2775,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
@@ -2807,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;
});
@@ -4032,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