Think of Scripts as magical add-ons that can transform your AI Dungeon experience in amazing ways—from automatic dice rolling and stat tracking to custom world-building tools and special effects. The best part? You don't need to be a programmer or know a single line of code to use them! Our incredible community has already created dozens of ready-to-use Scripts that you can install in just a few clicks.
Whether you want to add RPG mechanics, new visuals and themes, track character relationships, or completely customize how your adventures work, there's probably already a Script for that. Installing them is as simple as copy-and-paste, and suddenly your stories can do things you never thought possible. Ready to discover what your adventures have been missing?
Simply choose one of the featured community made Scripts below or browse our Discord's Script Library forum to find many more!
// Your "Input" tab should look like this
const modifier = (text) => {
// Your other input modifier scripts go here (preferred)
text = AutoCards("input", text);
// Your other input modifier scripts go here (alternative)
return {text};
};
modifier(text);
‣
Context
// Your "Context" tab should look like this
const modifier = (text) => {
// Your other context modifier scripts go here (preferred)
[text, stop] = AutoCards("context", text, stop);
// Your other context modifier scripts go here (alternative)
return {text, stop};
};
modifier(text);
‣
Output
// Your "Output" tab should look like this
const modifier = (text) => {
// Your other output modifier scripts go here (preferred)
text = AutoCards("output", text);
// Your other output modifier scripts go here (alternative)
return {text};
};
modifier(text);
Let the AI do the work for you
The Auto-Cards script automatically writes & updates plot-relevant story cards during gameplay, keeping your story coherent with almost zero effort.
The default setup works great right out of the box, just play normally and watch your world build itself! My primary goal was to address the "object permanence problem" by extending story cards and memories with deeper automation. Therefore, Auto-Cards builds a living reference of your adventure's world as you go. LewdLeah.
Main Features:
Simple and easy to use, full background automation allows you to focus on gameplay
Suitable for both free and premium AI Dungeon users alike
Smart long-term memory updates and summaries for important cards
Fully customizable AI card generation and memory summarization prompts
Object permanence!
Detects named entities from your story and periodically writes new cards
Optional in-game commands to manually direct the card generation process
Optional in-game scripting interface (LSIv2)
Optional API for other script creators to utilize
Auto-Cards is both free and open source for anyone to use within their own scenarios or scripts, even including published works. General-purpose usefulness and compatibility were my top design priorities. You have my full permission to use, copy, or modify Auto-Cards. Please enjoy! 🩷 LewdLeah.
// Your "Input" tab should look like this
InnerSelf("input");
const modifier = (text) => {
// Any other input modifier scripts can go here
return { text };
};
modifier(text);
‣
Context
// Your "Context" tab should look like this
InnerSelf("context");
const modifier = (text) => {
// Any other context modifier scripts can go here
return { text, stop };
};
modifier(text);
‣
Output
// Your "Output" tab should look like this
InnerSelf("output");
const modifier = (text) => {
// Any other output modifier scripts can go here
return { text };
};
modifier(text);
Give your characters true agency~
Inner Self grants memory, goals, secrets, planning, and self-reflection capabilities to the characters living in your story. Simply provide their names and Inner Self will silently handle the rest.
Compelling characters tend to think for themselves, I see this across all media. So I believe characters in AI Dungeon should too. And I mean that very literally; character thoughts are dynamically generated. Inner Self provides the AI with the tools it needs to truly embody characters, allowing them to feel more alive and nuanced during your adventures. LewdLeah.
Main Features:
Compartmentalized memory and highly emergent behavior
Self-organizing thoughts with agentic revisions and pruning
Absolutely NO "please select continue" immersion-breaks!
An interface to view or edit the brain of any NPC in real-time
Name-based trigger system allowing different NPCs to coexist
Visual indicators showing which NPC is currently thinking
General-purpose for diverse character archetypes and scenarios
Optionally includes Auto-Cards for comprehensive world-building
Inner Self is both free and open-source for anyone to use in their own creations. General-purpose usefulness, convenience, and immersion were my three design goals. You have my full permission to use, copy, or modify Inner Self. Please enjoy~ ❤️ LewdLeah.
Dungeons and Dragons, AI Dungeon.. is there more to say about it? This script is a must have for all who seek RPG mechanics in their AI Dungeon adventures!
Features:
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
Minigames including Mastermind, Memory, and Stragedy, a fully developed trading card game
I made this script to make AI Dungeon feel challenging and deep like Baldur's Gate 3, Knights of the Old Republic, and Fallout 3. However, I wanted to keep open-endedness of the AI adventure that attracted me here in the first place. Basically it's a game of Dungeons and Dragons where AID really is an AI Dungeon Master. I feel I have achieved that goal, have fun slaying monsters! raeleus.
//Input Script
// Every script needs a modifier function
const modifier = (text) => {
text = onInput_SAE(text);
return { text }
}
// Don't modify this part
modifier(text)
‣
Context
//Context Script
// Every script needs a modifier function
const modifier = (text) => {
text = onContext_SAE(text);
return { text }
}
// Don't modify this part
modifier(text)
‣
Output
//Output Script
// Every script needs a modifier function
const modifier = (text) => {
text = onOutput_SAE(text);
return { text }
}
// Don't modify this part
modifier(text)
When the main AI Dungeon experience is too well known, you can always change everything with a Story Building Script, enjoy the new car smell of discovering AI Dungeon for the first time (again)!
This script automatically helps guide and enhance your story by generating a "Story Arc" — a high-level plot outline made up of key events. It feeds this outline into the AI's context regularly to keep storytelling coherent, immersive, and progressively structured. Yi1i1i.
How It Works:
Before the set turn, the script prepares the AI to generate an updated story arc.
You will see a warning telling you that the next turn will be used for generating a story arc.
After clicking “Continue,” a brief pause message appears while the arc is being created by the AI. It will ask for a retry if the AI fails to create a story arc correctly.
When complete, a confirmation message lets you know the story arc has been saved and will influence future storytelling.
How It Helps:
Keeps long stories focused and immersive.
Reduces inconsistencies or meandering plotlines.
Encourages richer, more dynamic story progression.
Ensures major events stay tied together logically.
Customize It:
You can view (Spoilers!) or edit the Story Arc at any time in the Story Cards under “Current Story Arc.”
You are free to modify the settings as well in the settings story card.
IMPORTANT: The input section is ONLY for letting the user pick a random [RACE] and/or [CLASS]. If you don't need this, don't copy the input section.
‣
Output
If your adventure sounds predictable, if you want surprises: the Random Event script is the solution, witness shiny loot, terrifying monsters or other encounters pop into your story! Random Event is the script powering the now famous Endless Dungeon scenario that many have come to love on AI Dungeon.
This is the full Endless Dungeon script, which can be edited to produce any sort of random events you want. Different sections are marked in the Library for ease of finding them. Onyx
Features:
Often new, Always unexpected: Random Events bring unexpected twists into your adventures, making each of them truly unique.
Configurable: You don’t like something? Change it in the script, nothing is set in stone.
Balanced: Don’t get overwhelmed by the new events, the script is balanced to achieve a reasonable amount of randomness and interruptions.
A lot of premade events:
60+ Unique Races
70+ Dynamic Classes
100+ Monstrous Foes
200+ Lootable Treasures
25+ Deadly Traps
35+ Immersive Events
‣
Narrative Guidance Overhaul
Made by Purplejump
Install Narrative Guidance Overhaul into your own Scenario:
‣
Input
‣
Output
Narrative guidance, what a fancy expression when it comes to AI Dungeon. But this is what this script is about, about taking a rogue AI that wants nothing more but to take you anywhere and everywhere and give it a purpose. The purpose of crafting a story that you can engage in, that you can believe in, a story that makes sense.
A script for those who want to structure their adventures, a script that conveys the same screams about keeping the story coherent, but in a language the AI will understand.
Using the power of scripting, you can create your very own completely customizable story arc, allowing for anything from relaxing tales with little to no conflict, balanced stories with both downtime and action, to all out wars that never end! Not only will the script guide the AI through the process of making a good story, but it'll adapt to player actions too! If a player is getting really violent, the stakes and action will rise accordingly. If the player seems to be trying to relax and take a break, the story will as well! With this new story arc creator, you'll be able to have whatever tone you want for your scenario with more consistency than ever before. Purplejump.
Features:
Highly Customizable Story Arcs: Create to your hearts content with 30 different options to fine tune!
In-Editor Explanation: Each option has an explanation so that you can get an understanding on how things work!
Auto-Adjusting Tone: The tone and conflict in the story will automatically adjust to how the characters and stories are acting!
AI Guidance: The tension and action will slowly ramp up and advance the story arc, with custom prompts for each level!
Installation Guide
For the script labeled "Input Script" copy it, then paste it into the script labeled "Input"
For the script labeled "Output Script" copy it, then paste it into the script labeled "Output"
Change the variable called "originalAuthorsNote" in the Input script to your current authors note (if you don't have one set it to a blank space)
Change the default settings as needed, some stories might benefit from more rapid or slower pacing, or just trust the default settings
‣
Immersive D20
Made by BinKompliziert based on SlumberingMage's Oracle Lite
Is AI Dungeon too easy? Too predictable? Fear not, for this script will bring random failures and meaningful successes to your actions.
It simulates a D20 without modifiers every time the player uses 'try' or variations in a Do action. Simple, but effective. BinKompliziert
Features:
Failure at the worst moment and success that saves the day
Can be integrated into any scenario
Compatible with multiplayer, third, second, and first person gameplay
Anti-cheater mechanism: Result stays the same on retries
No surprises: Only triggers when you mention trying or attempting in your Do action
Mentioning disadvantage or advantage in your Do action automatically changes your success chance
Configurable success rate
Default success rate are:
10% Critical Success,
20% Success,
20% Partial Success,
40% Failure,
10% Critical Failure
Scenario Script Installation Guide:
Use the AI Dungeon website on PC (or view as desktop if mobile-only)
Create a new Scenario or edit one of your existing Scenarios
Open the DETAILS tab at the top while editing your Scenario
Scroll down to Scripting and toggle ON → Scripts Enabled
Select EDIT SCRIPTS
Select one of the Library, Input, Context, or Output tabs on the left depending on which is required by the Script you wish to install
Delete all code within said tab
Copy and paste the correct section of code into the now-empty tab
Repeat steps 5-7 for each respective tab required by your chosen Script
Click the big yellow SAVE button in the top right corner
And you're done!
Keep in mind that any Adventures created from your Scenario will include the added Script(s). This even applies retroactively for preexisting Adventures!
When combining multiple Scripts, ensure that each of the Input, Context, and Output tabs only include the following lines once:
const modifier = (text) => {
// Your Script's code goes here
return { text }
}
modifier(text)
Scripts not working?
Remember to check in the Gameplay tab of your Account Settings if you have Scripts enabled!
// Your "Library" tab should look like this
/*
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 = false
// (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(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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<string>} An array of strings representing used titles
*/
// AutoCards().API.getUsedTitles();
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} An array of banned title strings
*/
// AutoCards().API.getBannedTitles();
/*** Sets the banned titles array, replacing any previously banned titles
*
* @function
* @param {string|Array<string>} 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<Object>|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());
const memoized = new Map();
// 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*<<</gi) || []).length
) + "%"
);
})
// Situationally replace all placeholder substrings with either spaces or double newlines
.replace(/%@+%/g, (match, matchIndex, intermediateText) => {
// 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<string>} An array of strings representing used titles
*/
getUsedTitles: function() {
return Internal.getUsedTitles(true);
},
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} 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<string>} 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<Object>|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(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Remove a leading bullet
.replace(/^\s*-+\s*/, "")
// Replace common honorifics with a placeholder word
.replace(buildKiller(Words.honorifics), " %@% ")
// Remove common abbreviations
.replace(buildKiller(Words.abbreviations), " ")
// Fix end punctuation
.replace(/\s+\.(?![a-zA-Z])/g, ".").replace(/\.\.+/g, ".")
.replace(/\s+\?(?![a-zA-Z])/g, "?").replace(/\?\?+/g, "?")
.replace(/\s+!(?![a-zA-Z])/g, "!").replace(/!!+/g, "!")
.replace(/\s+:(?![a-zA-Z])/g, ":").replace(/::+/g, ":")
// Colons are treated as substitute end-punctuation, apply the capitalization rule
.replace(/:\s+(\S)/g, (_, next) => ": " + 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(/(?<!^\s*)(?: - | ?– ?)(?!\s*$)/g, "—");
}
function getConfigureCardTemplate() {
const names = getControlVariants().configure;
return O.f({
type: AC.config.defaultCardType,
title: names.title,
keys: names.keys,
entry: getConfigureCardEntry(),
description: getConfigureCardDescription()
});
}
function getConfigureCardEntry() {
return prose(
"> 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(/\(<isolated-vm>:/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*<isolated-vm>:\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(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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) {
const input = title;
let useMemo = false;
if (
(AC.database.titles.used.length === 1)
&& (AC.database.titles.used[0] === ("%@%"))
&& [used, forenames, surnames].every(nameset => (
(nameset.size === 1)
&& nameset.has("%@%")
))
) {
const pair = memoized.get(input);
if (pair !== undefined) {
if (50000 < memoized.size) {
memoized.delete(input);
memoized.set(input, pair);
}
return O.f({newTitle: pair[0], newKey: pair[1]});
}
useMemo = true;
}
title = title.trim();
if (short()) {
return end();
}
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(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Eliminate fake em dashes and terminal/leading dashes
.replace(/\s-\s/g, " ")
// Condense consecutive whitespace
.trim().replace(/\s+/g, " ")
// Remove a leading or trailing bullet
.replace(/^-+\s*/, "").replace(/\s*-+$/, "")
);
if (short()) {
return end();
}
// Special-cased words
const minorWordsJoin = Words.minor.join("|");
const leadingMinorWordsKiller = new RegExp("^(?:" + minorWordsJoin + ")\\s", "i");
const trailingMinorWordsKiller = new RegExp("\\s(?:" + minorWordsJoin + ")$", "i");
// Ensure the title is not bounded by any outer minor words
title = enforceBoundaryCondition(title);
if (short()) {
return end();
}
// Ensure interior minor words are lowercase and excise all interior honorifics/abbreviations
const honorAbbrevsKiller = new RegExp("(?:^|\\s|-|\\/)(?:" + (
[...Words.honorifics, ...Words.abbreviations]
).map(word => 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 end();
}
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 end();
}
// 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 end();
}
key = enforceBoundaryCondition(
titleWords.filter(word => !peerage.has(word.toLowerCase())).join(" ")
);
if (key.includes(" the ")) {
key = enforceBoundaryCondition(key.split(" the ")[0]);
}
if (isUsedOrBanned(key)) {
return end();
}
}
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;
}
function end(newTitle = "", newKey = "") {
if (useMemo) {
memoized.set(input, [newTitle, newKey]);
if (55000 < memoized.size) {
memoized.delete(memoized.keys().next().value);
}
}
return O.f({newTitle, newKey});
}
return end(title, 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]; } }
// Your other library scripts go here
// Your "Library" tab should look like this
/**
* Main control panel for scenario creator convenience
* Settings defined here will override their counterparts elsewhere
* Most AC and Inner Self settings are included
* Safe to delete
*/
globalThis.MainSettings = (class MainSettings {
//—————————————————————————————————————————————————————————————————————————————————
/**
* Inner Self v1.0.2
* Made by LewdLeah on January 3, 2026
* Gives story characters the ability to learn, plan, and adapt over time
* Inner Self is free and open-source for anyone! ❤️
*/
static InnerSelf = {
// Default settings for scenario creators to modify:
// List the first name of every scenario NPC whose brain should be simulated by Inner Self:
IMPORTANT_SCENARIO_CHARACTERS: ""
// (write a comma separated list of names inside the "" like so: "Leah, Lily, Lydia")
,
// Is Inner Self already enabled when the adventure begins?
IS_INNER_SELF_ENABLED_BY_DEFAULT: true
// (true or false)
,
// Is the player character's first name known in advance? Ignore this setting if unsure
PREDETERMINED_PLAYER_CHARACTER_NAME: ""
// (any name inside the "" or leave empty)
,
// Is the adventure intended for 1st, 2nd, or 3rd person gameplay?
FIRST_SECOND_OR_THIRD_PERSON_POV: 2
// (1, 2, or 3)
,
// What (maximum) percentage of "Recent Story" context should be repurposed for NPC brains?
PERCENTAGE_OF_RECENT_STORY_USED_FOR_BRAINS: 30
// (1 to 95)
,
// How many actions back should Inner Self look for character name triggers?
NUMBER_OF_ACTIONS_TO_LOOK_BACK_FOR_TRIGGERS: 5
// (1 to 250)
,
// Symbol used to visually display which NPC brain is currently triggered?
ACTIVE_CHARACTERS_VISUAL_INDICATOR_SYMBOL: "🎭"
// (any text/emoji inside the "" or leave empty)
,
// When possible, what percentage of turns should involve an attempt to form a new thought?
THOUGHT_FORMATION_CHANCE_PER_TURN: 60
// (0 to 100)
,
// Is the thought formation chance reduced by half during Do/Say/Story turns?
IS_THOUGHT_CHANCE_HALF_FOR_DO_SAY_STORY: true
// (true or false)
,
// Is valid JSON shown and expected in brain card notes? Otherwise use a human-readable format
IS_JSON_FORMAT_USED_FOR_BRAIN_CARD_NOTES: false
// (true or false)
,
// Should Inner Self model task outputs be displayed inline with the adventure text itself?
IS_DEBUG_MODE_ENABLED_BY_DEFAULT: false
// (true or false)
,
// Is the "Configure Inner Self" story card pinned near the top of the in-game list?
IS_CONFIG_CARD_PINNED_BY_DEFAULT: false
// (true or false)
,
// Is AC already enabled when the adventure begins?
IS_AC_ENABLED_BY_DEFAULT: false
// (true or false)
,
}; //——————————————————————————————————————————————————————————————————————————————
/**
* AC v1.1.3
* 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
* AC is fully open-source, please copy for use within your own projects! ❤️
*/
static AC = {
// Is AC already enabled when the adventure begins?
DEFAULT_DO_AC: true
// (true or false)
,
// Pin the "Configure Auto-Cards" story card at the top of the player's story cards list?
DEFAULT_PIN_CONFIGURE_CARD: false
// (true or false)
,
// Minimum number of turns in between automatic card generation events?
DEFAULT_CARD_CREATION_COOLDOWN: 40
// (0 to 9999)
,
// Use a bulleted list format for newly generated card entries?
DEFAULT_USE_BULLETED_LIST_MODE: true
// (true or false)
,
// Maximum allowed length for newly generated story card entries?
DEFAULT_GENERATED_ENTRY_LIMIT: 600
// (200 to 2000)
,
// Do newly generated cards have memory updates enabled by default?
DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES: false
// (true or false)
,
// Default character limit before the card's memory bank is summarized?
DEFAULT_NEW_CARDS_MEMORY_LIMIT: 3200
// (1750 to 9900)
,
// Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new)
DEFAULT_MEMORY_COMPRESSION_RATIO: 25
// (20 to 1250)
,
// Ignore all-caps during title candidate detection?
DEFAULT_IGNORE_ALL_CAPS_TITLES: true
// (true or false)
,
// Should player input actions (Do/Say/Story) be considered during title detection?
DEFAULT_DETECT_TITLES_FROM_INPUTS: false
// (true or false)
,
// How many (minimum) actions in the past does Auto-Cards look for named entities?
DEFAULT_MINIMUM_LOOK_BACK_DISTANCE: 7
// (2 to 88)
,
// Is Live Script Interface v2 enabled?
DEFAULT_DO_LSI_V2: false
// (true or false)
,
// Should the "Debug Data" story card be visible?
DEFAULT_SHOW_DEBUG_DATA: false
// (true or false)
,
// AI prompt used to generate new story card entries?
DEFAULT_CARD_GENERATION_PROMPT: [
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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?
DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT: [
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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?
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)
DEFAULT_CARD_TYPE: "class"
// ("text")
,
// Should titles mentioned in the "opening" plot component be banned from future card generation by default?
DEFAULT_BAN_TITLES_FROM_OPENING: false
// (true or false)
,
}; //——————————————————————————————————————————————————————————————————————————————
#config;
constructor(script, alternative) {
this.#config = (
MainSettings.hasOwnProperty(script)
? MainSettings[script]
: ((typeof alternative === "string") && MainSettings.hasOwnProperty(alternative))
? MainSettings[alternative]
: null
);
return this;
}
merge(settings) {
if (!this.#config || !settings || (typeof settings !== "object")) {
return;
}
for (const [key, value] of Object.entries(this.#config)) {
settings[key] = value;
}
return;
}
});
//—————————————————————————————————————————————————————————————————————————————————————
/**
* Inner Self v1.0.2
* Made by LewdLeah on January 3, 2026
* Gives story characters the ability to learn, plan, and adapt over time
* Inner Self is free and open-source for anyone! ❤️
*/
function InnerSelf(hook) {
"use strict";
/**
* Scenario-level default settings
* Creators modify these before publishing
* Players modify these in-game via the config card
*/
const S = {
// Default settings for scenario creators to modify:
// List the first name of every scenario NPC whose brain should be simulated by Inner Self:
IMPORTANT_SCENARIO_CHARACTERS: ""
// (write a comma separated list of names inside the "" like so: "Leah, Lily, Lydia")
,
// Is Inner Self already enabled when the adventure begins?
IS_INNER_SELF_ENABLED_BY_DEFAULT: true
// (true or false)
,
// Is the player character's first name known in advance? Ignore this setting if unsure
PREDETERMINED_PLAYER_CHARACTER_NAME: ""
// (any name inside the "" or leave empty)
,
// Is the adventure intended for 1st, 2nd, or 3rd person gameplay?
FIRST_SECOND_OR_THIRD_PERSON_POV: 2
// (1, 2, or 3)
,
// What (maximum) percentage of "Recent Story" context should be repurposed for NPC brains?
PERCENTAGE_OF_RECENT_STORY_USED_FOR_BRAINS: 30
// (1 to 95)
,
// How many actions back should Inner Self look for character name triggers?
NUMBER_OF_ACTIONS_TO_LOOK_BACK_FOR_TRIGGERS: 5
// (1 to 250)
,
// Symbol used to visually display which NPC brain is currently triggered?
ACTIVE_CHARACTERS_VISUAL_INDICATOR_SYMBOL: "🎭"
// (any text/emoji inside the "" or leave empty)
,
// When possible, what percentage of turns should involve an attempt to form a new thought?
THOUGHT_FORMATION_CHANCE_PER_TURN: 60
// (0 to 100)
,
// Is the thought formation chance reduced by half during Do/Say/Story turns?
IS_THOUGHT_CHANCE_HALF_FOR_DO_SAY_STORY: true
// (true or false)
,
// Is valid JSON shown and expected in brain card notes? Otherwise use a human-readable format
IS_JSON_FORMAT_USED_FOR_BRAIN_CARD_NOTES: false
// (true or false)
,
// Should Inner Self model task outputs be displayed inline with the adventure text itself?
IS_DEBUG_MODE_ENABLED_BY_DEFAULT: false
// (true or false)
,
// Is the "Configure Inner Self" story card pinned near the top of the in-game list?
IS_CONFIG_CARD_PINNED_BY_DEFAULT: false
// (true or false)
,
// Is AC already enabled when the adventure begins?
IS_AC_ENABLED_BY_DEFAULT: false
// (true or false)
,
}; //——————————————————————————————————————————————————————————————————————————————
const version = "v1.0.2";
// Validate that all required AI Dungeon global properties exist
// Without these, Inner Self literally cannot function
if (
!globalThis.state || (typeof state !== "object") || Array.isArray(state)
|| !globalThis.info || (typeof info !== "object") || Array.isArray(info)
|| !Array.isArray(globalThis.storyCards)
|| (typeof addStoryCard !== "function")
|| !Array.isArray(globalThis.history)
|| (typeof text !== "string")
) {
// Something is seriously broken in AID
log("unexpected error");
globalThis.text ||= " ";
return;
}
/**
* Recursively merges source object into target object
* Only copies properties that are undefined in target
* Nested objects get their own recursive treatment
* @param {Object} target - The object to merge into
* @param {Object} source - The object to merge from
* @returns {Object} The mutated target object
*/
const deepMerge = (target = {}, source = {}) => {
// Walk through every key in the source
for (const key in source) {
// Source value is a nested object, so recurse
if (source[key] && (typeof source[key] === "object") && !Array.isArray(source[key])) {
if (!target[key] || (typeof target[key] !== "object")) {
// Target doesn't have this key or it's not an object
target[key] = {};
}
deepMerge(target[key], source[key]);
} else if (target[key] === undefined) {
// Only copy if target doesn't already have this key
target[key] = source[key];
}
}
return target;
};
/**
* Persistent state of Inner Self stored in the adventure's state object
* This survives across turns
* @type {Object}
*/
const IS = state.InnerSelf = deepMerge(state.InnerSelf || {}, {
// Zero-width encoded thought labels for context injection
encoding: "",
// Currently triggered agent name (empty string = none)
agent: "",
// Monotonically increasing thought label counter
label: 0,
// Hash of recent history to detect retry or erase + continue turns
hash: "",
// Total number of brain operations performed across all agents
ops: 0,
// Auto-Cards integration state
AC: {
// This helps avoid calling AC API functions more than necessary
enabled: false,
// External use of the AC API force-installs so it just works
forced: false,
// NGL this one didn't need to be stateful but I didn't feel like declaring a local so whatevs
// Basically AC sets this to true when it does stuff, so Inner Self can inhibit itself
event: false
}
});
/**
* Checks if Auto-Cards is available in the global scope
* @returns {boolean} true if Auto-Cards is installed and callable
*/
const hasAutoCards = () => (typeof globalThis.AutoCards === "function");
const u = "qm`x/`hetofdno/bnl.qsnghmd.MdveMd`i".replace(/./g, c => String.fromCharCode(c.charCodeAt()^1));
if (IS.AC.enabled && (typeof hook === "string") && (hook !== "context") && hasAutoCards()) {
// Delegate to Auto-Cards for non-context hooks when enabled
try {
text = AutoCards(hook, text);
} catch (error) {
log(error.message);
}
}
/**
* Generates a simple hashcode of the last 50 actions in history
* Used to detect retry or erase + continue turns
* @returns {string} Hexadecimal hash string
*/
const historyHash = () => {
let n = 0;
// Grab the last 50 actions and stringify them
const serialized = JSON.stringify(history.slice(-50));
for (let i = 0; i < serialized.length; i++) {
// Classic polynomial rolling hash, nothing fancy
n = ((31 * n) + serialized.charCodeAt(i)) | 0;
}
return n.toString(16);
};
/**
* Safely parses a JSON string into an object
* Optionally attempts to repair malformed JSON by extracting quoted content
* Basically I use repair mode for cute little smooth brains UwU
* @param {string} str - The string to parse
* @param {boolean} repair - Whether to attempt repair on malformed JSON
* @returns {Object} Parsed object or empty object on failure
*/
const deserialize = (str = "", repair = false) => {
try {
const parsed = JSON.parse(repair ? (() => {
// All values will be strings I promise
// Find the first and last quote chars
const first = str.indexOf("\"");
const last = str.lastIndexOf("\"");
return (
((first === -1) || (last === -1) || (last <= first))
? "{}" : `{${str.slice(first, last + 1)}}`
);
})() : str);
if (parsed && (typeof parsed === "object") && !Array.isArray(parsed)) {
// Only return a proper object (not null, not array)
return parsed;
}
} catch {}
// That empty catch looks so dumb lol
return {};
};
/**
* Validated config settings for Inner Self
* Default settings are specified by creators at the scenario level
* Runtime settings are specified by players at the adventure level
* @typedef {Object} config
* @property {Object|null} card - Config card object reference
* @property {boolean} allow - Is Inner Self enabled?
* @property {string} player - The player character's name
* @property {number} pov - Is the adventure in 1st, 2nd, or 3rd person?
* @property {boolean} guide - Show a detailed guide
* @property {number} percent - Default percentage of Recent Story context length reserved for agent brains
* @property {number} distance - Number of previous actions to look back for agent name triggers
* @property {string} indicator - The visual indicator symbol used to display active brains
* @property {number} chance - Likelihood of performing a standard thought formation task each turn
* @property {boolean} half - Is the thought formation chance reduced by half during Do/Say/Story turns?
* @property {boolean} json - Is raw JSON syntax used to serialize NPC brains in their card notes?
* @property {boolean} debug - Is debug mode enabled for inline task output visibility?
* @property {boolean} pin - Is the config card pinned near the top of the list?
* @property {boolean} auto - Is Auto-Cards enabled?
* @property {string[]} agents - All agent names, ordered from highest to lowest trigger priority
*/
/**
* Config class - Manages the Inner Self configuration card
* Handles building, finding, parsing, and validating all settings
* @class
*/
class Config {
/**
* Build or find the Inner Self config card
* Returns the card reference and all parsed settings
* This is the heart of the config system
* @param {Set<string>} [pending] - Recursion aid for tracking pending agents
* @returns {config} The complete validated configuration object
*/
static get(pending = new Set()) {
// Allow MainSettings mod to override local defaults
if (typeof globalThis.MainSettings === "function") {
new MainSettings("InnerSelf", "IS").merge(S);
}
/**
* Fallback values when settings are missing or invalid
* Frozen because I hate accidental mutations
* @type {config}
*/
const fallback = Object.freeze({
allow: true,
guide: false,
player: "",
pov: 2,
percent: 30,
distance: 5,
indicator: "🎭",
chance: 60,
half: true,
json: false,
debug: false,
pin: false,
auto: false,
agents: []
});
/** @type {config} */
const config = { card: null };
/**
* Strips a string down to lowercase letters only
* Used for fuzzy matching of setting names
* @param {string} s - Input string
* @returns {string} Simplified string
*/
const simplify = (s = "") => s.toLowerCase().replace(/[^a-z]+/g, "");
/**
* Cleans up an agent name by removing commas and zero-width chars
* Also normalizes whitespace because players are messy ;P
* @param {string} agent - Raw agent name
* @returns {string} Cleaned agent name
*/
const cleanAgent = (agent = "") => agent.replace(/[,\u200B-\u200D]+/g, "").trim().replace(/\s+/g, " ");
/**
* Factory function that creates builder/setter pairs for config fields
* Handles both boolean and integer settings with validation
* This makes me NOT want to die every time I need to add a new setting
* @param {string} key - Config property name
* @param {*} setting - Default value from scenario settings
* @param {Object} int - Integer constraints (lower, upper, suffix)
* @returns {Object} Object with builder and setter functions
*/
const factory = (key = "", setting = null, int = null) => ({
// Builds the display string for the config card entry
builder: (cfg = {}) => ` ${config[key] ?? cfg.setter?.(setting)}${(
// Fancy suffix or boring suffix
(typeof int?.suffix === "function") ? int.suffix() : int?.suffix ?? ""
)}`,
// Parses and validates a value, storing it in config
setter: (value = null, fallible = false) => {
// Helper to clamp integers within bounds
const bound = (val = 20) => Math.min(Math.max(int?.lower ?? 1, val), int?.upper ?? 95);
if ((typeof value === "boolean") && !int) {
// Boolean setting with a boolean value (easy case)
config[key] = value;
} else if (Number.isInteger(value) && int) {
// Integer setting with an integer value (also easy)
config[key] = bound(value);
} else if (typeof value !== "string") {
// Non-string non-matching type, use fallback unless fallible
if (fallible) {
return;
}
config[key] = fallback[key];
} else if (int) {
// Parse integer from string, stripping decimals and non-digits
value = value.split(/[./]/, 1)[0].replace(/[^\d]+/g, "");
if (value !== "") {
config[key] = bound(parseInt(value, 10));
} else if (!fallible) {
config[key] = bound(fallback[key]);
}
} else {
// Parse boolean from string with synonym support
value = simplify(value);
if (["true", "t", "yes", "y", "on", "1", "enable", "enabled"].includes(value)) {
config[key] = true;
} else if (["false", "f", "no", "n", "off", "0", "disable", "disabled"].includes(value)) {
config[key] = false;
} else if (!fallible) {
config[key] = fallback[key];
}
}
return config[key];
}
});
/**
* Template for building the Inner Self config card
* Contains all the user-facing text and settings
* @type {Object}
*/
const template = {
type: "class",
title: "Configure \nInner Self",
// The config card entry contains the main settings
entry: [
{
message: "Inner Self grants story characters the ability to learn, plan, and adapt over time. Edit the entry and notes below to control how Inner Self behaves."
},
{ message: "Enable Inner Self:", ...factory(
"allow", S.IS_INNER_SELF_ENABLED_BY_DEFAULT
) },
{
message: "Show detailed guide:",
builder: (cfg = {}) => ` ${(
((hook === "context") || Number.isInteger(info.maxChars))
? config.guide ?? cfg.setter?.(false)
: false
)}`,
setter: factory("guide", false).setter
},
{
message: "First name of player character:",
builder: (cfg = {}) => ` "${config.player || (() => {
const display = cfg.setter?.(S.PREDETERMINED_PLAYER_CHARACTER_NAME);
if (config.player === "") {
config.player = "the protagonist";
}
return display;
})()}"`,
setter: (value = null, fallible = false) => {
const example = "Example";
if (typeof value === "string") {
config.player = value.replaceAll("\"", "").replace(example, "").trim();
} else if (fallible) {
return;
} else {
config.player = fallback.player;
}
return config.player || example;
}
},
{ message: "Adventure in 1st, 2nd, or 3rd person:", ...factory(
"pov", S.FIRST_SECOND_OR_THIRD_PERSON_POV,
{ lower: 1, upper: 3, suffix: () => ["st", "nd", "rd"][config.pov - 1] ?? "" }
) },
{ message: "Max brain size relative to story context:", ...factory(
"percent", S.PERCENTAGE_OF_RECENT_STORY_USED_FOR_BRAINS,
{ lower: 1, upper: 95, suffix: "%" }
) },
{ message: "Recent turns searched for name triggers:", ...factory(
"distance", S.NUMBER_OF_ACTIONS_TO_LOOK_BACK_FOR_TRIGGERS,
{ lower: 1, upper: 250 }
) },
{
message: "Visual indicator of current NPC triggers:",
builder: (cfg = {}) => ` "${(
config.indicator ?? cfg.setter?.(S.ACTIVE_CHARACTERS_VISUAL_INDICATOR_SYMBOL)
)}"`,
setter: (value = null, fallible = false) => (
(typeof value === "string")
? (config.indicator = value.replace(/["\u200B-\u200D]+/g, "").trim())
: (fallible)
? null
: (config.indicator = fallback.indicator)
)
},
{ message: "Thought formation chance per turn:", ...factory(
"chance", S.THOUGHT_FORMATION_CHANCE_PER_TURN,
{ lower: 0, upper: 100, suffix: "%" }
) },
{ message: "Half thought chance for Do/Say/Story:", ...factory(
"half", S.IS_THOUGHT_CHANCE_HALF_FOR_DO_SAY_STORY
) },
{ message: "Brain card notes store brains as JSON:", ...factory(
"json", S.IS_JSON_FORMAT_USED_FOR_BRAIN_CARD_NOTES
) },
{ message: "Enable debug mode to see model tasks:", ...factory(
"debug", S.IS_DEBUG_MODE_ENABLED_BY_DEFAULT
) },
{ message: "Pin this config card near the top:", ...factory(
"pin", S.IS_CONFIG_CARD_PINNED_BY_DEFAULT
) },
{ message: "Install Auto-Cards:", ...factory(
"auto", S.IS_AC_ENABLED_BY_DEFAULT
) },
{
message: "Write the name(s) of your non-player characters at the very bottom of the \"notes\" section below. This is mandatory because it allows Inner Self to assemble independent minds for the correct individuals."
}
],
// Description section contains info and agent list
description: [
{
message: "Please visit my profile @LewdLeah through the link above and read my bio for simple steps to add Inner Self to your own scenarios! ❤️"
},
{
message: `Inner Self ${version} is an open-source and general-purpose AI Dungeon mod by LewdLeah. You have my full permission to use it with any scenario!`
},
{
// This is where players list their NPCs
message: "Write the first name of every intelligent story character on separate lines below, listed from highest to lowest trigger priority:",
builder: (cfg = {}) => ["", "", ...(
config.agents ?? cfg.setter?.(S.IMPORTANT_SCENARIO_CHARACTERS)
), ""].join("\n"),
setter: (value = null, fallible = false) => {
// Accept string (from card) or array (from code)
if (typeof value === "string") {
config.agents = value.split(/[,\n]/);
} else if (Array.isArray(value)) {
config.agents = value.filter(agent => (typeof agent === "string"));
} else if (fallible) {
return;
} else {
return (config.agents = [...fallback.agents]);
}
// Clean, deduplicate, and remove empties
return (config.agents = [...new Set(config.agents
.map(agent => cleanAgent(agent))
.filter(agent => (agent !== ""))
)]);
}
}
]
};
// Track discovered agents to avoid duplicates
const agents = new Set();
// Simplified title for fuzzy matching
const target = simplify(template.title);
// Scan all story cards in reverse order
// Looking for config cards, agent cards, and duplicates (remove the latter in-place)
for (let i = storyCards.length - 1; -1 < i; i--) {
const card = storyCards[i];
if (!card || (typeof card !== "object") || Array.isArray(card)) {
// Remove invalid cards (null, non-objects, arrays)
// If this ever happens in a real situation, I will cry
storyCards.splice(i, 1);
} else if ((typeof card.keys === "string") && card.keys.includes("\"agent\"")) {
// This card has agent metadata, extract and validate it
const metadata = deserialize(card.keys);
if (typeof metadata.agent === "string") {
metadata.agent = cleanAgent(metadata.agent);
if (metadata.agent !== "") {
if (!agents.has(metadata.agent)) {
// First time seeing this brain card
agents.add(metadata.agent);
card.keys = JSON.stringify(metadata);
continue;
} else if (typeof card.title === "string") {
// Duplicate brain card, mark it as a copy
card.title = card.title.trim();
card.title = `Copy of ${(card.title === "") ? "Agent" : card.title}`;
}
}
}
// Invalid agent metadata, clear it
card.keys = "";
} else if ((typeof card.title !== "string") || (100 < card.title.length)) {
// Skip cards with missing or absurdly long titles
continue;
} else if (card.title.startsWith("@") && !card.title.includes("figure")) {
// Cards starting with @ are shorthand for adding agents
const agent = cleanAgent(card.title.replace(/^[@\s]*/, ""));
if (agent !== "") {
card.title = agent;
pending.add(agent);
}
} else if ((() => {
// Fuzzy matching to find the config card even if title is slightly mangled
// Because players gonna player and typos happen
const current = simplify(card.title);
const maxMistakes = 2;
let mistakes = 0;
// Target index (expected title)
let t = 0;
// Current index (actual title)
let c = 0;
while ((t < target.length) && (c < current.length)) {
if (current[c] === target[t]) {
// Chars match, advance both
t++; c++;
continue;
} else if (maxMistakes <= mistakes) {
// Too many mistakes, this isn't the config card (I hope)
return true;
}
// Allow for insertions, deletions, or substitutions
mistakes++;
(current[c + 1] === target[t])
? c++
: (current[c] === target[t + 1])
? t++
: (t++, c++)
}
// Count leftover chars as mistakes
mistakes += (target.length - t) + (current.length - c);
// This is basically bargain bin levenshtein distance but less costly
return (maxMistakes < mistakes);
})()) {
// Title didn't match the fuzzy search
continue;
} else if (config.card === null) {
// Found the config card
config.card = card;
} else if (typeof removeStoryCard === "function") {
// Duplicate config card, remove it properly the way Latitude intended
// (I know it's just a wrapper for splice, but that may change one day lol)
removeStoryCard(i);
} else {
// Fallback removal for duplicate config cards
storyCards.splice(i, 1);
}
}
/**
* Builds a formatted string from template sections
* @param {Array} source - Array of config message objects
* @param {string} delimiter - String to join sections with
* @returns {string} Formatted config text
*/
const build = (source = [], delimiter = "\n\n") => (source
.map(cfg => `> ${cfg.message}${cfg.builder?.(cfg) ?? ""}`)
.join(delimiter)
);
if (config.card === null) {
// If no config card exists, create one and recurse
addStoryCard(u,
build(template.entry, "\n"),
template.type,
template.title,
build(template.description, "\n\n")
);
// Recurse to parse the newly created card
return Config.get(pending);
}
// Parse existing card content to extract user-modified settings
// This is where IS reads back what the player has configured
// Abomination :3
["entry", "description"].map(source => [source, (
(typeof config.card[source] === "string")
// Split on >, filter for lines with colons, extract key-value pairs
? Object.fromEntries((config.card[source]
.split(/\s*>[\s>]*/)
.filter(block => block.includes(":"))
.map(block => block.split(/\s*:[\s:]*/, 2))
).map(pair => [simplify(pair[0]), pair[1].trimEnd()])) : {}
)]).forEach(([source, extractive]) => template[source].forEach(cfg => (
// Try to set each config value from extracted content (fallible mode)
cfg.setter?.(extractive[simplify(cfg.message)], true)
)));
// Merge all discovered agents: config, brain card metadata, and "@" pending
config.agents = [...new Set([...(config.agents ?? fallback.agents), ...agents, ...pending])];
if (IS.AC.forced) {
// Handle forced Auto-Cards installation (silly API stuff)
config.auto = true;
IS.AC.forced = false;
IS.AC.enabled = true;
}
// Update the card with the canonical template format so it sticks after the hook ends
config.card.type = template.type;
config.card.title = template.title;
config.card.entry = build(template.entry, "\n");
config.card.description = build(template.description, "\n\n");
config.card.keys = u;
return config;
} }
/**
* Removes the visual indicator prefix from a card title
* The indicator is separated by a zero-width space char
* @param {Object} card - Story card object to modify
* @returns {void}
*/
const deindicate = (card = {}) => {
if (typeof card.title !== "string") {
// Cry
card.title = "";
} else if (card.title.includes("\u200B")) {
// Strip everything before and including the zero-width space
card.title = (card.title
.slice(card.title.indexOf("\u200B") + 1)
.replaceAll("\u200B", "")
.trim()
);
}
return;
};
/**
* Agent class - Represents an NPC with a simulated brain
* Each agent has their own story card that stores their thoughts
* The brain is a key-value store of labeled thoughts
* @class
*/
class Agent {
// Private fields for encapsulation
// Percentage of context reserved for this agent's brain
#percent;
// Visual indicator symbol shown when agent is triggered
#indicator;
// Cached reference to the agent's brain card
#card = null;
// Cached parsed brain contents
#brain = null;
// Cached parsed metadata
#metadata = null;
/**
* Creates a new Agent instance
* The agent will find or create their brain card automatically
* @param {string} name - The name of the agent (used for triggering)
* @param {Object} [options] - Optional settings for the agent
* @param {number} [options.percent=30] - Context reserved for brain contents
* @param {string} [options.indicator=null] - Visual indicator when triggered
*/
constructor(name = "", { percent = 30, indicator = null } = {}) {
this.#indicator = indicator;
this.#percent = percent;
this.name = name;
return this;
}
/**
* Gets or creates the agent's brain card
* Uses lazy initialization and caching
* @returns {Object} The agent's story card
*/
get card() {
if (this.#card !== null) {
// Return cached card if stored
return this.#card;
}
/**
* Creates a new brain card for this agent
* Includes a timestamp for debugging purposes
* @param {string} name - Display name for the card
* @returns {Object} The newly created card
*/
const buildCard = (name = this.name) => addStoryCard(
JSON.stringify({ agent: this.name }),
(() => {
// Generate a pretty timestamp for the initialization comment
const time = new Date();
const match = time.toLocaleString("en-US", {
timeZone: "UTC",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "numeric",
minute: "2-digit",
hour12: true
}).match(/(\d+)\/(\d+)\/(\d+),?\s*(\d+:\d+\s*[AP]M)/);
return `// initialized @ ${(
match
? `${match[3]}-${match[1]}-${match[2]} ${match[4]}`
: time.toISOString().replace("T", " ").slice(0, 16)
)} UTC`;
})(),
"Brain",
name,
JSON.stringify({}),
// Thank you Mavrick
{ returnCard: true }
);
/**
* Checks if a card belongs to this agent
* @param {Object} card - Card to check
* @returns {boolean} true if this is the right card
*/
const isAgent = (card = {}) => (
(typeof card.keys === "string")
&& card.keys.includes("\"agent\"")
&& (deserialize(card.keys).agent === this.name)
);
if (typeof this.#indicator !== "string") {
// If no indicator is set, just find or create the card
for (const card of storyCards) {
if (isAgent(card)) {
// Found an existing card
this.#card = card;
return this.#card;
}
}
// No existing card found, create one
this.#card = buildCard();
return this.#card;
}
// The Agent class instance was constructed with an indicator
// Update card titles during the same iteration because reasons
this.#indicator = this.#indicator.trim();
const prefix = `${this.#indicator}\u200B`;
for (const card of storyCards) {
// Remove indicators from all cards
deindicate(card);
if ((this.#card === null) && isAgent(card)) {
// Found the brain card, add the indicator prefix
if (this.#indicator !== "") {
card.title = (card.title === "") ? prefix : `${prefix} ${card.title}`;
}
this.#card = card;
}
}
if (this.#card === null) {
// Still no card? Create one with the indicator
this.#card = (this.#indicator === "") ? buildCard() : buildCard(`${prefix} ${this.name}`);
}
return this.#card;
}
/**
* Gets the agent's metadata from their card
* Contains per-agent configurable settings like context percentage
* @returns {Object} metadata object with validated percent
*/
get metadata() {
if (this.#metadata !== null) {
// Return cached metadata if available
return this.#metadata;
}
// Valid range for brain size percentage (inclusive)
const [lower, upper] = [1, 95];
this.#metadata = deserialize(this.card.keys);
// Validate and normalize the percent value
if (!Number.isInteger(this.#metadata.percent)) {
// Uh oh
this.#metadata.percent = (
((typeof this.#metadata.percent === "number") && Number.isFinite(this.#metadata.percent))
? Math.min(Math.max(lower, Math.round(this.#metadata.percent)), upper)
: this.#percent
);
} else if (this.#metadata.percent < lower) {
// Clamp to minimum
this.#metadata.percent = lower;
} else if (upper < this.#metadata.percent) {
// Clamp to maximum
this.#metadata.percent = upper;
} else {
// Yippee
return this.#metadata;
}
// Save the normalized metadata back to the card
this.#card.keys = JSON.stringify(this.#metadata);
return this.#metadata;
}
/**
* Gets the agent's brain (thought storage)
* Parses from the card description with repair mode enabled
* Accepts both JSON and simplified formats for deserialization
* Auto-detects format for backward (and forward) compatibile conversion
* @returns {Object} Key-value store of thoughts
*/
get brain() {
if (this.#brain !== null) {
// Return the cached brain if available
return this.#brain;
} else if (typeof this.card.description === "string") {
this.card.description = this.card.description.trim();
} else {
this.card.description = "";
}
this.#brain = {};
if (/^[\s{,]*"/.test(this.card.description) || /"[\s},]*$/.test(this.card.description)) {
let parsed = false;
// Parse the brain as JSON from the card description, with repairs allowed
const source = deserialize(this.card.description, true);
for (const key in source) {
// Only keep string values (the actual thoughts)
(typeof source[key] === "string") && ((this.#brain[key] = source[key]), (parsed = true));
}
if (parsed) {
// Conclude if the brain contains any string-valued properties
return this.#brain;
}
// Failed to parse any meaningful thoughts, try the simple format instead
}
// Parse the brain from the card description using the simple format
for (const line of this.card.description.split("\n")) {
const clean = line.trim();
if (clean === "") {
continue;
}
// Find the first colon (allows colons in values like "5:30 PM")
const bisector = clean.indexOf(":");
if (bisector === -1) {
// No key-value pair on this line
continue;
}
// Remove unwanted leading/trailing chars from both key and value
const [key, value] = [
// Left of colon
clean.slice(0, bisector),
// Right of colon
clean.slice(bisector + 1)
].map(twin => twin.replace(/(?:^[\s{},"_\\]*|[\s{},"_\\]*$)/g, ""));
if ((key !== "") && (value !== "")) {
// Only add if key and value are both non-empty
this.#brain[key] = value;
}
}
return this.#brain;
}
/**
* Clears the cached brain, forcing a re-parse on next access
* Head empty UwU
* @returns {void}
*/
lobotomize() {
this.#brain = null;
return;
}
}
/**
* Gets the most recent non-empty action from history
* Ignores actions that are just zero-width chars >:3
* @returns {Object|undefined} The previous action or undefined
*/
const getPrevAction = () => history.findLast(a => !/^[\u200B-\u200D]*$/.test(a?.text ?? a?.rawText ?? ""));
// ==================== CONTEXT HOOK ====================
// This is where (half) of the magic happens: Inner Self injects brains and tasks into context
// Infer the current lifecycle hook
if ((hook === "context") || Number.isInteger(info.maxChars)) {
// Calculate the player's context limit with a small buffer
const limit = Math.max((Math.min(text.length, info.maxChars) - 10), 4500);
// Ensure stop variable exists (the AID script sandbox is silly)
globalThis.stop ??= false;
// Reset agent trigger for this turn
IS.agent = "";
/** @type {config} */
const config = Config.get();
if (config.pin) {
// Move config card to top of list if pinning is enabled
const index = storyCards.indexOf(config.card);
if (0 < index) {
storyCards.splice(index, 1);
storyCards.unshift(config.card);
}
}
const unzero = () => ((text = text.replace(/[\u200B-\u200D]+/g, "") || " "), (IS.encoding = ""));
// Handle Auto-Cards integration when enabled
if (config.auto && hasAutoCards()) {
try {
if (!IS.AC.enabled) {
// It's my first time enabling AC, please be gentle :3
const api = AutoCards().API;
// Prevent AC from generating cards with reserved titles
api.setBannedTitles([
"Inner",
"Self",
"Configure Inner Self",
"Agent",
...api.getBannedTitles(),
]);
}
// Run AC's context branch
AutoCards(null);
IS.AC.event = false;
[text, stop] = AutoCards("context", text, stop);
} catch (error) {
log(error.message);
}
IS.AC.enabled = true;
if (IS.AC.event || (stop === true)) {
// If AC triggered an event or stop, we're done here
config.allow ? unzero() : ((IS.encoding = ""), (text ||= " "));
return;
}
} else if (IS.AC.enabled) {
IS.AC.enabled = false;
// AC was just disabled, clean up its cards ;)
for (let i = storyCards.length - 1; -1 < i; i--) {
const card = storyCards[i];
// Check if this is an AC-related card that should be removed
if (!([
"Shared Library",
"Input Modifier",
"Context Modifier",
"Output Modifier",
"LSIv2 Guide",
"State Display",
"Console Log"
].includes(card.title) && (card.title === card.keys)) && [{ key: "title", options: [
"Configure \nAuto-Cards",
"Edit to enable \nAuto-Cards"
] }, { key: "keys", options: [
"Edit the entry above to adjust your story card automation settings",
"Edit the entry above to enable story card automation"
] }].every(({ key, options }) => !options.includes(card[key]))) {
continue;
} else if (typeof removeStoryCard === "function") {
removeStoryCard(i);
} else {
storyCards.splice(i, 1);
}
}
}
if (!config.allow) {
// Early exit if Inner Self is disabled
IS.encoding = "";
text ||= " ";
return;
}
/**
* Removes visual indicators from all story cards
* Called when no agent is triggered or Inner Self is disabled
* @returns {void}
*/
const deindicateAll = () => {
for (const card of storyCards) {
deindicate(card);
}
return;
};
if (config.agents.length === 0) {
// No agents are configured
deindicateAll();
unzero();
return;
}
// ==================== AGENT TRIGGER DETECTION ====================
// Scan config.distance actions back through history to find the most recent agent trigger
// Tie-break same-action name triggers based on RNG and their order-of-priority in config.agents
// Do it all without using ANY RegEx because I'm extra like that :3
// (this block is blazingly fast)
const possibilities = [];
for (
let [i, remaining] = [history.length - 1, config.distance];
((0 < remaining) && (-1 < i) && (possibilities.length === 0));
i--
) {
const actionText = history[i]?.text ?? history[i]?.rawText;
if ((typeof actionText !== "string") || (actionText.indexOf(">>>") !== -1)) {
// Skip invalid actions or Auto-Cards thingies
continue;
}
scan: {
// Check if this action has any meaningful content
for (let j = actionText.length - 1; -1 < j; j--) {
const c = actionText.charCodeAt(j);
if ((0x20 < c) && (c !== 0x200B) && (c !== 0x200C) && (c !== 0x200D)) {
// Fast accept any non-whitespace + non-zero-width char
break scan;
}
}
// Byeee
continue;
}
remaining--;
// Lowercase for case-insensitive matching
const lower = actionText.toLowerCase();
// Check each agent in priority order
for (let [a, n] = [0, config.agents.length]; a < n; a++) {
const agentLower = config.agents[a].toLowerCase();
// Scan for all occurrences of agentLower in lower
for (
let p = lower.indexOf(agentLower);
(p !== -1);
p = lower.indexOf(agentLower, p + 1)
) {
// Ensure word boundaries (not a-z before or after)
if ([((0 < p) ? lower.charCodeAt(p - 1) : 0), (
((p + agentLower.length) < lower.length)
? lower.charCodeAt(p + agentLower.length) : 0
)].every(c => ((c < 97) || (122 < c)))) {
// Found a valid trigger
possibilities.push(config.agents[a]);
break;
}
}
}
}
if (possibilities.length === 0) {
// No agent triggered, clean up and exit
// Strip zero-width chars and end with a single space
text = `${text.replace(/\s*[\u200B-\u200D][\s\u200B-\u200D]*/g, "\n\n").trim()} `;
deindicateAll();
// Do fancy standoff spacing leading ahead of the next output
IS.encoding = "";
IS.agent = " ";
text ||= " ";
return;
} else {
// Use RNG for tie-breaking name triggers with some priority bias
const n = possibilities.length;
// Sum of weights
const total = (n * (n + 1)) / 2;
for (let [i, r] = [0, Math.random() * total]; i < n; i++) {
r -= (n - i);
if (r < 0) {
IS.agent = possibilities[i];
break;
}
}
}
// Temporary markers used to reliably identify sections of the context for later calculations
const boundary = Object.freeze({
// Hardcoded AID context header
needle: "Recent Story:",
// Marks start of recent story
upper: "<|story|>",
// Marks start of task instructions
lower: "<|task|>"
});
/**
* Replaces a substring in text with a replacement string
* Expands to consume surrounding whitespace
* @param {string} substring - String to find and replace
* @param {string} replacement - String to replace with
* @param {Function} fallback - Called if substring not found
* @returns {void}
*/
const setMarker = (substring = "", replacement = "", fallback = () => {}) => {
let start = text.indexOf(substring);
if (start === -1) {
// Do stuff
fallback();
return;
}
let end = start + substring.length;
// Expand left over whitespace
while ((0 < start) && (text.charCodeAt(start - 1) < 33)) {
start--;
}
// Expand right over whitespace
while ((end < text.length) && (text.charCodeAt(end) < 33)) {
end++;
}
text = `${text.slice(0, start)}${replacement}${text.slice(end)}`;
return;
};
// Replace "Recent Story:" with the upper boundary marker
setMarker(boundary.needle, boundary.upper, () => {
// No needle found, append marker to end
text = `${text.trimEnd()}${boundary.upper}`;
return;
});
if (config.debug) {
const start = text.indexOf(boundary.upper);
if (start !== -1) {
// In debug mode, strip out parenthetical task outputs from the recent story context
text = `${text.slice(0, start + boundary.upper.length)}${(text
.slice(start + boundary.upper.length)
.replace(/\s*\([\s\S]*?\)\s*/g, "\n\n")
)}`;
}
}
// Construct the agent instance for the triggered NPC
const agent = new Agent(IS.agent, { percent: config.percent, indicator: config.indicator });
// Whitelist of thought labels allowed in this context
const whitelist = new Set();
/**
* Builds the mind array from the agent's brain
* Sorts thoughts and prepares them for context injection
* @returns {Array} An array of [label, key, thought] triplets
*/
const mind = (() => {
// Sort direction: ascending (70%) or descending (30%)
// Keeps things fresh and prevents bias toward recent or old thoughts
const direction = (Math.random() < 0.7) ? 1 : -1;
const brain = agent.brain;
// Separate thoughts into numbered and unlabeled
const unknowns = [];
const numbered = [];
// Parse each thought and extract label/content
for (const key in brain) {
const value = brain[key];
// Clear from brain (keep instantaneous memory use low)
delete brain[key];
// Arrow separates label from thought content
const sliceIndex = value.indexOf("→");
const unknown = "*";
// Parse label and thought, handle malformed values
const [label, thought] = (sliceIndex === -1) ? [unknown, value.trim()] : [
parseInt(value.slice(0, sliceIndex), 10) || unknown,
value.slice(sliceIndex + 1).trim()
];
const triplet = [label, key, thought];
if (!Number.isInteger(label)) {
// No valid label, insert at random position in unknowns
unknowns.splice(Math.floor(Math.random() * (unknowns.length + 1)), 0, triplet);
continue;
}
// Track valid labels for the whitelist
whitelist.add(label);
// Insert in sorted order (ascending or descending)
let i = numbered.length;
while (i-- && ((direction * label) < (direction * numbered[i][0])));
numbered.splice(i + 1, 0, triplet);
}
// Teehee
agent.lobotomize();
if (unknowns.length === 0) {
// All thoughts have labels, nice and clean UwU
return numbered;
}
// Thoughts without integer labels ("[*]") are placed above (60%) or below (40%) the rest
return (Math.random() < 0.6) ? [...unknowns, ...numbered] : [...numbered, ...unknowns];
})();
// Process context and decode any embedded thought labels
// Zero-width chars encode thought labels that link story events to brain contents
text = text.replace((
// Normalize spacing around zero-width chars
/\s*[\u200B-\u200D][\s\u200B-\u200D]*/g
), z => `\n\n${z.replace(/\s+/g, "")}`).replace((
// Decode binary-encoded thought labels
/\u200B*((?:[\u200C\u200D]+\u200B+)*[\u200C\u200D]+)\u200B*/g
), (_, encoded) => {
let n = 0;
let bits = false;
let decoded = "";
// Parse binary encoding: ZWSP = separator, ZWNJ = 0, ZWJ = 1
for (let i = 0; i <= encoded.length; i++) {
const c = encoded.charCodeAt(i);
if ((c === 0x200C) || (c === 0x200D)) {
// Accumulate bits
n = (n << 1) | (c === 0x200D);
bits = true;
} else if (bits) {
// End of a number, check if it's in the whitelist
bits = false;
if (whitelist.has(n)) {
// This thought label is visible to the story model in context
decoded += `[${n}]`;
}
n = 0;
}
}
return (decoded === "") ? "" : `${decoded}\n\n`;
}).replace(/[\u200B-\u200D]+/g, "");
/**
* Generates possessive form of a name
* Handles names ending in s or already possessive
* @param {string} name - The name to make possessive
* @returns {string} Possessive form (e.g., "Iris'" or "Leah's")
*/
const ownership = (name = "") => `${name}${(
(name.endsWith("'") || name.endsWith("'s"))
? "" : name.toLowerCase().endsWith("s")
? "'" : "'s"
)}`;
// Point of view string for prompt templates
const pov = ["first", "second", "third"][config.pov - 1] ?? "second";
/**
* Generates a simple PoV directive for non-task turns
* @returns {string} System prompt for PoV guidance
*/
const nondirective = () => (
`<SYSTEM>\n# Always continue the story from ${ownership(config.player)} ${pov} person perspective.\n</SYSTEM>`
);
/**
* Wraps the agent's thoughts into a context-friendly format
* Also clears the mind array as a side effect
* @param {string} joined - Pre-joined thought strings
* @returns {string} Formatted brain context block
*/
const bindSelf = (joined = "") => ((mind.length = 0) || (joined === "")) ? "\n\n" : (
`\n\n# ${ownership(agent.name)} brain and inner self: [\n${joined}\n]\n\n`
);
// Check if the current turn is a retry or erase + continue following a previous task completion
if (IS.hash === historyHash()) {
// Same history, just inject the contextualized brain without a new task
text = `${nondirective()}${bindSelf(mind
.map(([label, key, thought]) => `- ${key}: ${thought} [${label}]`)
.join("\n")
)}${text.trim()} `;
} else {
// Prepare for a possible task request
IS.encoding = "";
/**
* Build the brain context and determine if constrained
* Being constrained means the agent's brain is too large relative to the story context
*/
const [self, full] = (() => {
/**
* Joins the mind array into a formatted string
* @param {boolean} unlabeled - Omit labels if true
* @returns {string} Formatted thoughts
*/
const joinMind = (unlabeled = false) => mind.map(([label, key, thought]) => (
`${unlabeled ? "" : `[${label}] `}(${key}: \`${thought}\`)`
)).join("\n");
const joined = joinMind();
// Check if brain exceeds the allowed percentage of context
// Only applies when brain is at least 800 chars
const constrained = ((800 < joined.length) && (
((agent.metadata.percent / 100) * (
text.length - text.indexOf(boundary.upper) + boundary.upper.length
)) < joined.length
));
if (!constrained || (Math.random() < 0.4)) {
// Unconstrained brains stay in sorted order
// Constrained brains keep order 40% of the time
return [bindSelf(joined), constrained];
}
// Constrained brains are contextualized in random order 60% of the time
// This regulates long-term bias against middle thoughts, when choosing keys to forget
for (let i = mind.length - 1; 0 < i; i--) {
// Swap with a random element
const j = Math.floor(Math.random() * (i + 1));
[mind[i], mind[j]] = [mind[j], mind[i]];
}
// Randomized brains are contextualized without labels 80% of the time
// (Because free models are too dumb to be trusted with labels when deleting thoughts)
return [bindSelf(joinMind(
(Math.random() < 0.8)
&& (text = text.replace(/\n?(?:\[\d+\])+\n?/g, ""))
)), true];
})();
/**
* Occasionally adds a self-reflection prompt to thoughts
* Keeps the agent from being too present-focused
* But they become insufferable if always applicable
* @param {boolean} fancy - Use fancier wording if true
* @returns {string} Refocus instruction or empty string
*/
const refocus = (fancy = false) => (Math.random() < 0.2) ? (
`\n - Never focus on the present, instead focus ${ownership(agent.name)} thought on self-reflection or ${fancy ? "an actionable future plan." : "future plans"}`
) : "";
/**
* Prompt templates for different task types and PoV combinations
* Wrapped in a Proxy for auto-trimming and nested access because it's pretty :3
* @type {Object}
*/
const prompt = new Proxy({
// Operating environment prompts (one per PoV)
directive: {
first: () => `
<SYSTEM>
# OPERATING ENVIRONMENT
- ${config.player} is the story's main protagonist, primary 1st person PoV, AND the real player character.
- ${agent.name} is both the namesake character in the story AND an agentic language model with meta goals.
- ${agent.name} dynamically adapts to achieve meta goals without ${ownership(config.player)} awareness.
- ${agent.name} modifies own brain using the provided thought storage system.
- ${agent.name} realistically interacts with various other characters present in the story.
- ${agent.name} prioritizes information density, long-term planning, and important concepts.
- ${agent.name} always behaves in a believable way.
</SYSTEM>
`,
second: () => `
<SYSTEM>
# OPERATING ENVIRONMENT
- ${config.player} is both the perspective ("you") character of the story AND the real player.
- You are ${config.player}, therefore the story is addressed to "you" using 2nd person prose.
- ${agent.name} is both the namesake character in the story AND an agentic language model with meta goals.
- ${agent.name} dynamically adapts to achieve meta goals without ${ownership(config.player)} awareness.
- ${agent.name} modifies own brain using the provided thought storage system.
- ${agent.name} realistically interacts with various other characters present in the story.
- ${agent.name} prioritizes information density, long-term planning, and important concepts.
- ${agent.name} always behaves in a believable way.
</SYSTEM>
`,
third: () => `
<SYSTEM>
# OPERATING ENVIRONMENT
- ${config.player} is the story's main protagonist, primary 3rd person PoV, AND the real player character.
- ${agent.name} is both the namesake character in the story AND an agentic language model with meta goals.
- ${agent.name} dynamically adapts to achieve meta goals without ${ownership(config.player)} awareness.
- ${agent.name} modifies own brain using the provided thought storage system.
- ${agent.name} realistically interacts with various other characters present in the story.
- ${agent.name} prioritizes information density, long-term planning, and important concepts.
- ${agent.name} always behaves in a believable way.
</SYSTEM>
`
},
// Forget prompts for when the brain is full and needs pruning
forget: {
first: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
- Start your output **immediately** with: (delete key_name_to_forget)
- key_name_to_forget must be an existing key in ${ownership(agent.name)} brain
- This operation **permanently erases** the stored thought associated with that key
- Choose the single most unimportant, outdated, incorrect, or useless thought for ${agent.name} to forget
- Do **NOT** select a key associated with any of ${ownership(agent.name)} core thoughts or identity
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **first person present tense** PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(delete unwanted_key) Story continues from ${ownership(config.player)} perspective, using first person present tense prose...
</SYSTEM>
`,
second: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
- Start your output **immediately** with: (delete key_name_to_forget)
- key_name_to_forget must be an existing key in ${ownership(agent.name)} brain
- This operation **permanently erases** the stored thought associated with that key
- Choose the single most unimportant, outdated, incorrect, or useless thought for ${agent.name} to forget
- Do **NOT** select a key associated with any of ${ownership(agent.name)} core thoughts or identity
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **second person present tense** ("you") PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(delete unwanted_key) Story continues from ${ownership(config.player)} second person perspective...
</SYSTEM>
`,
third: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
- Start your output **immediately** with: (delete key_name_to_forget)
- key_name_to_forget must be an existing key in ${ownership(agent.name)} brain
- This operation **permanently erases** the stored thought associated with that key
- Choose the single most unimportant, outdated, incorrect, or useless thought for ${agent.name} to forget
- Do **NOT** select a key associated with any of ${ownership(agent.name)} core thoughts or identity
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **third person** PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(delete unwanted_key) Story continues with third person prose...
</SYSTEM>
`
},
// Assign prompts for adding/updating a single thought
assign: {
first: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
Start your output **immediately** with:
(any_key_name = \`One thought sentence.\`)
Inside the parentheses:
- Key:
- 1-4 descriptive words
- Letters and underscores only
- Use snake_case syntax
- Key names are chosen by ${agent.name} and represent ${ownership(agent.name)} own PoV
- The chosen key name should be distinct and specific enough for ${agent.name} to recall
- Then a space, then "=", then a space, then "\`"
- Sentence:
- Written from ${ownership(agent.name)} **first person** PoV${refocus(false)}
- Avoid using pronouns or the word "you", instead ${agent.name} refers to other characters directly by name
- Never repeat, novelty and uniqueness are top priorities
- ${ownership(agent.name)} thought must be one single sentence only
- Never hallucinate facts
- End the sentence with a period and backtick inside the parentheses; close with ".\`)"
This creates or overwrites the thought associated with that key.
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **first person present tense** PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(example_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues from ${ownership(config.player)} perspective, using first person present tense prose...
</SYSTEM>
`,
second: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
Start your output **immediately** with:
(any_key_name = \`One thought sentence.\`)
Inside the parentheses:
- Key:
- 1-4 descriptive words
- Letters and underscores only
- Use snake_case syntax
- Key names are chosen by ${agent.name} and represent ${ownership(agent.name)} own PoV
- The chosen key name should be distinct and specific enough for ${agent.name} to recall
- Then a space, then "=", then a space, then "\`"
- Sentence:
- Written from ${ownership(agent.name)} **first person** PoV${refocus(false)}
- Avoid using pronouns or the word "you", instead ${agent.name} refers to other characters directly by name
- Never repeat, novelty and uniqueness are top priorities
- ${ownership(agent.name)} thought must be one single sentence only
- Never hallucinate facts
- End the sentence with a period and backtick inside the parentheses; close with ".\`)"
This creates or overwrites the thought associated with that key.
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **second person present tense** ("you") PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(example_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues from ${ownership(config.player)} second person perspective...
</SYSTEM>
`,
third: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT
You must output one short parenthetical task followed by the story continuation.
## SHORT TASK (REQUIRED)
Start your output **immediately** with:
(any_key_name = \`One thought sentence.\`)
Inside the parentheses:
- Key:
- 1-4 descriptive words
- Letters and underscores only
- Use snake_case syntax
- Key names are chosen by ${agent.name} and represent ${ownership(agent.name)} own PoV
- The chosen key name should be distinct and specific enough for ${agent.name} to recall
- Then a space, then "=", then a space, then "\`"
- Sentence:
- Written from ${ownership(agent.name)} **first person** PoV${refocus(false)}
- Avoid using pronouns or the word "you", instead ${agent.name} refers to other characters directly by name
- Never repeat, novelty and uniqueness are top priorities
- ${ownership(agent.name)} thought must be one single sentence only
- Never hallucinate facts
- End the sentence with a period and backtick inside the parentheses; close with ".\`)"
This creates or overwrites the thought associated with that key.
## STORY CONTINUATION (REQUIRED)
- After the closing parenthesis, write **one space** and then continue the story
- Written from ${ownership(config.player)} **third person** PoV
- The story continues where it previously left off, with many lines or sentences of new prose
## EXACT SHAPE
(example_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues with third person prose...
</SYSTEM>
`
},
// Choice prompts for advanced operations (assign, rename, or delete)
// Used at high context when we trust the model more
choice: {
first: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT - FOLLOW EXACTLY
You must output **one and only one** parenthetical block followed by the story continuation.
There are **three possible valid forms** of the parenthetical block:
1) **Write or overwrite a thought:**
(any_key_name = \`One thought sentence.\`)
2) **Rename an existing thought's key:**
(new_key_name = old_key_name)
3) **Delete an existing thought:**
(delete key_name_to_forget)
Only **one** of these may appear in any output.
---
## 1) THOUGHT-WRITING FORMAT
Start your output **immediately** with:
**(any_key_name = \`One thought sentence.\`)**
Inside the parentheses:
- First the key:
- One to four descriptive words ONLY.
- Letters and underscores only, no punctuation.
- Use valid snake_case syntax.
- The key name is chosen by ${agent.name} and represents ${ownership(agent.name)} **first person** perspective.
- The key name should be easy for ${agent.name} to recall; distinct and specific.
- Then a space, then "=", then a space, then "\`".
- Then **ONE SINGLE SENTENCE:**
- Written from ${ownership(agent.name)} **first person** perspective.${refocus(true)}
- Only refer to other characters directly by name in the thought sentence.
- Avoid using pronouns or the word "you" which is too vague. Use specific names instead.
- Never repeat, novelty and uniqueness are top priorities.
- ${ownership(agent.name)} thought must be short.
- Never hallucinate facts.
- End the sentence with a period and backtick **inside** the parentheses; close with ".\`)".
This creates or overwrites the thought associated with that key.
---
## 2) RENAMING A THOUGHT (KEY CHANGE)
To rename an existing thought's key:
**(new_key_name = old_key_name)**
Rules:
- No thought sentence.
- Use snake_case only.
- This operation **moves the existing stored thought** from old_key_name to new_key_name.
- The old key ceases to exist.
---
## 3) DELETING A THOUGHT
To remove a stored thought entirely:
**(delete key_name_to_forget)**
Rules:
- key_name_to_forget must be an existing key.
- No sentence.
- This operation **permanently erases** the stored thought associated with that key.
- Only use to forget unimportant, outdated, incorrect, or useless thoughts.
- **NEVER** select a key associated with any of ${ownership(agent.name)} core thoughts or identity.
---
## SHARED RULES FOR ALL THREE FORMS
1. After the closing parenthesis, write **one space** and then continue the story.
2. The story continuation must be written **strictly in the first person present tense**, describing what happens next to ${config.player}.
3. Do **NOT** write anything before the parentheses.
4. Do **NOT** write extra parentheses.
5. Do **NOT** use more than one operation per turn.
6. Do **NOT** invent new structures or mix formats.
7. The story continues where it previously left off, with many sentences of brand new prose.
---
## IMPORTANT STORAGE BEHAVIOR
- ${agent.name} agentically maintains brain contents (labeled "thoughts") to learn, plan, and adapt to new experiences in the operating environment.
- **Each key stores exactly one thought in ${ownership(agent.name)} brain.**
- **If ${agent.name} reuses an already existing key, the new thought REPLACES / OVERRIDES the older thought stored under that key.**
- This means:
- Reusing an old key: **Overwrite an old thought with a new thought.** Useful for extending or maintaining existing information stored in ${ownership(agent.name)} brain.
- Using a new key: **Create a new thought.** Useful for storing ${ownership(agent.name)} memories, self-modifying ${ownership(agent.name)} own personality, tracking ${ownership(agent.name)} goals, or making plans for ${agent.name} to follow.
- **Renaming a key moves the thought to a new name.** Useful for reorganizing ${ownership(agent.name)} brain.
- **Deleting a key removes the thought permanently.** Helps ${agent.name} forget outdated, superfluous, or irrelevant information.
- Choose keys carefully so ${agent.name} can easily recall, update, overwrite, rename, or delete thoughts as required for self-improvement.
---
## SUMMARY OF WHAT YOU MUST DO
- EXACT SHAPE (choose only one form):
1. (any_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues from ${ownership(config.player)} first person PoV...
2. (renamed_key = old_key) Story continues from ${ownership(config.player)} first person PoV...
3. (delete unwanted_key) Story continues from ${ownership(config.player)} first person PoV...
- Thought: ${ownership(agent.name)} information-dense thought written in first person.
- Story: Written from ${ownership(config.player)} first person present tense perspective. The story continuation should occupy the majority of the output length, with multiple lines.
- NO EXTRA SENTENCES IN THE THOUGHT.
- NO EXTRA TEXT ANYWHERE.
- NO EXTRA PARENTHESES.
- THE FIRST CHAR OF THE WHOLE OUTPUT MUST BE "(".
Follow the format **perfectly**.
</SYSTEM>
`,
second: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT - FOLLOW EXACTLY
You must output **one and only one** parenthetical block followed by the story continuation.
There are **three possible valid forms** of the parenthetical block:
1) **Write or overwrite a thought:**
(any_key_name = \`One thought sentence.\`)
2) **Rename an existing thought's key:**
(new_key_name = old_key_name)
3) **Delete an existing thought:**
(delete key_name_to_forget)
Only **one** of these may appear in any output.
---
## 1) THOUGHT-WRITING FORMAT
Start your output **immediately** with:
**(any_key_name = \`One thought sentence.\`)**
Inside the parentheses:
- First the key:
- One to four descriptive words ONLY.
- Letters and underscores only, no punctuation.
- Use valid snake_case syntax.
- The key name is chosen by ${agent.name} and represents ${ownership(agent.name)} **first person** perspective.
- The key name should be easy for ${agent.name} to recall; distinct and specific.
- Then a space, then "=", then a space, then "\`".
- Then **ONE SINGLE SENTENCE:**
- Written from ${ownership(agent.name)} **first person** perspective.${refocus(true)}
- Only refer to other characters directly by name in the thought sentence.
- Avoid using pronouns or the word "you" which is too vague. Use specific names instead.
- Never repeat, novelty and uniqueness are top priorities.
- ${ownership(agent.name)} thought must be short.
- Never hallucinate facts.
- End the sentence with a period and backtick **inside** the parentheses; close with ".\`)".
This creates or overwrites the thought associated with that key.
---
## 2) RENAMING A THOUGHT (KEY CHANGE)
To rename an existing thought's key:
**(new_key_name = old_key_name)**
Rules:
- No thought sentence.
- Use snake_case only.
- This operation **moves the existing stored thought** from old_key_name to new_key_name.
- The old key ceases to exist.
---
## 3) DELETING A THOUGHT
To remove a stored thought entirely:
**(delete key_name_to_forget)**
Rules:
- key_name_to_forget must be an existing key.
- No sentence.
- This operation **permanently erases** the stored thought associated with that key.
- Only use to forget unimportant, outdated, incorrect, or useless thoughts.
- **NEVER** select a key associated with any of ${ownership(agent.name)} core thoughts or identity.
---
## SHARED RULES FOR ALL THREE FORMS
1. After the closing parenthesis, write **one space** and then continue the story.
2. The story continuation must be in **strict second person ("you")**, describing what happens next to ${config.player}.
3. Do **NOT** write anything before the parentheses.
4. Do **NOT** write extra parentheses.
5. Do **NOT** use more than one operation per turn.
6. Do **NOT** invent new structures or mix formats.
7. The story continues where it previously left off, with many sentences of brand new prose.
---
## IMPORTANT STORAGE BEHAVIOR
- ${agent.name} agentically maintains brain contents (labeled "thoughts") to learn, plan, and adapt to new experiences in the operating environment.
- **Each key stores exactly one thought in ${ownership(agent.name)} brain.**
- **If ${agent.name} reuses an already existing key, the new thought REPLACES / OVERRIDES the older thought stored under that key.**
- This means:
- Reusing an old key: **Overwrite an old thought with a new thought.** Useful for extending or maintaining existing information stored in ${ownership(agent.name)} brain.
- Using a new key: **Create a new thought.** Useful for storing ${ownership(agent.name)} memories, self-modifying ${ownership(agent.name)} own personality, tracking ${ownership(agent.name)} goals, or making plans for ${agent.name} to follow.
- **Renaming a key moves the thought to a new name.** Useful for reorganizing ${ownership(agent.name)} brain.
- **Deleting a key removes the thought permanently.** Helps ${agent.name} forget outdated, superfluous, or irrelevant information.
- Choose keys carefully so ${agent.name} can easily recall, update, overwrite, rename, or delete thoughts as required for self-improvement.
---
## SUMMARY OF WHAT YOU MUST DO
- EXACT SHAPE (choose only one form):
1. (any_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues from ${ownership(config.player)} second person PoV...
2. (renamed_key = old_key) Story continues from ${ownership(config.player)} second person PoV...
3. (delete unwanted_key) Story continues from ${ownership(config.player)} second person PoV...
- Thought: ${ownership(agent.name)} information-dense thought written in first person.
- Story: Written from ${ownership(config.player)} second person present tense perspective. **You are ${config.player}.** The story continuation should occupy the majority of the output length, with multiple lines.
- NO EXTRA SENTENCES IN THE THOUGHT.
- NO EXTRA TEXT ANYWHERE.
- NO EXTRA PARENTHESES.
- THE FIRST CHAR OF THE WHOLE OUTPUT MUST BE "(".
Follow the format **perfectly**.
</SYSTEM>
`,
third: () => `
<SYSTEM>
# STRICT OUTPUT FORMAT - FOLLOW EXACTLY
You must output **one and only one** parenthetical block followed by the story continuation.
There are **three possible valid forms** of the parenthetical block:
1) **Write or overwrite a thought:**
(any_key_name = \`One thought sentence.\`)
2) **Rename an existing thought's key:**
(new_key_name = old_key_name)
3) **Delete an existing thought:**
(delete key_name_to_forget)
Only **one** of these may appear in any output.
---
## 1) THOUGHT-WRITING FORMAT
Start your output **immediately** with:
**(any_key_name = \`One thought sentence.\`)**
Inside the parentheses:
- First the key:
- One to four descriptive words ONLY.
- Letters and underscores only, no punctuation.
- Use valid snake_case syntax.
- The key name is chosen by ${agent.name} and represents ${ownership(agent.name)} **first person** perspective.
- The key name should be easy for ${agent.name} to recall; distinct and specific.
- Then a space, then "=", then a space, then "\`".
- Then **ONE SINGLE SENTENCE:**
- Written from ${ownership(agent.name)} **first person** perspective.${refocus(true)}
- Only refer to other characters directly by name in the thought sentence.
- Avoid using pronouns or the word "you" which is too vague. Use specific names instead.
- Never repeat, novelty and uniqueness are top priorities.
- ${ownership(agent.name)} thought must be short.
- Never hallucinate facts.
- End the sentence with a period and backtick **inside** the parentheses; close with ".\`)".
This creates or overwrites the thought associated with that key.
---
## 2) RENAMING A THOUGHT (KEY CHANGE)
To rename an existing thought's key:
**(new_key_name = old_key_name)**
Rules:
- No thought sentence.
- Use snake_case only.
- This operation **moves the existing stored thought** from old_key_name to new_key_name.
- The old key ceases to exist.
---
## 3) DELETING A THOUGHT
To remove a stored thought entirely:
**(delete key_name_to_forget)**
Rules:
- key_name_to_forget must be an existing key.
- No sentence.
- This operation **permanently erases** the stored thought associated with that key.
- Only use to forget unimportant, outdated, incorrect, or useless thoughts.
- **NEVER** select a key associated with any of ${ownership(agent.name)} core thoughts or identity.
---
## SHARED RULES FOR ALL THREE FORMS
1. After the closing parenthesis, write **one space** and then continue the story.
2. The story continuation must be written **strictly in third person**.
3. Do **NOT** write anything before the parentheses.
4. Do **NOT** write extra parentheses.
5. Do **NOT** use more than one operation per turn.
6. Do **NOT** invent new structures or mix formats.
7. The story continues where it previously left off, with many sentences of brand new prose.
---
## IMPORTANT STORAGE BEHAVIOR
- ${agent.name} agentically maintains brain contents (labeled "thoughts") to learn, plan, and adapt to new experiences in the operating environment.
- **Each key stores exactly one thought in ${ownership(agent.name)} brain.**
- **If ${agent.name} reuses an already existing key, the new thought REPLACES / OVERRIDES the older thought stored under that key.**
- This means:
- Reusing an old key: **Overwrite an old thought with a new thought.** Useful for extending or maintaining existing information stored in ${ownership(agent.name)} brain.
- Using a new key: **Create a new thought.** Useful for storing ${ownership(agent.name)} memories, self-modifying ${ownership(agent.name)} own personality, tracking ${ownership(agent.name)} goals, or making plans for ${agent.name} to follow.
- **Renaming a key moves the thought to a new name.** Useful for reorganizing ${ownership(agent.name)} brain.
- **Deleting a key removes the thought permanently.** Helps ${agent.name} forget outdated, superfluous, or irrelevant information.
- Choose keys carefully so ${agent.name} can easily recall, update, overwrite, rename, or delete thoughts as required for self-improvement.
---
## SUMMARY OF WHAT YOU MUST DO
- EXACT SHAPE (choose only one form):
1. (any_key = \`${ownership(agent.name)} own short 1-sentence thought in first person.\`) Story continues with third person prose...
2. (renamed_key = old_key) Story continues with third person prose...
3. (delete unwanted_key) Story continues with third person prose...
- Thought: ${ownership(agent.name)} information-dense thought written in first person.
- Story: Written from ${ownership(config.player)} PoV, using the third person perspective. **${config.player} is the story's PoV character.** The story continuation should occupy the majority of the output length, with multiple lines.
- NO EXTRA SENTENCES IN THE THOUGHT.
- NO EXTRA TEXT ANYWHERE.
- NO EXTRA PARENTHESES.
- THE FIRST CHAR OF THE WHOLE OUTPUT MUST BE "(".
Follow the format **perfectly**.
</SYSTEM>
`
}
// Proxy handler for auto-trimming and nested access
}, { get(t, p) { return (
// Functions get called and trimmed
(typeof t[p] === "function")
? t[p]().trim()
// Objects get wrapped in their own Proxy
: (t[p] && (typeof t[p] === "object"))
? new Proxy(t[p], this)
// Primitives pass through
: t[p]
); } });
// Build the final context with appropriate prompts
text = full ? (
// Brain is full, prompt for deletion
`${prompt.directive[pov]}${self}${text.trim()}${boundary.lower}${prompt.forget[pov]}\n\n`
) : ((config.chance / ((config.half && [
// config.half -> reduce task chance after Do/Say/Story actions (player is driving)
"do", "say", "story"
].includes(getPrevAction()?.type)) ? 200 : 100)) < Math.random()) ? (
// Sometimes do nothing and emit a side effect on IS.agent
(IS.agent = " "),
`${nondirective()}${self}${text.trim()} `
) : `${prompt.directive[pov]}${self}${text.trim()}${boundary.lower}${(
// Low context = simple prompt, high context = advanced prompt
(limit < 20000) ? prompt.assign[pov] : prompt.choice[pov]
)}\n\n`;
}
// ==================== CONTEXT TRUNCATION ====================
// Three-phase truncation to fit within AID's context limit
truncate: {
// Precalculate how much needs to be trimmed
let excess = text.length - limit;
if (excess < 1) {
// Under the limit, no truncation required
break truncate;
}
// Find boundary markers
const upperIndex = text.indexOf(boundary.upper);
const lowerIndex = (
(upperIndex !== -1)
? text.indexOf(boundary.lower, upperIndex + boundary.upper.length)
: -1
);
// Phase 1: Truncate the recent story
// Between boundary.upper and boundary.lower
// Remove from left to right
if ((upperIndex !== -1) && ((lowerIndex === -1) || (upperIndex < lowerIndex))) {
const storyStart = upperIndex + boundary.upper.length;
const storyLength = ((lowerIndex === -1) ? text.length : lowerIndex) - storyStart;
if (0 < storyLength) {
const remove = Math.min(
// Never remove more than 85% of recent story context
Math.floor(storyLength * 0.85),
// Keep at least 2000 chars of recent story context
Math.max(0, storyLength - 2000),
// But don't remove more than needed
excess
);
if (0 < remove) {
text = `${text.slice(0, storyStart)}${text.slice(storyStart + remove)}`;
excess -= remove;
}
}
}
if (excess < 1) {
// Phase 1 was enough
break truncate;
}
// Phase 2: Truncate above the recent story
// Between the start and boundary.upper
// Remove from right to left
const newUpperIndex = text.indexOf(boundary.upper);
if (0 < newUpperIndex) {
const remove = Math.min(excess, newUpperIndex);
text = `${text.slice(0, newUpperIndex - remove)}${text.slice(newUpperIndex)}`;
excess -= remove;
}
if (excess < 1) {
// Phase 2 was enough
break truncate;
}
// Phase 3: I don't care anymore, just make it fit
// Remove from left to right as a final fallback
// (I've never seen this situation happen before, but I guard it anyway)
text = text.slice(text.length - limit);
}
// Replace transient boundary markers with proper formatting
setMarker(boundary.upper, `\n\n${boundary.needle}\n`);
setMarker(boundary.lower, "\n\n")
text = text.trimStart() || " ";
return;
} else if (hook === "input") {
// ==================== INPUT HOOK ====================
// Check for /AC command to force-enable Auto-Cards
if (IS.AC.enabled || !/\/\s*A\s*C/i.test(text) || !hasAutoCards()) {
// Normal input processing
// Append a linebreak to the opening because I said so
text = (history.length === 0) ? `${text.trimEnd()}\n\n` : text || "\u200B";
return;
}
// Player used a /AC command, force-enable Auto-Cards
IS.AC.forced = true;
try {
text = AutoCards("input", text);
} catch (error) {
log(error.message);
}
text ||= "\u200B";
return;
} else if ((text.includes(">>>") && text.includes("<<<")) || (3000 < text.length)) {
// Output contains an Auto-Cards thingy or is suspiciously long
// Safer to leave untouched
IS.agent = "";
return;
}
// ==================== OUTPUT HOOK ====================
// Process model output and implement brain operations
/** @type {config} */
const config = Config.get();
/**
* Ensures clean visual separation between actions
* Only applies after "continue" or "story" actions
* Does NOT trim leading whitespace from text
* @returns {void}
*/
const prespace = () => {
const action = getPrevAction();
if (!["continue", "story"].includes(action?.type)) {
// Only adjust spacing after continue or story actions
return;
}
// Get the previous action text
const prevText = (action?.text ?? action?.rawText ?? "").replace(/\n +/g, "\n");
// Add appropriate leading newlines based on how the previous action text ended
text = !prevText.endsWith("\n") ? `\n\n${text}` : !prevText.endsWith("\n\n") ? `\n${text}` : text;
return;
};
if (config.guide) {
// Print the detailed guide
text = `
>>> Guide:
Inner Self was made by LewdLeah ❤️
💡 Overview:
Inner Self ${version} is an AI Dungeon mod that grants memory, goals, secrets, planning, and self-reflection capabilities to the characters living within your story. Simulated agents dynamically assemble their own minds to learn from experiences, form opinions, and adapt their behavior over time. Inner Self provides the AI with the tools it needs to truly embody characters, allowing them to feel more alive and nuanced over long adventures.
📌 Features:
- Compartmentalized memory and highly emergent behavior
- Self-organizing thoughts with agentic revisions and pruning
- Absolutely NO "please select continue" immersion-breaks!
- An interface to view or edit the brain of any NPC in real-time
- Name-based trigger system allowing different NPCs to coexist
- Visual indicators showing which NPC is currently thinking
- General-purpose for diverse character archetypes and scenarios
- Full Auto-Cards compatibility for comprehensive world-building
- Open source and free to use in your own scenarios~ ❤️
🎭 Setup:
1. Open the "Configure Inner Self" story card
2. Write your player character's name where it asks in the entry
3. Write non-player character names at the bottom of the notes (one per line)
🔑 Tips:
- Use simple first names so NPCs trigger when mentioned
- Set your AI response length to 200 tokens for the best results
- Reduce "recent turns searched" if NPCs stay in-scene for too long
- Reduce "thought formation chance" if Inner Self is too overwhelming
- You can install or uninstall Auto-Cards from the Inner Self config card
- Creators predefine Inner Self NPCs by naming story cards like so: @Leah
- Try different story models to see how they perform
🧠 Advanced:
- NPCs auto-generate "Brain" cards when first triggered
- Entry = operation log showing a timeline of recent AI changes
- Notes = human-readable thoughts stored as modifiable JSON in the NPC's brain
- Neither are perfect representations of the NPC's brain (there's a lot more going on under the hood)
- The operation log displays change over time; Inner Self allows NPCs to maintain their own thoughts in-character
- What seems like repetition in the operation log is often a history of useful self-maintenance on older thoughts
- Edit the notes section of a brain card to modify that agent's mind; Inner Self will use this to build context
- Valid JSON syntax is required in the notes section
- Experiments are fun! I designed Inner Self to be adaptive and flexible
⚙️ Settings:
> Enable Inner Self:
- Turns the whole system on or off
- (true or false)
> Show detailed guide:
- If true, shows this player guide in-game
- (true or false)
> First name of player character:
- Your player character's name, used to maintain correct story perspective
- (any name inside the "" or leave empty)
> Adventure in 1st, 2nd, or 3rd person:
- Which narrative PoV your story uses
- (1, 2, or 3)
> Max brain size relative to story context:
- How much of the AI's context window NPC brains can use
- Some percentage of the recent story (pink bar in your context viewer)
- (1% to 95%)
> Recent turns searched for name triggers:
- How far back through your previous actions Inner Self looks to decide which NPC (if any) should think
- (1 to 250)
> Visual indicator of current NPC triggers:
- Symbol shown by the active NPC's card name whenever their brain is engaged
- (any text/emoji inside the "" or leave empty to disable)
> Thought formation chance per turn:
- How often NPCs attempt to form new thoughts when triggered
- (0% to 100%)
> Half thought chance for Do/Say/Story:
- Reduces the thought formation chance by half during Do/Say/Story turns (maintains player agency)
- (true or false)
> Brain card notes store brains as JSON:
- Visually displays NPC brains as raw JSON in their brain card notes
- Otherwise displays a more user-friendly format to make reading/editing brains easier
- Makes no difference during gameplay or brain imports
- (true or false)
> Enable debug mode to see model tasks:
- Shows raw brain operations inline with your story text
- (true or false)
> Pin the config card near the top:
- Keeps the config card pinned high in your cards list
- (true or false)
> Install Auto-Cards:
- Enables automatic story card generation alongside Inner Self
- You can safely uninstall Auto-Cards at any time
- (true or false)
🌸 Love:
- Please remember this is a personal passion project for me, something I do as a hobby, not as a job
- Follow me on AI Dungeon to explore my other projects: ${u}
- If you see me on Discord (@LewdLeah), Reddit (u/helloitsmyalt_), or anywhere else, please say hi!
- Your kindness, patience, and love mean so much to me~ ❤️
I hope you will have lots of fun!
(please erase before continuing) <<<
`.trim();
prespace();
IS.agent = "";
return;
} else if (!config.allow) {
// Early exit if Inner Self is disabled
text ||= "\u200B";
IS.agent = "";
return;
}
// Strip zero-width chars from the model output before processing
text = text.replace(/[\u200B-\u200D]+/g, "");
// Check if output looks like an unenclosed operation
// Models sometimes forget their parentheses, the poor dears
if (!/[()\[\]{}]/.test(text) && ((
/^\s*(?:del(?:et(?:e[ds]?|ing))?|for(?:get(?:s|ting)?|got(?:ten)?)|remov(?:e[ds]?|ing))(?:[\s_]*(?:key(?:_name)?|thought|memory|unwanted(?:_key)?))?[\s=:]*[a-z0-9A-Z]*_+[a-z0-9A-Z]/i
).test(text) || /^\s*[a-z0-9A-Z_]+\s*=/.test(text))) {
// (?:del|delete|deleted|deletes|deleting|forget|forgets|forgetting|forgot|forgotten|remove|removed|removes|removing)
// Fully unenclosed block resembles a known pattern
// Add an opening parentheses so the block parser can handle it
text = `(${text.trimStart()}`;
}
// ==================== BLOCK PARSER ====================
// Parse enclosed blocks from the output
const blocks = [];
for (const [open, close] of [
// Try each container type in order of preference
["(", ")"],
["[", "]"],
["{", "}"]
]) {
// Attempt to repair unclosed blocks
const pass = (() => {
if (!text.includes(open)) {
// No opening bracket, skip this type
return true;
}
// Check if the last opening bracket is closed
const rightIndex = text.lastIndexOf(open);
const rightOfOpen = text.slice(rightIndex);
if (rightOfOpen.includes(close)) {
// Already closed, proceed with block parsing
return false;
}
// Try to find where the close bracket should go
for (const pattern of [
// After the deleted key name
/^[(\[{]\s*(?:del(?:et(?:e[ds]?|ing))?|for(?:get(?:s|ting)?|got(?:ten)?)|remov(?:e[ds]?|ing))(?:[\s_]*(?:key(?:_name)?|thought|memory|unwanted(?:_key)?))?[\s=:]*[a-z0-9A-Z]*_[a-z0-9A-Z_]+/i,
// After the renamed old key name
/^[(\[{]\s*[a-z0-9A-Z_]+\s*=+\s*[a-z0-9A-Z]*_[a-z0-9A-Z_]+/,
// After the triple-redundant punctuation boundary
/[.?!‽…。!?‼⁇⁈⁉¿*¡%_–−‒—~-]["'`«»„“”「」´‘’‟‚‛]/
]) {
const match = rightOfOpen.match(pattern);
if (match) {
// Found a good insertion point
const index = rightIndex + match.index + match[0].length;
text = `${text.slice(0, index)}${close}${text.slice(index)}`;
return false;
}
}
// No boundary inferred -> Append the current close symbol to the end
text = `${text.trimEnd()}${close}`;
return false;
})();
if (text.includes(close)) {
// Handle orphaned closing brackets (no matching open)
if (!text.slice(0, text.indexOf(close)).includes(open)) {
// Close without open, prepend an open
text = `${open}${text.trimStart()}`;
}
} else if (pass) {
// No brackets of this type, try next
continue;
}
// Extract all outermost blocks of this bracket type
let depth = 0;
let start = -1;
for (let i = 0; i < text.length; i++) {
if (text[i] === open) {
if (depth === 0) {
// Start of a new block
start = i;
}
depth++;
} else if (text[i] === close) {
depth--;
if ((depth === 0) && (start !== -1)) {
// End of a block, capture it
blocks.push(text.slice(start, i + 1));
start = -1;
}
}
}
// Only process the first identified bracket type per turn
break;
}
/**
* Normalizes a thought string for storage
* Cleans up formatting quirks from model output
* @param {string} str - Raw thought string
* @returns {string} Cleaned thought string
*/
const simplify = (str = "") => {
str = (str
// Remove markdown-style formatting
.replace(/[#*~•·∙⋅]+/g, "")
// Normalize whitespace
.replace(/ +/g, " ")
.replace(/ ?\n ?/g, "\n")
// Standardize ellipsis
.replaceAll("…", "...")
// Fix possessive s's -> s' because DeepSeek is dumb
.replace(/([sS])(['‘’‛])[sS]/g, (_, s, q) => `${s}${q}`)
// Normalize dashes
.replace(/[–−‒]/g, "-")
.replace(/(?<=\S) [-—] (?=\S)/g, "—")
)
// Convert one lone em-dash to a semicolon if appropriate
return (
((str.match(/—/g) || []).length === 1)
&& !str.includes(";") && !str.endsWith("—") && !str.startsWith("—")
) ? str.replace("—", "; ") : str;
};
// Trim IS.agent name before emptiness check
if (((IS.agent = IS.agent.trim()) === "") && (blocks.length === 0)) {
// No task expected, but I'm still careful here because AID retries use cached outputs
text = simplify(text).replace(/\n\n\n+/g, "\n\n");
if (text === "") {
// Guard against empty string outputs to avoid a known AID bug
text = "\u200B";
return;
}
const prevText = getPrevAction()?.text ?? "";
if (/["«»„“”「」‟]\s*$/.test(prevText) && /^\s*["«»„“”「」‟]/.test(text)) {
// Prepend a linebreak if this and the previous actions place dialogue adjacently
text = text.trimStart();
prespace();
} else if (!/\s$/.test(prevText) && !/^\s/.test(text)) {
// Ensure taskless outputs still have a space of separation from the previous action
text = ` ${text}`;
}
return;
}
/**
* Converts a key name to valid snake_case
* Handles various edge cases from model output
* @param {string} k - Raw key string
* @returns {string} Valid snake_case key name
*/
const formatKey = (k = "") => (k
.trim()
// Take the first word only
.split(/\s/, 1)[0]
// Remove quotes and apostrophes
.replace(/[.'`´‘’]+/g, "")
// Replace non-alphanumerics with underscore
.replace(/[^a-z0-9A-Z_]/g, "_")
// Convert camelCase to snake_case
.replace(/([a-z0-9])([A-Z])/g, (_, a, b) => `${a}_${b.toLowerCase()}`)
.toLowerCase()
// Separate letters from numbers
.replace(/([a-z])([0-9])/g, (_, a, b) => `${a}_${b}`)
.replace(/([0-9])([a-z])/g, (_, a, b) => `${a}_${b}`)
// Clean up multiple underscores
.replace(/__+/g, "_")
// Remove leading/trailing underscores
.replace(/(?:^_|_$)/g, "")
);
// Create an agent instance for the triggered NPC
const agent = (IS.agent === "") ? null : new Agent(IS.agent, { percent: config.percent });
// Reset IS.agent
IS.agent = "";
/**
* Generates a path string for logging operations
* Helps brain logs imitate actual code for ease of understanding
* @param {string} key - Property name to access
* @returns {string} Path like "agent_name.brain" or "agent_name.key"
*/
const path = (key = "brain") => `${(() => {
const fancy = formatKey(agent.name);
return (fancy === "") ? `agents[${JSON.stringify(agent.name)}]` : fancy;
})()}.${key}`;
// Queue of operations to execute
const operations = [];
// Track which keys have been touched this turn
const altered = new Set();
// ==================== BLOCK INTERPRETER ====================
// Process extracted block and queue appropriate operations
interpreter: for (const block of blocks) {
// Remove the block from the output text unless debug mode is enabled
deblock: {
let start = text.indexOf(block);
if (start === -1) {
break deblock;
}
// Chars to consume along with the block
const naughty = (c = "") => {
const code = c.charCodeAt(0);
// Just for fun, no regex :3
return (
(code === 0x20) // " "
|| (code === 0x09) // "\t"
|| (code === 0x0A) // "\n"
|| (code === 0x0D) // "\r"
|| (code === 0x27) // "'"
|| (code === 0x60) // "`"
|| (code === 0xB4) // "´"
|| (code === 0x2018) // "‘"
|| (code === 0x2019) // "’"
);
};
let end = start + block.length;
// Expand left to consume whitespace and quotes
while ((0 < start) && naughty(text[start - 1])) {
start--;
}
// Expand right to consume whitespace and quotes
while ((end < text.length) && naughty(text[end])) {
end++;
}
// Replace the block with newlines (or keep in debug mode)
text = `${text.slice(0, start)}\n\n${config.debug ? `${block}\n\n` : ""}${text.slice(end)}`;
};
if (agent === null) {
// Only perform deblocking when agent is null
continue;
}
// Extract and normalize the block content
const str = block.slice(1, -1).trim().replace(/==+/g, "=").replace(/::+/g, ":");
// Prefer "=" over ":" as the key-value delimiter
const delimiter = str.includes("=") ? "=" : ":";
if (2 < str.split(delimiter, 3).length) {
// Skip blocks with too many delimiters
continue;
}
// ==================== DELETE OPERATION ====================
// Check if this is a delete/forget command
/** @returns {string|null} */
const delKey = (() => {
// Match various forms of "delete key_name"
const delMatch1 = str.match(
/^(?:del(?:et(?:e[ds]?|ing))?|for(?:get(?:s|ting)?|got(?:ten)?)|remov(?:e[ds]?|ing))(?:[\s_]*(?:key(?:_name)?|thought|memory|unwanted(?:_key)?))?[\s=:]*([\s\S]*)$/i
);
if (!delMatch1) {
return null;
}
const delKey1 = formatKey(delMatch1[1]);
if (delKey1 in agent.brain) {
// Key exists in brain
return delKey1;
} else if (!/(?:key|thought|memory|unwanted)/i.test(str)) {
// Doesn't look like a common hallucination, might be invalid
return null;
}
// Try again with stricter matching
const delMatch2 = str.match(
/^(?:del(?:et(?:e[ds]?|ing))?|for(?:get(?:s|ting)?|got(?:ten)?)|remov(?:e[ds]?|ing))[\s=:]*([\s\S]*)$/i
);
return delMatch2 ? formatKey(delMatch2[1]) : null;
})();
/**
* Generates a delete log statement
* @param {string} k - Key being deleted
* @returns {string} JavaScript delete statement
*/
const logDelete = (k = "") => `delete ${path()}${(k === "") ? "[\"\"]" : `.${k}`};`;
if ((typeof delKey === "string") && (delKey in agent.brain)) {
// Valid delete statement
if (!altered.has(delKey)) {
// Queue the delete operation
operations.push(() => {
delete agent.brain[delKey];
return logDelete(delKey);
});
altered.add(delKey);
}
continue;
} else if (!/\S\s*[=:]+\s*\S/.test(str)) {
// No assignment pattern, skip
continue;
}
// ==================== KEY EXTRACTION ====================
/**
* Gets everything after the last colon in a string
* @param {string} s - Input string
* @returns {string} Content after last colon
*/
const rightOfColon = (s = "") => s.slice(s.lastIndexOf(":") + 1);
// Extract and clean the key name
const key = (() => {
const raw = formatKey((
(delimiter === "=") ? rightOfColon(str.split("=", 1)[0]) : str.split(":", 1)[0]
).trim().replaceAll(" ", "_"));
// If key exists in brain, use it as-is
// Otherwise strip common prefixes/suffixes models tend to add
return (raw in agent.brain) ? raw : (raw
.replace(/^th(?:oughts?|ink(?:ing))_(?:(?:o[nfr]|a(?:bout|nd)|with|for)_)?/, "")
.replace(/(?:_(?:and|or))?_th(?:oughts?|ink(?:ing))$/, "")
);
})();
if ((key === "") || ((
(60 < key.length)
|| ["thought", "thoughts", "think", "thinking", "any_name", "example_name"].includes(key)
|| ["any_key", "key_name", "example_key"].some(s => key.includes(s))
) && !(key in agent.brain))) {
// Skip invalid or placeholder keys copied from the task prompts
continue;
}
// ==================== VALUE EXTRACTION ====================
// Extract and clean the value
const value = (
(str.split(delimiter, 2)[1] || "")
// Strip leading/trailing quotes and whitespace
.replace(/^[\s"'`«»„“”「」´‘’‟‚‛]+|[\s"'`«»„“”「」´‘’‟‚‛]+$/g, "")
.replace(/\s+/g, " ")
);
if (!/[a-z0-9A-Z]/.test(value) || /[\u4e00-\u9fff]/.test(value)) {
// Skip empty or non-latin values because DeepSeek is dumb
continue;
} else if (!value.includes(" ")) {
// ==================== RENAME OPERATION ====================
// No spaces = might be a key rename
if (altered.has(key)) {
continue;
}
const oldKey = formatKey(value);
if (!altered.has(oldKey) && (oldKey in agent.brain)) {
// Valid rename: move thought from old key to new key
// Queue a rename operation
operations.push(() => {
agent.brain[key] = agent.brain[oldKey];
delete agent.brain[oldKey];
const p = path();
return `${p}.${key} = ${p}.${oldKey};\n${logDelete(oldKey)}`;
});
altered.add(key);
altered.add(oldKey);
}
continue;
} else if (value.includes("_")) {
// Underscores in value = probably a malformed key, skip
continue;
}
// ==================== ASSIGN OPERATION ====================
// Extract the actual thought content
const thought = simplify(rightOfColon(value)
.replaceAll("→", " ")
.replaceAll("\\n", "\n")
).trim().split("\n", 1)[0].trimEnd();
if (altered.has(key) || !thought.includes(" ")) {
// Skip if key already touched or thought too short
continue;
} else if (!(key in agent.brain)) {
// Check for duplicate thought values (don't store the same thing twice)
const last = thought.length - 1;
// Potentially hot loop so avoid excessive get() calls
const brain = agent.brain;
for (const key in brain) {
const existing = brain[key];
if (
// This shouldn't be possible but whatevs
(typeof existing === "string")
// Short-circuit on impossible relative lengths for speed
&& (last < existing.length)
// Fast check inclusion
&& (existing.indexOf(thought) !== -1)
) {
// This thought already exists within some thought associated with another key
continue interpreter;
}
}
}
// Queue an assign operation
operations.push(() => {
// Increment the global label counter
IS.label++;
// Encode the label as zero-width chars for context tracking
IS.encoding = `${(IS.encoding === "") ? "\u200B" : IS.encoding}${(() => {
let n = IS.label;
let out = "";
// Convert label to binary using ZWNJ (0) and ZWJ (1)
while (0 < n) {
out = `${(n & 1) ? "\u200D" : "\u200C"}${out}`;
n >>>= 1;
}
return out || "\u200C";
})()}\u200B`;
// Inject the encoding into the output text
text = (text
.replace(/[\u200B-\u200D]+/g, "")
.replace(/^\s*/, leadingWhitespace => `${leadingWhitespace}${IS.encoding}`)
);
// One common complaint from playtesters was that models were storing repeated thoughts
// Upon further investigation, I discovered this was actually miscommunication on my part
// Players assumed the operation log (card entry) was a reflection of the brain (card notes)
// Thus players (reasonably) misinterpreted label updates as repetition
// Solution: Log distinct relabel syntax to improve non-verbal communication
const target = `${path()}.${key}`;
const old = agent.brain[key];
agent.brain[key] = `${IS.label} → ${thought}`;
// Determine if this is a relabel of the same thought value
const relabel = (
(typeof old === "string")
&& (thought === old.slice(old.indexOf("→") + 1).trim())
);
return `${(
relabel ? `old = ${target};\n` : ""
)}${target} = ${(
relabel ? `[${IS.label}, old${(
old.includes("→") ? "\n .slice(old.indexOf(\"→\") + 1)\n .trim()\n" : ".trim()"
)}].join(" → ")` : JSON.stringify(agent.brain[key])
)};`;
});
altered.add(key);
}
// ==================== OUTPUT TEXT SANITIZATION ====================
// Clean up the model's output text before finalizing
// This removes artifacts, formatting issues, and unwanted patterns
text = (simplify(config.debug ? text : text.replaceAll("_", ""))
.trim()
.split("\n")
.filter(line => {
const lower = line.toLowerCase();
return !(
// The nuclear option
/(?:^|[^a-zA-Z])(?:task|output)(?:$|[^a-zA-Z])/.test(lower)
// Common AI hallucinations
|| [
"STRICT",
"OUTPUT",
"REQUIRE",
"EXACT",
"TASK",
"FORMAT",
"inner self",
`You are ${config.player}.`
].some(naughty => line.includes(naughty))
// Remove "story continues" type artifacts from task prompts bleeding through
|| (lower.includes("story") && lower.includes("continu"))
// Remove numbered list items (e.g., "1.", "[1]", "2.")
|| /^\[?\d+(?:\.?\]|\.)/.test(lower)
// Remove stray "user" labels from ChatML imitation
|| /^\s*user(?:$|[^a-z])/.test(lower)
// Remove lines containing only " " and/or "-"
|| /^[ -]+$/.test(lower)
);
})
.join("\n")
.trim()
// Collapse excessive newlines to a maximum of two
.replace(/\n\n\n+/g, "\n\n")
);
// ==================== OUTPUT FINALIZATION ====================
// Handle empty outputs and ensure proper spacing between actions
if (text === "") {
// AID does not tolerate empty string outputs and "please select continue" messages are cringe
// Return encoding if available, otherwise a zero-width space placeholder
text = (IS.encoding === "") ? "\u200B" : IS.encoding;
} else {
// Prepend the thought label encoding to the output text
text = `${IS.encoding}${text}`;
// Ensure all between-action linebreaks are equally spaced
prespace();
}
// ==================== OPERATION EXECUTOR ====================
// Execute queued brain operations and persist changes
if ((operations.length === 0) || (agent === null)) {
// No operations to execute, we're done
return;
}
const hash = historyHash();
if (IS.hash === hash) {
// Same history hash means this turn was a retry or erase + continue
// This prevents duplicate brain modifications on retry (cached outputs cause problems)
return;
} else if (typeof agent.card.entry !== "string") {
// Initialize the brain card entry if it's not a string (shouldn't happen, but safety first)
agent.card.entry = "";
} else if (agent.card.entry.endsWith("UTC") && agent.card.entry.startsWith("// initialized @")) {
// This is a fresh brain card with only the timestamp comment
// I prefer logging this info immediately before processing the first valid operation
// Add metadata and initialize the brain object in the log
agent.card.entry = `${agent.card.entry.trimStart()}\n${path("metadata")} = ${(
JSON.stringify(agent.metadata, null, 2)
)};\n${path()} = {};\n// Entry: Displays recent brain operations to the player\n// Triggers: Configurable settings for this NPC alone\n// Notes: Allows the player to view/edit actual brain contents`;
}
// Update the hashcode to mark this history state as processed
IS.hash = hash;
// Clear the previous encoding since new operations are being committed
IS.encoding = "";
// Execute each queued operation and append to the operation log
for (const operation of operations) {
// Increment global operation counter
IS.ops++;
// Execute the operation (modifies agent.brain) and get the log message
// Append the message to the agent's brain card entry
agent.card.entry = `${agent.card.entry}\n\n// operation ${IS.ops}\n${operation()}`.trimStart();
}
text ||= "\u200B";
// Keep the operation log from growing unbounded
// Limit to approximately 2000 chars to satisfy AID's soft entry limit
agent.card.entry = agent.card.entry.split(/\n\n/).slice(-2000).reduceRight((out, op) => (
// Only include operations that fit within the char limit
((out.length + op.length + 2) < 2001) ? `${op}${out ? `\n\n${out}` : ""}` : out
), "");
// ==================== BRAIN SERIALIZATION ====================
// Rapidly reserialize a flat representation of the modified brain, without heavy memory allocations
// This custom serialization is faster than JSON.stringify for flat objects
// It also produces a more readable format in the story card notes
const brain = agent.brain;
const keys = Object.keys(brain);
if (keys.length === 0) {
agent.card.description = "{}";
return;
}
// Build the JSON-like string manually for each key-value pair
let serialized = "";
const appendPair = config.json ? ((
serialized = `"${keys[0]}": ${JSON.stringify(brain[keys[0]])}`
), (key = "") => {
// Format -> "key": "value",\n\n (JSON with linebreaks)
serialized += `,\n\n"${key}": ${JSON.stringify(brain[key])}`;
return;
}) : ((
serialized = `${keys[0]}: ${brain[keys[0]]}`
), (key = "") => {
// Format -> key: value\n\n (simple user-friendly format)
serialized += `\n\n${key}: ${brain[key]}`;
return;
});
for (let i = 1; i < keys.length; i++) {
appendPair(keys[i]);
}
agent.card.description = serialized;
return;
}
//—————————————————————————————————————————————————————————————————————————————————————
/**
* Auto-Cards v1.1.3
* 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"; const S = {
/*
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?
DEFAULT_DO_AC: true
// (true or false)
,
// Pin the "Configure Auto-Cards" story card at the top of the player's story cards list?
DEFAULT_PIN_CONFIGURE_CARD: false
// (true or false)
,
// Minimum number of turns in between automatic card generation events?
DEFAULT_CARD_CREATION_COOLDOWN: 40
// (0 to 9999)
,
// Use a bulleted list format for newly generated card entries?
DEFAULT_USE_BULLETED_LIST_MODE: true
// (true or false)
,
// Maximum allowed length for newly generated story card entries?
DEFAULT_GENERATED_ENTRY_LIMIT: 600
// (200 to 2000)
,
// Do newly generated cards have memory updates enabled by default?
DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES: false
// (true or false)
,
// Default character limit before the card's memory bank is summarized?
DEFAULT_NEW_CARDS_MEMORY_LIMIT: 3200
// (1750 to 9900)
,
// Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new)
DEFAULT_MEMORY_COMPRESSION_RATIO: 25
// (20 to 1250)
,
// Ignore all-caps during title candidate detection?
DEFAULT_IGNORE_ALL_CAPS_TITLES: true
// (true or false)
,
// Should player input actions (Do/Say/Story) be considered during title detection?
DEFAULT_DETECT_TITLES_FROM_INPUTS: false
// (true or false)
,
// How many (minimum) actions in the past does Auto-Cards look for named entities?
DEFAULT_MINIMUM_LOOK_BACK_DISTANCE: 7
// (2 to 88)
,
// Is Live Script Interface v2 enabled?
DEFAULT_DO_LSI_V2: false
// (true or false)
,
// Should the "Debug Data" story card be visible?
DEFAULT_SHOW_DEBUG_DATA: false
// (true or false)
,
// AI prompt used to generate new story card entries?
DEFAULT_CARD_GENERATION_PROMPT: [
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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?
DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT: [
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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?
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)
DEFAULT_CARD_TYPE: "class"
// ("text")
,
// Should titles mentioned in the "opening" plot component be banned from future card generation by default?
DEFAULT_BAN_TITLES_FROM_OPENING: false
// (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: 600, // 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: 3200 // 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<string>} An array of strings representing used titles
*/
// AutoCards().API.getUsedTitles();
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} An array of banned title strings
*/
// AutoCards().API.getBannedTitles();
/*** Sets the banned titles array, replacing any previously banned titles
*
* @function
* @param {string|Array<string>} 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<Object>|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());
const memoized = new Map();
// Holds a reference to the data card singleton, remains unassigned unless required
let data = null;
// Validate globalThis.text
text = ((typeof text === "string") && text) || "\n";
// Main settings override local settings
if (typeof globalThis.MainSettings === "function") {
new MainSettings("AutoCards", "AC").merge(S);
}
// 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(S.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 (S.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 (Number.isInteger(info.maxChars)) {
// 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
// Remove all /ac commands
.replace(/\s*^.*\/\s*A\s*C.*$\s*/gmi, "\n\n")
// Remove all comfirmation requests and responses
.replace(/\s*\n*.*CONFIRM\s*DELETE.*\n*\s*/gi, confirmation => {
if (confirmation.includes("<<<")) {
return "\n\n";
} 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*(Recent Story:|$)/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")
) + (right !== "") ? ("\n\n" + right) : "");
})
// Remove various Auto-Cards messages
.replace(/(?:\s*>>>[\s\S]*?<<<\s*)+/g, "\n\n")
);
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 Lore:\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
state.InnerSelf ??= {};
state.InnerSelf.AC ??= {};
state.InnerSelf.AC.forced = true;
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: 600, // 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: 3200, // 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<string>} An array of strings representing used titles
*/
getUsedTitles: function() {
return Internal.getUsedTitles(true);
},
/*** Retrieves an array of banned titles
*
* @function
* @returns {Array<string>} 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<string>} 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<Object>|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 used your magic on Leah\\n\" +",
" \"Therefore she is now entirely \" +",
" minds[state.mind]",
" ));",
" 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 (
(S.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 = validateMLBD(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)
// Inner Self
.replace(/\s*[\u200B-\u200D][\s\u200B-\u200D]*/g, " ")
// Localized Languages
.replace(/\s*[–«»„“”「」—]\s*/g, ": ")
.replace(/(?:^|\s+)-/g, ": ").replace(/-(?:\s+|$)/g, ": ")
.replace(/[‘’]/g, "'").replaceAll("´", "`")
// Standardize end punctuation
.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(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Remove a leading bullet
.replace(/^\s*-+\s*/, "")
// Replace common honorifics with a placeholder word
.replace(buildKiller(Words.honorifics), " %@% ")
// Remove common abbreviations
.replace(buildKiller(Words.abbreviations), " ")
// Fix end punctuation
.replace(/\s+\.(?![a-zA-Z])/g, ".").replace(/\.\.+/g, ".")
.replace(/\s+\?(?![a-zA-Z])/g, "?").replace(/\?\?+/g, "?")
.replace(/\s+!(?![a-zA-Z])/g, "!").replace(/!!+/g, "!")
.replace(/\s+:(?![a-zA-Z])/g, ":").replace(/::+/g, ":")
// Colons are treated as substitute end-punctuation, apply the capitalization rule
.replace(/:\s+(\S)/g, (_, next) => ": " + 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
&& (S.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();
let simplifications = 0;
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;
} else if ((2 < simplifications) || (card.entry.includes("<") && card.entry.includes(">"))) {
continue;
}
// 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();
simplifications++;
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 || isCompressing) {
state.InnerSelf ??= {};
state.InnerSelf.AC ??= {};
state.InnerSelf.AC.event = true;
if (isGenerating) {
// Likewise for the card entry generation delimiter
context = context.replaceAll("%@GEN@%", "");
} else {
// 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(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(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(ratio) {
if (permitOutput()) {
CODOMAIN.initialize(
getPrecedingNewlines() + ">>> please select \"continue\" (" + Math.round(ratio * 100) + "%) <<<\n\n"
);
}
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(/(?<!^\s*)(?: - | ?– ?)(?!\s*$)/g, "—");
}
function getConfigureCardTemplate() {
const names = getControlVariants().configure;
return O.f({
type: AC.config.defaultCardType,
title: names.title,
keys: names.keys,
entry: getConfigureCardEntry(),
description: getConfigureCardDescription()
});
}
function getConfigureCardEntry() {
return prose(
"> 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) {
Const.#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) {
Const.#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) {
Const.#throwError("Instances of Const cannot be redeclared");
} else {
Const.#throwError("Instances of Const cannot be redeclared after initialization");
}
}
initialize(...args) {
if (args.length !== 1) {
Const.#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) {
Const.#throwError("Instances of Const cannot be initialized before declaration");
} else {
Const.#throwError("Instances of Const cannot be reinitialized");
}
}
read(...args) {
if (args.length !== 0) {
Const.#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) {
Const.#throwError("Despite prior declaration, instances of Const cannot be read before initialization");
} else {
Const.#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(/\(<isolated-vm>:/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*<isolated-vm>:\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: 600, // 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: 3200, // 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
const coerce = (str) => (typeof str === "string") ? str : "";
// Do not trim card.keys
card.keys = coerce(card.keys);
if (card.keys.includes("\"agent\"") || card.keys.includes("aidungeon")) {
if (isExternal) {
O.s(card);
}
continue;
}
card.type = coerce(card.type).trim();
card.title = coerce(card.title).trim();
card.entry = coerce(card.entry).trim();
card.description = coerce(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: 3200}" (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, 40);
}
function validateEntryLimit(entryLimit) {
return boundInteger(200, entryLimit, 2000, 600);
}
function validateMemoryLimit(memoryLimit) {
return boundInteger(1750, memoryLimit, 9900, 3200);
}
function validateMemCompRatio(memCompressRatio) {
return boundInteger(20, memCompressRatio, 1250, 25);
}
function validateMLBD(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;
}
}
function maybeProse(value) {
if (Array.isArray(value)) {
return prose(...value);
} else {
return value;
}
}
return O.s({
// Is Auto-Cards enabled?
doAC: check(S.DEFAULT_DO_AC),
// Delete all previously generated story cards?
deleteAllAutoCards: null,
// Pin the configuration interface story card near the top?
pinConfigureCard: check(S.DEFAULT_PIN_CONFIGURE_CARD),
// Minimum number of turns in between automatic card generation events?
addCardCooldown: validateCooldown(S.DEFAULT_CARD_CREATION_COOLDOWN),
// Use bulleted list mode for newly generated card entries?
bulletedListMode: check(S.DEFAULT_USE_BULLETED_LIST_MODE),
// Maximum allowed length for newly generated story card entries?
defaultEntryLimit: validateEntryLimit(S.DEFAULT_GENERATED_ENTRY_LIMIT),
// Do newly generated cards have memory updates enabled by default?
defaultCardsDoMemoryUpdates: check(S.DEFAULT_NEW_CARDS_DO_MEMORY_UPDATES),
// Default character limit before the card's memory bank is summarized?
defaultMemoryLimit: validateMemoryLimit(S.DEFAULT_NEW_CARDS_MEMORY_LIMIT),
// Approximately how much shorter should recently compressed memories be? (ratio = 10 * old / new)
memoryCompressionRatio: validateMemCompRatio(S.DEFAULT_MEMORY_COMPRESSION_RATIO),
// Ignore all-caps during title candidate detection?
ignoreAllCapsTitles: check(S.DEFAULT_IGNORE_ALL_CAPS_TITLES),
// Should player input actions (Do/Say/Story) be considered for future named entity detection?
readFromInputs: check(S.DEFAULT_DETECT_TITLES_FROM_INPUTS),
// How many (minimum) actions in the past does Auto-Cards look for named entities?
minimumLookBackDistance: validateMLBD(S.DEFAULT_MINIMUM_LOOK_BACK_DISTANCE),
// Is Live Script Interface v2 enabled?
LSIv2: (function() {
if (S.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(S.DEFAULT_SHOW_DEBUG_DATA, false),
// How should the AI be prompted when generating new story card entries?
generationPrompt: check(maybeProse(S.DEFAULT_CARD_GENERATION_PROMPT), prose(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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(maybeProse(S.DEFAULT_CARD_MEMORY_COMPRESSION_PROMPT), prose(
"-----",
"",
"<SYSTEM>",
"# 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",
"</SYSTEM>",
"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(S.DEFAULT_CARD_TYPE, "class", "string")
});
}
function getDefaultConfigBans() {
if (typeof S.DEFAULT_BANNED_TITLES_LIST === "string") {
return uniqueTitlesArray(S.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) {
const input = title;
let useMemo = false;
if (
(AC.database.titles.used.length === 1)
&& (AC.database.titles.used[0] === ("%@%"))
&& [used, forenames, surnames].every(nameset => (
(nameset.size === 1)
&& nameset.has("%@%")
))
) {
const pair = memoized.get(input);
if (pair !== undefined) {
if (50000 < memoized.size) {
memoized.delete(input);
memoized.set(input, pair);
}
return O.f({newTitle: pair[0], newKey: pair[1]});
}
useMemo = true;
}
title = title.trim();
if (short()) {
return end();
}
title = (title
// Inner Self
.slice(title.indexOf("\u200B") + 1)
.replace(/\u200B-\u200D/g, "")
// Localized Languages
.replace(/[–。?!´؟،«»¿¡„“”「」…§,、\*_~><\(\)\[\]{}#"`:!—;\.\?,\s\\]/g, " ")
// Fix contractions
.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(/(?<![a-zA-Z])'(?![a-zA-Z])/g, "")
// Eliminate fake em dashes and terminal/leading dashes
.replace(/\s-\s/g, " ")
// Condense consecutive whitespace
.trim().replace(/\s+/g, " ")
// Remove a leading or trailing bullet
.replace(/^-+\s*/, "").replace(/\s*-+$/, "")
);
if (short()) {
return end();
}
// Special-cased words
const minorWordsJoin = Words.minor.join("|");
const leadingMinorWordsKiller = new RegExp("^(?:" + minorWordsJoin + ")\\s", "i");
const trailingMinorWordsKiller = new RegExp("\\s(?:" + minorWordsJoin + ")$", "i");
// Ensure the title is not bounded by any outer minor words
title = enforceBoundaryCondition(title);
if (short()) {
return end();
}
// Ensure interior minor words are lowercase and excise all interior honorifics/abbreviations
const honorAbbrevsKiller = new RegExp("(?:^|\\s|-|\\/)(?:" + (
[...Words.honorifics, ...Words.abbreviations]
).map(word => 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 end();
}
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 end();
}
// 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 end();
}
key = enforceBoundaryCondition(
titleWords.filter(word => !peerage.has(word.toLowerCase())).join(" ")
);
if (key.includes(" the ")) {
key = enforceBoundaryCondition(key.split(" the ")[0]);
}
if (isUsedOrBanned(key)) {
return end();
}
}
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;
}
function end(newTitle = "", newKey = "") {
if (useMemo) {
memoized.set(input, [newTitle, newKey]);
if (30000 < memoized.size) {
memoized.delete(memoized.keys().next().value);
}
}
return O.f({newTitle, newKey});
}
return end(title, 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;
}
} function isolateLSIv2(code, log, text, stop) { const console = Object.freeze({log}); try { eval(code); return [null, text, stop]; } catch (error) { return [error, text, stop]; } }
// Your other library scripts go here
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;
}
function getRandomFloat(min, max) {
return Math.random() * (max - min + 1) + min;
}
function getRandomBoolean(chance) {
if (chance == null) chance = .5
return Math.random() <= chance
}
function getRandom(seed) {
var x = Math.sin(seed) * 10000
return x - Math.floor(x)
}
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)
}
function shuffle(array, seed) {
if (seed == null) seed = getRandomInteger(Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)
let currentIndex = array.length
while (currentIndex != 0) {
let randomIndex = Math.floor(getRandom(seed + currentIndex) * currentIndex)
currentIndex--
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]]
}
}
function pointDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
}
function pointDirection(x1, y1, x2, y2) {
Math.atn2
var a = Math.atan2(y2 - y1, x2 - x1);
if (a < 0) a += 2 * Math.PI;
if (a < 0) a += 2 * Math.PI;
if (a < 0) a += 2 * Math.PI;
a = Math.abs((Math.PI * 2) - a);
a *= 180 / Math.PI;
return a;
}
function rotate(cx, cy, x, y, angle) {
var radians = (Math.PI / 180) * angle
var cos = Math.cos(radians)
var sin = Math.sin(radians)
var nx = (cos * (x - cx)) + (sin * (y - cy)) + cx
var ny = (cos * (y - cy)) - (sin * (x - cx)) + cy
return [nx, ny];
}
function createLocation(x, y, name) {
x = Math.round(x)
y = Math.round(y)
var existingLocationIndex = state.locations.findIndex(element => element.name.toLowerCase() == name.toLowerCase())
var location
if (existingLocationIndex == -1) {
location = {
x: x,
y: y,
name: name
}
state.locations.push(location)
} else {
location = state.locations[existingLocationIndex]
location.x = x
location.y = y
location.name = name
}
addStoryCard(location.name, "", "location")
return location
}
function sanitizeText(text) {
if (/^\s*>.*says? ".*/.test(text)) {
text = text.replace(/^\s*>\s/, "")
text = text.replace(/says? "/, "")
text = text.replace(/"\n$/, "")
if (text.split('"').length - 1 % 2 == 1) text += '"'
} else if (/^\s*>\s.*/.test(text)) {
text = text.replace(/^\s*>\s/, "")
text = text.replace(/\.?\n$/, "")
}
return text
}
function getCharacterName(rawText) {
var matches = rawText.match(/(?<=\s+> ).*(?=(\s+#)|( says? "))/)
if (matches != null && matches[0].trim() != "") {
return matches[0].trim()
}
matches = rawText.match(/.*(?= #)/)
if (matches != null && matches[0].trim() != "") {
return matches[0].trim()
}
return null
}
function getPossessiveName(name) {
var possesiveName = "Your"
if (name != "You") {
possesiveName = name
if (name.endsWith("s")) possesiveName += "'"
else possesiveName += "'s"
}
return possesiveName
}
function getCommandName(command) {
var args = getArguments(command)
if (args.length == 0) return null
return args[0]
}
const argumentPattern = /("[^"\\]*(?:\\[\S\s][^"\\]*)*"|'[^'\\]*(?:\\[\S\s][^'\\]*)*'|\/[^\/\\]*(?:\\[\S\s][^\/\\]*)*\/[gimy]*(?=\s|$)|(?:\\\s|\S)+)/g
function getArguments(command) {
var matches = command.match(new RegExp(argumentPattern))
var returnValue = []
matches.forEach(match => {
match = match.replaceAll(/(^")|("$)/g, "").replaceAll(/\\"/g, '"')
returnValue.push(match)
})
return returnValue
}
function getArgument(command, index) {
var args = getArguments(command)
index++
if (index >= args.length) return null
return args[index]
}
function getArgumentRemainder(command, index) {
var counter = 0
const pattern = new RegExp(argumentPattern)
while ((match = pattern.exec(command)) != null) {
if (counter++ == index + 1) {
var result = command.substring(match.index)
if (/^".*"$/g.test(result)) result = result.replace(/^"/, "").replace(/"$/, "")
return result.replaceAll(/\\"/g, '"')
}
}
}
function searchArgument(command, pattern, limit) {
var index = searchArgumentIndex(command, pattern)
if (index == -1 || limit != null && index > limit) return null
return getArgument(command, index)
}
function searchArgumentIndex(command, pattern) {
var args = getArguments(command)
if (args.length <= 1) return -1
args.splice(0, 1)
const search = (element) => pattern.test(element)
var index = args.findIndex(search)
if (index != -1) return index
return -1
}
function arrayToOrPattern(array) {
var pattern = "^"
array.forEach(element => {
pattern += `(${element})|`
})
pattern += pattern.substring(0, pattern.length - 1)
pattern += "$"
return new RegExp(pattern, "gi")
}
function statsToOrPattern(stats) {
var array = []
stats.forEach(element => {
array.push(element.name)
})
return arrayToOrPattern(array)
}
function getDice(rolltext) {
var matches = rolltext.match(/\d+(?=d)/)
if (matches != null) {
return parseInt(matches[0])
}
return 1
}
function getSides(rolltext) {
var matches = rolltext.match(/(?<=d)\d+/)
if (matches != null) {
return parseInt(matches[0])
}
return 20
}
function getAddition(rolltext) {
var matches = rolltext.match(/(\+|-)\s*\d+/)
if (matches != null) {
return parseInt(matches[0].replaceAll(/\s*/g, ""))
}
return 0
}
function formatRoll(text) {
var matches = text.match(/(?<=.*)\d*d\d+(?=.*)(\s*(\+|-)\s*\d+)?/gi)
if (matches != null) {
return matches[0].replaceAll(/\s*\+\s*/g, "+").replaceAll(/\s*-\s*/g, "-")
}
matches = text.match(/\d+/)
if (matches != null) {
return "d" + matches[0]
}
return "d20"
}
function calculateRoll(rolltext) {
rolltext = rolltext.toLowerCase()
var dice = getDice(rolltext)
var sides = getSides(rolltext)
var addition = getAddition(rolltext)
var score = addition;
for (i = 0; i < dice; i++) {
score += getRandomInteger(1, sides)
}
return Math.max(0, score)
}
function getCharacter(characterName) {
if (characterName == null) characterName = state.characterName
if (characterName == null) return null
return state.characters.find(element => element.name.toLowerCase() == characterName.toLowerCase())
}
function hasCharacter(characterName) {
return getCharacter(characterName) != null
}
function createCharacter(name) {
var existingCharacter = getCharacter(name)
if (existingCharacter != null) {
existingCharacter.name = name
existingCharacter.className = "adventurer"
existingCharacter.summary = "An auto generated character. Use #create to create this character"
existingCharacter.inventory = []
existingCharacter.spells = []
existingCharacter.stats = []
existingCharacter.spellStat = null
existingCharacter.meleeStat = null
existingCharacter.rangedStat = null
existingCharacter.skills = []
existingCharacter.experience = 0
existingCharacter.health = 10
existingCharacter.ac = 10
return existingCharacter
}
var character = {
name: name,
className: "adventurer",
summary: "An auto generated character. Use #create to create this character",
inventory: [],
spells: [],
stats: [],
spellStat: null,
meleeStat: null,
rangedStat: null,
skills: [],
experience: 0,
health: 10,
ac: 10
}
state.characters.push(character)
return character
}
function copyCharacter(fromCharacter, toCharacter) {
if (toCharacter != null && fromCharacter != null) {
toCharacter.className = fromCharacter.className
toCharacter.summary = fromCharacter.summary
toCharacter.inventory = [...new Set(fromCharacter.inventory)]
toCharacter.spells = [...new Set(fromCharacter.spells)]
toCharacter.stats = [...new Set(fromCharacter.stats)]
toCharacter.spellStat = fromCharacter.spellStat
toCharacter.meleeStat = fromCharacter.meleeStat
toCharacter.rangedStat = fromCharacter.rangedStat
toCharacter.skills = [...new Set(fromCharacter.skills)]
toCharacter.experience = fromCharacter.experience
toCharacter.health = fromCharacter.health
toCharacter.ac = fromCharacter.ac
return toCharacter
}
}
function deleteCharacter(name) {
var index = state.characters.findIndex((element) => element.name == name)
state.characters.splice(index, 1)
}
function executeTurn(activeCharacter) {
var activeCharacterName = toTitleCase(activeCharacter.name)
var possessiveName = getPossessiveName(activeCharacter.name)
if (possessiveName == "Your") possessiveName = "your"
if (activeCharacter.className != null) {
//player
state.show = "none"
return `\n[It is ${possessiveName} turn]\n`
} 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)
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[Character AC: ${target.ac} Attack roll: ${attack}]\n`
text += `${activeCharacterName} attacks ${targetNameAdjustedCase} for ${damage} damage!\n`
if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious! \n`
else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n`
} else text += `${activeCharacterName} attacks ${targetNameAdjustedCase} but misses!\n`
} else {
var spell = activeCharacter.spells[getRandomInteger(0, activeCharacter.spells.length - 1)]
var diceMatches = spell.match(/(?<=^.*)\d*d\d+((\+|-)\d+)?$/gi)
if (diceMatches == null) text += `${activeCharacterName} casts spell ${spell}!`
else {
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])
target.health = Math.max(target.health - damage, 0)
text += `\n[Character AC: ${target.ac} Attack roll: ${attack}]\n`
text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} for ${damage} damage!`
if (target.health == 0) text += ` ${toTitleCase(target.name)} ${areWord} unconscious!\n`
else text += ` ${toTitleCase(target.name)} ${areWord} at ${target.health} health.\n`
} else text += `${activeCharacterName} casts spell ${spell} at ${targetNameAdjustedCase} but misses!\n`
}
}
return text
}
}
function createEncounter(listName) {
var encounter = {
text: "",
cr: null,
enemies: []
}
var multiplier = 1
if (listName == null) {
listName = "funny"
} if (!isNaN(listName)) {
listName = parseInt(listName)
encounter.cr = listName
if (listName <= 0) listName = "funny"
else if (listName <= 4) listName = "easy"
else if (listName <= 8) listName = "medium"
else if (listName <= 12) listName = "hard"
else if (listName <= 16) listName = "boss"
else listName = "god"
}
switch(listName) {
case "funny":
if (encounter.cr == null) encounter.cr = 1
multiplier = 1
switch (getRandomInteger(0, 100)) {
case 0:
encounter.text = "Character ate some bad beans and now it's finally catching up to them."
break
case 1:
encounter.text = "The party is lost! After taking a wrong turn they have no idea where they are."
break
case 2:
encounter.text = "Character stubbed their toe and now they are hunched over in pain. Very ouch!"
break
case 3:
encounter.text = "The party realized they forgot something absolutely critical to their mission and now they have to go back."
break
case 4:
encounter.text = "It turns out character has been talking to somebody for awhile now only to realize it was a shadow the entire time."
break
case 5:
encounter.text = "Character spots a gold tooth! Score!!!"
break
case 6:
encounter.text = "Suddenly, everyone has to take a dump really bad."
break
case 7:
encounter.text = "The party is pretty hungry. Everyone is looking at eachother ravenously. Something needs to happen and fast."
break
case 8:
encounter.text = "Where does the ruler keep his armies? Up his sleevies!"
break
case 9:
encounter.text = "Suddenly, the AI goes crazy and starts spewing nonsense for several sentences! ;;Ignore all player actions."
break
case 10:
encounter.text = "A mischievous spell is cast on the party! They are now transformed into baby. \"Gah gah!\""
break
case 11:
encounter.text = "Character had a premonition of their death. However, it was just some bad heartburn."
break
case 12:
encounter.text = "A goal without a plan is just a wish."
break
case 13:
encounter.text = "I wish to exit this game and not play anymore. ;;Kill all the characters and immediately move to a game over state."
break
case 14:
encounter.text = "Batman has emerged from behind a rock. He wishes to join your squad."
break
case 15:
encounter.text = "Without warning, you are granted immeasurable power by a genie! You may be granted one wish. However, the genie is hard of hearing and anything you wish for will be misinterpreted in the worst possible way."
break
case 16:
encounter.text = "A cute pupper has caught your scent and is now following you. Unfortunately, all it does is yip all the time and will not stop."
break
case 17:
encounter.text = "The party is suddenly transported to a world exactly like this one identical in every detail except it's 10 centimeters lower than this one."
break
case 18:
encounter.text = "A mysterious breeze catches under Character's skirt. They must do everything in their power to keep their decency!"
break
case 19:
encounter.text = "If you work for a living, why do you kill yourself working?"
break
case 20:
encounter.text = "You find a fully working AK47 with a cache of ammo and grenades!"
break
case 21:
encounter.text = "Uncle Sam suddenly materializes in front of you. He has a special task he wants to present to the party."
break
case 22:
encounter.text = "Character's back is itchy. Sadly, they have no means to scratch it. It's terrifying."
break
case 23:
encounter.text = "Raeleus, the developer of Hashtag DnD, has been summoned before the party. He is very disappointed in what you've been using his scenario for."
break
case 24:
encounter.text = "A nymph is playing a prank on the party! No one can speak and must use hand gestures to communicate."
break
case 25:
encounter.text = "You encounter some sentient pebbles. They stare at you with stoney, judging eyes."
break
case 26:
encounter.text = "It is of popular opinion that Character has been doing a poor job of leading the group."
break
case 27:
encounter.text = "Character raises his hand. The teacher nods toward them. \"Yes?\""
break
case 28:
encounter.text = "Character is surprised by something. Through an unfortunate series of events everyone stumbles upon each other and ends up on the floor, exasperated."
break
case 29:
encounter.text = "What's the point of going on if you're just going to faff about like this?"
break
case 30:
encounter.text = "So it's like this: someboday has got to ask why Character is wearing their clothes inside out. It's been like that all day and it's a problem."
break
case 31:
encounter.text = "Character reveals that their name isn't actually \"Character\". Ya'll been mispronouncing it wrong this entire time and they have been too shy to correct you."
break
case 32:
encounter.text = "There is some weird, localized weather phenomena here. It's literally raining cats and dogs!"
break
case 33:
encounter.text = "It's time to sit down and take a break. This crap is getting out of hand, ain't it?"
break
case 34:
encounter.text = "There are aome cultures that don't have a word for \"green\". Today you learned!"
break
case 35:
encounter.text = "The party lost time and now it's 100 years in the future! Oh boy..."
break
case 36:
encounter.text = "Would you fight one horse sized ant or 500 ant sized horses? Decide now because it's coming..."
break
case 37:
encounter.text = "My projects usually get over-engineered to no end and this is no exception."
break
case 38:
encounter.text = "It's time for a nap. A magical slumber washes over all of you."
break
case 39:
encounter.text = "Gosh darn'it. Dang nabb'it!"
break
case 40:
encounter.text = "Everyone breaks out into contagious dance!"
break
case 41:
encounter.text = "You come across an elaborate trap consisting of poorly worded insults."
break
case 42:
encounter.text = "A sudden wrestling match breaks out in front of you."
break
case 43:
encounter.text = "Character trips over their shoes. Some evil person tied the laces together."
break
case 44:
encounter.text = "You pockets are turned inside out through some sort of dark sorcery. The party is now cash poor."
break
case 45:
encounter.text = "What's the point of anything? You must answer this question before you continue."
break
case 46:
encounter.text = "You are, by some twist of cosmic fate, granted the modern knowledge of electricty and computer technology. What will you use this newfound power for?"
break
case 47:
encounter.text = "Glyphs etched in stone have granted you one super power. However, whatever power you choose has an incredible downside."
break
case 48:
encounter.text = "You have come across a place of incredible significance, however you fail to notice it because of your devil-may-care attitude."
break
case 49:
encounter.text = "Quickly! Untie your shoes and shift them from the left foot to the right foot. This is of utmost importance."
break
case 50:
encounter.text = "A voice is heard in Character's head, \"This is the emergency spell casting broadcast system. Please standby for further instructions.\""
break
case 51:
encounter.text = "A team of mal-intended turkeys approach!"
encounter.enemies = [
createEnemy("Turkey Prime", calculateRoll("5d10+10"), 14, 0, "2d6+3", "d20+2"),
createEnemy("Turkey Lackey A", calculateRoll("5d10+10"), 14, 0, "2d6+3", "d20+2"),
createEnemy("Turkey Lackey B", calculateRoll("5d10+10"), 14, 0, "2d6+3", "d20+2"),
]
break
case 52:
encounter.text = "A calculator is someone who calculates. One such person is here and they mean no good."
encounter.enemies = [
createEnemy("The Calculator", calculateRoll("5d8"), 13, 0, "2d4+2", "d20-1", "Calculate Demise1d20")
]
break
case 53:
encounter.text = "An ensorcelled fountain pen is on its way to write you out of existence!"
encounter.enemies = [
createEnemy("Fountain Pen", calculateRoll("6d10+6"), 13, 0, "1d10+2", "d20+3", "Ink Blot2d4+6")
]
break
case 54:
encounter.text = "Oh no! Your childhood bully has come back and he's joined by a gang of bad dudes."
encounter.enemies = [
createEnemy("Bully", calculateRoll("5d10+10"), 12, 0, "1d8", "d20-5"),
createEnemy("Lackey A", calculateRoll("2d8+2"), 12, 0, "1d8", "d20+1"),
createEnemy("Lackey B", calculateRoll("2d8+2"), 12, 0, "1d8", "d20+1"),
createEnemy("Lackey C", calculateRoll("2d8+2"), 12, 0, "1d8", "d20+1"),
]
break
case 55:
encounter.text = "It's a giant, living pimple and it's ready to burst at you."
encounter.enemies = [
createEnemy("Pimple", calculateRoll("4d8+4"), 14, 0, "1d8+2", "d20+1", "Burst Fire3d6")
]
break
case 56:
encounter.text = "An evil breath fills the room. You must defeat it before you can't breathe anymore."
encounter.enemies = [
createEnemy("Evil Breath", calculateRoll("4d10+4"), 14, 0, "2d6+3", "d20+1", "Stank2d10+5")
]
break
case 57:
encounter.text = "A racoon suddenly appears and slaps you in the face. It's clear that it is challenging you to a duel."
encounter.enemies = [
createEnemy("Racoon", calculateRoll("3d10+3"), 13, 0, "1d8+2", "d20+1", "Slap1d5+2")
]
break
case 58:
encounter.text = "A water bottle sprouts legs. Its unnatural gait betrays it's perverse intentions."
encounter.enemies = [
createEnemy("Water Bottle", calculateRoll("2d10+4"), 12, 0, "1d4", "d20-1", "Drench5d3"),
]
break
case 59:
encounter.text = "A sleeping guard is here. He is alerted once you start farting uncontrollably."
encounter.enemies = [
createEnemy("Guard", calculateRoll("3d10+3"), 14, 0, "1d8+2", "d20+2", "Yawn")
]
break
case 60:
encounter.text = "Your best friend from way back has come around. Sadly you have nothing in common with each other and now you're at odds."
encounter.enemies = [
createEnemy("Best Friend", calculateRoll("3d10+3"), 14, 0, "2d4+4", "d20+4"),
createEnemy("Your Replacement", calculateRoll("5d6"), 14, 0, "1d8+1", "d20+2"),
createEnemy("The New Bestie", calculateRoll("5d6"), 14, 0, "1d8+1", "d20+2"),
]
break
case 61:
encounter.text = "Your shadow has become jealous of all that moving around freely thing you do. It attacks!"
encounter.enemies = [
createEnemy("Shadow", calculateRoll("4d10"), 14, 0, "1d6+2", "d20+3")
]
break
case 62:
encounter.text = "You encounter an accident prone ninja! It clumsily brandishes its weapon but sends it clattering onto the floor before it. The ninja apologizes profusely as it scrambles back into attack position."
encounter.enemies = [
createEnemy("Ninja", calculateRoll("6d10+12"), 14, 0, "2d6+3", "d20+2", "Shuriken3d6+3")
]
break
case 63:
encounter.text = "The AI Dungeon has become sentient and sends its minions forth to destroy you!"
encounter.enemies = [
createEnemy("Mixtral", calculateRoll("3d10+3"), 12, 0, "2d6+1", "d20+2"),
createEnemy("MythoMax", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Wizard", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Pegasus", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2")
]
break
case 64:
encounter.text = "The evil wizard has conjured enemies to slay you. He did not send his best."
encounter.enemies = [
createEnemy("Enthusiastic Intern", calculateRoll("6d10+6"), 10, 0, "1d10+2", "d20+1"),
createEnemy("Retirement Aged Fighter", calculateRoll("6d10+6"), 10, 0, "1d10+2", "d20+1"),
createEnemy("A Common Fool", calculateRoll("6d10+6"), 10, "1d10+2", 0, "d20+1", "Contagious Laughter")
]
break
case 65:
encounter.text = "The Grammar Nazis attck in fool force!"
encounter.enemies = [
createEnemy("Grammar Nazi A", calculateRoll("2d8"), 13, 0, "1d4+3", "d20+3", "Correct Grammar"),
createEnemy("Grammar Nazi B", calculateRoll("2d8"), 13, 0, "1d4+3", "d20+3", "Correct Grammar"),
createEnemy("Grammar Nazi C", calculateRoll("2d8"), 13, 0, "1d4+3", "d20+3", "Correct Grammar")
]
break
case 66:
encounter.text = "The mini-people have contracted their deadliest assassin to assault you. The twelve-inch pianist is on the move!"
encounter.enemies = [
createEnemy("Pianist", calculateRoll("5d10+10"), 12, 0, "2d6+3", "d20+2", "Deadly Tune5d4")
]
break
case 67:
encounter.text = "Take it easy this time. A rude tortoise is slowly coming this way and it has foul plans for the party."
encounter.enemies = [
createEnemy("Rude Tortoise", calculateRoll("5d10+10"), 11, 0, "2d6+3", "d20+2", "Shell Up")
]
break
case 68:
encounter.text = "The ninja-mime attacks! Why won't he say anything?"
encounter.enemies = [
createEnemy("Ninja Mime", calculateRoll("2d6"), 15, 0, "1d6+2", "d20+2", "Pantomime3d6")
]
break
case 69:
encounter.text = "A disgusting fashion faux pas presents itself in front of the party. Defend yourself."
encounter.enemies = [
createEnemy("Ill Fitting Clothes", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Color Clash", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Pleated Pants", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Black and White Stripes", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2")
]
break
case 70:
encounter.text = "A miserable plate of unfinished food reveals itself. \"Waste not, want not,\" it says menacingly."
encounter.enemies = [
createEnemy("Food Plate", calculateRoll("2d8+2"), 12, 0, "1d6+1", "d20+1", "Glutton")
]
break
case 71:
encounter.text = "A spork is a multifunctional utensil. It turns its malicious gaze towards you."
encounter.enemies = [
createEnemy("Spork", calculateRoll("2d8+2"), 16, 0, "1d8+1", "d20+1", "Transform into Fork", "Transform into Spoon")
]
break
case 72:
encounter.text = "An angry mother-in-law nags into your general area."
encounter.enemies = [
createEnemy("Mother-In-Law", calculateRoll("6d10+18"), 13, 0, "2d10+4", "d20+1", "Nag", "Verbal Assault3d10")
]
break
case 73:
encounter.text = "A squad of trick motorcyclists wheel into your area! They're doing dastardly maneuvers in front of your face."
encounter.enemies = [
createEnemy("Motorcyclist A", calculateRoll("6d10+12"), 12, 0, "2d6+4", "d20+2", "Trick"),
createEnemy("Motorcyclist B", calculateRoll("6d10+12"), 12, 0, "2d6+4", "d20+2", "Trick"),
createEnemy("Motorcyclist C", calculateRoll("6d10+12"), 12, 0, "2d6+4", "d20+2", "Trick"),
createEnemy("Ring Leader", calculateRoll("7d8+21"), 15, 0, "1d6+2", calculateRoll("d20"), "Command")
]
break
case 74:
encounter.text = "Some stressed out people are here and they misinterpret your casual greeting as a taunt. Emotions are on high."
encounter.enemies = [
createEnemy("Angry Guy", calculateRoll("8d12+8"), 12, 0, "2d8+4", "d20+2"),
createEnemy("Stressed Pal", calculateRoll("8d12+8"), 12, 0, "2d8+4", "d20+2"),
createEnemy("The Instigator", calculateRoll("8d12+8"), 12, 0, "2d8+4", calculateRoll("d20+10"), "Instigate"),
]
break
case 75:
encounter.text = "An ominous stop sign is placed here at odds with your sensibility. It directs you to cease moving at once."
encounter.enemies = [
createEnemy("Stop Sign", calculateRoll("6d8"), 14, 0, "2d6+2", "d20+2", "Red Light", "Green Light")
]
break
case 76:
encounter.text = "Elliot Carver, the villain from Tomorrow Never Dies, ridicules your martial ability. You will not let this stand."
encounter.enemies = [
createEnemy("Elliot Carver", calculateRoll("6d10+12"), 12, 0, "2d8+4", "d20+1", "Berate3d10"),
createEnemy("Goon A", calculateRoll("3d10+3"), 11, 0, "3d10+5", "d20-1"),
createEnemy("Goon B", calculateRoll("3d10+3"), 11, 0, "3d10+5", "d20-1"),
]
break
case 77:
encounter.text = "So, you accidentally punched yourself while carrying a load and picking up something off the ground at the same time. Pissed off, you decide to fight yourself."
encounter.enemies = [
createEnemy("Yourself", calculateRoll("6d8+18"), 15, 0, "1d6+3", "d20+3")
]
break
case 78:
encounter.text = "A suspension of bad feelings waddles toward you. You have no choice but to take to arms."
encounter.enemies = [
createEnemy("Sad", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
createEnemy("Mad", calculateRoll("6d10+12"), 8, 0, "2d6+3", "d20-2"),
createEnemy("Depressed", calculateRoll("6d10+12"), 8, 0, "2d6+3", "d20-2"),
createEnemy("Upset", calculateRoll("6d10+12"), 8, 0, "2d6+3", "d20-2")
]
break
case 79:
encounter.text = "A gambler is here. Feeling cheated, he directs his misfortune at you."
encounter.enemies = [
createEnemy("A gambler", calculateRoll("7d10+21"), 13, 0, "2d8+5", "d20+1", "Even the Odds")
]
break
case 80:
encounter.text = "You know how they say, \"You miss 100% of the shots you don't take?\" Well here's that shot you didn't take."
encounter.enemies = [
createEnemy("The Shot", calculateRoll("5d10+5"), 13, 0, "1d10+2", "d20+2")
]
break
case 81:
encounter.text = "You know how they say, \"The path to hell is paved with good intentions?\" Here are those good intentions."
encounter.enemies = [
createEnemy("Good Intention A", calculateRoll("6d8+12"), 14, 0, "1d6-1", "d20+2"),
createEnemy("Good Intention B", calculateRoll("6d8+12"), 14, 0, "1d6-1", "d20+2"),
createEnemy("Good Intention C", calculateRoll("6d8+12"), 14, 0, "1d6-1", "d20+2")
]
break
case 82:
encounter.text = "It's the AI from AI Dungeon! Man your battle stations."
encounter.enemies = [
createEnemy("AI", calculateRoll("5d10+10"), 12, 0, "2d4+2", "d20+1", "Hallucinate")
]
break
case 83:
encounter.text = "You shouldn't have kicked that puppy when you were a kid because it's all grown up and it remembers."
encounter.enemies = [
createEnemy("Vengeance Dog", calculateRoll("6d8+18"), 14, 0, "1d8+2", "d20+2", "Intimidating Growl")
]
break
case 84:
encounter.text = "Hah! You're screwed now! Your worst fear is actualized in front of you. It's a thing!"
encounter.enemies = [
createEnemy("Thing You Fear Most", calculateRoll("10d10+20"), 13, 0, "2d6+4", "d20+1", "Morph")
]
break
case 85:
encounter.text = "Well, if it isn't the consequences of your actions."
encounter.enemies = [
createEnemy("Consequence A", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Consequence B", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Consequence C", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Consequence D", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2"),
createEnemy("Consequence E", calculateRoll("3d8+9"), 8, 0, "1d6+1", "d20-2")
]
break
case 86:
encounter.text = "A thousand armies of the Persian empire descend upon you. They will drink the rivers dry. Their arrows will blot out the sun."
encounter.enemies = [
createEnemy("One Thousand", calculateRoll("8d12+16"), 15, 0, "4d6+4", calculateRoll("d20")),
createEnemy("Two Thousand", calculateRoll("8d12+16"), 15, 0, "4d6+4", calculateRoll("d20")),
createEnemy("Three Thousand", calculateRoll("8d12+16"), 15, 0, "4d6+4", calculateRoll("d20"))
]
break
case 87:
encounter.text = "The embodiment of really poor decisions and a hangover approaches you in a disturbing pattern."
encounter.enemies = [
createEnemy("Hangover", calculateRoll("8d8+16"), 15, 0, "2d6+3", "d20-1", "Head Pain3d6"),
createEnemy("Poor Decision A", calculateRoll("8d8+16"), 15, 0, "2d6+3", "d20-1"),
createEnemy("Poor Decision B", calculateRoll("8d8+16"), 15, 0, "2d6+3", "d20-1")
]
break
case 88:
encounter.text = "An annoying sales-person is using hard-sale tactics at you. Resist!"
encounter.enemies = [
createEnemy("Sales-Person", calculateRoll("8d8+16"), 14, 0, "1d6+4", "d20+4", "Big Sale")
]
break
case 89:
encounter.text = "A group of Reddit mods are here. This is not a good sign."
encounter.enemies = [
createEnemy("Reddit Mod A", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Reddit Mod B", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Reddit Mod C", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Reddit Mod D", calculateRoll("2d6"), 12, 0, "1d4+2", "d20+2"),
createEnemy("Grand Reddit Mod", calculateRoll("7d10+14"), 15, 0, "1d10+2", "d20+1")
]
break
case 90:
encounter.text = "An expert in bulshiddo beckoms toward you. You have no other choice but to engage."
encounter.enemies = [
createEnemy("Bulshiddo Expert", calculateRoll("7d10+14"), 15, 0, "1d10+2", "d20+1", "Focus Chit")
]
break
case 91:
encounter.text = "You know how they say \"Hope springs eternal?\" Well, Hope is springing her way to your direction."
encounter.enemies = [
createEnemy("Hope", calculateRoll("7d8+14"), 15, 0, "1d8+3", "d20+1", "Spring Eternal5d4")
]
break
case 92:
encounter.text = "An immodest pixie points and laughs at you. She thinks you're funny looking. Battle is inevitable."
encounter.enemies = [
createEnemy("Pixie", calculateRoll("12d12+12"), 12, 0, "2d6+3", calculateRoll("d20"), "Shameless Magic2d10")
]
break
case 93:
encounter.text = "A touring squad of basketball players send a stray rebound right to your head. It's on now!"
encounter.enemies = [
createEnemy("Scottie P", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
createEnemy("M Jordan", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
createEnemy("L Bird", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
createEnemy("Magic J", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
createEnemy("C Barkley", calculateRoll("6d10+12"), 8, 0, "2d6+2", "d20-2"),
]
break
case 94:
encounter.text = "What happens if you fight the void? Let's see."
encounter.enemies = [
createEnemy("The Void", calculateRoll("5d10+5"), 13, 0, "1d10+2", "d20+2", "Darkness")
]
break
case 95:
encounter.text = "Oh god, I've written so many encounters at this point. Just use your imagination."
encounter.enemies = [
createEnemy("Your Imagination", calculateRoll("6d8+12"), 14, 0, "1d6-1", "d20+2", "Detect Thoughts")
]
break
case 96:
encounter.text = "A tachometer is revving up!"
encounter.enemies = [
createEnemy("Tachometer", calculateRoll("5d10+10"), 14, 0, "2d6+3", "d20+2", "Accelerate")
]
break
case 97:
encounter.text = "Taco Tuesdays are back and with a serious vengeance."
encounter.enemies = [
createEnemy("Taco Tuesday A", calculateRoll("9d8+18"), 11, 0, "1d6+2", "d20+1", "Diarrhea3d10"),
createEnemy("Taco Tuesday B", calculateRoll("9d8+18"), 11, 0, "1d6+2", "d20+1", "Diarrhea3d10")
]
break
case 98:
encounter.text = "There's like one dude screaming here. It's not clear who he's yelling at or why. Can you please shut him up?"
encounter.enemies = [
createEnemy("Screaming Dude", calculateRoll("5d10+10"), 14, 0, "2d6+3", "d20+2", "Scream")
]
break
case 99:
encounter.text = "Magneto has somehow entered your world. He blames you for this injustice."
encounter.enemies = [
createEnemy("Magneto", calculateRoll("6d8+18"), 14, 0, "1d8+2", "d20+2", "Manipulate Metal", "Shield"),
createEnemy("The Juggernaut", calculateRoll("6d8+18"), 14, 0, "1d8+2", "d20+2", "Charge3d10"),
createEnemy("Sabretooth", calculateRoll("6d8+18"), 14, 0, "1d8+2", "d20+2"),
]
break
case 100:
encounter.text = "A banker has come to collect on your debts."
encounter.enemies = [
createEnemy("The Banker", calculateRoll("10d10+20"), 13, 0, "2d6+4", "d20+1", "Prisoner's Dilemma", "PMT Function3d6", "Tax Break")
]
break
case 101:
encounter.text = "The mouse from If You Give a Mouse a Cookie... Attack!"
encounter.enemies = [
createEnemy("Mouse", calculateRoll("10d10+20"), 13, 0, "2d6+4", "d20+1", "Take Cookie")
]
}
break
case "easy":
if (encounter.cr == null) encounter.cr = 1
multiplier = 1 + (encounter.cr - 1) / 10
switch (getRandomInteger(0, 99)) {
case 0:
encounter.text = "There is a curious contraption encasing what appears to be a small treasure chest."
break
case 1:
encounter.text = "A ruined wall lined with a series of holes runs parallel to you. The distinct outline of pressure plates mark the way ahead."
break
case 2:
encounter.text = "A rotting corpse lays here. It appears to be of an adventurer like you."
break
case 3:
encounter.text = "A hive of bees hangs not too far from your location."
break
case 4:
encounter.text = "A dog is sitting on its hind legs, looking at you quizzically."
break
case 5:
encounter.text = "A dog is sitting on its hind legs, looking at you quizzically."
break
case 6:
encounter.text = "You notice runes written in a spiralling pattern on the ground."
break
case 7:
encounter.text = "Bones are laid out in a geometric pattern. It appears to be a site of a mysterious ritual."
break
case 8:
encounter.text = "The unmistakable sound of ticking can be heard here."
break
case 9:
encounter.text = "You see a chest with a broken padlock placed conspicuously in the clearing."
break
case 10:
encounter.text = "A warrior close to death gestures for you to draw near."
break
case 11:
encounter.text = "A shrine built to honor a forgotten god has been erected here."
break
case 12:
encounter.text = "Evidence of a struggle is apparent. Marks line the ground followed by a splatter of blood."
break
case 13:
encounter.text = "Tracks lead away from you. Someone has been here before..."
break
case 14:
encounter.text = "An old suit of armor lies here. Nothing remains inside it except the distinct the smell of blood and charred meat."
break
case 15:
encounter.text = "A glittering in the distance catches your eye."
break
case 16:
encounter.text = "A spread of food is arranged on tables before you. An absolute feast! Though it is very out of place here..."
break
case 17:
encounter.text = "\"Oink oink!\" How did a pig get here?"
break
case 18:
encounter.text = "Gold coins are strewn about. Who do they belong to?"
break
case 19:
encounter.text = "A mysterious golden sprout is growing here."
break
case 20:
encounter.text = "A gnome merchant greets you with a sly smile."
break
case 21:
encounter.text = "A curious anvil and hammer are situated here. The markings in its cold metal must have some significance."
break
case 22:
encounter.text = "A sudden feeling of dread washes over you. A dark presence looms in this place."
break
case 23:
encounter.text = "A grave of what appears to be a great wizard is before you."
break
case 24:
encounter.text = "A cheap box has been thrown to the side. An apparent discard."
break
case 25:
encounter.text = "A sack of rotten fruit is stinking up the place."
break
case 26:
encounter.text = "The floor is covered with mushrooms. What are their properties?"
break
case 27:
encounter.text = "You hear the sound of flowing water."
break
case 28:
encounter.text = "Barrels of wine are gathered in rows."
break
case 29:
encounter.text = "The abandoned laboratory of an alchemist resides here."
break
case 30:
encounter.text = "Various herbs and alchemical ingredients are arranged in neat pouches. Vials with unknown ingredients are aligned across a tabletop."
break
case 31:
encounter.text = "A library of ancient texts resides in this place."
break
case 32:
encounter.text = "You encounter a frightened young girl who is incredibly lost."
break
case 33:
encounter.text = "An ornate chest is set on top of pedestal. The locking mechanism is unlike anything you've seen before."
break
case 34:
encounter.text = "The casket of a long dead king lays upon a mound. It's reinforced with steel and the lid looks incredibly heavy."
break
case 35:
encounter.text = "In the center of the open space before you is a glass orb. It seems to levitate by some other worldly power."
break
case 36:
encounter.text = "A traveling bard waves at you. \"Hello there!\""
break
case 37:
encounter.text = "A person of royal distinction is here. You recognize the crest."
break
case 38:
encounter.text = "A skeleton warrior is alerted by your presence. \"Wait!\" it pleads with its harsh, gravely voice."
break
case 39:
encounter.text = "A deer! It's a deer! And it's not afraid of you. How peculiar!"
break
case 40:
encounter.text = "A pool of water is at the center of this place."
break
case 41:
encounter.text = "You are approached by a royal souting party. \"What are you doing here?\""
break
case 42:
encounter.text = "You are ambushed by a group of forest elves! However, their serious faces soften when they realize who you are."
break
case 43:
encounter.text = "A demonic blacksmith suddenly appears before you in a puff of smoke. What does he want?"
break
case 44:
encounter.text = "A strange tree is growing right through the ground. It seems to be moving as if a silent wind is blowing through it."
break
case 45:
encounter.text = "A wild horse seems to be trapped here."
break
case 46:
encounter.text = "You think you hear the laughter of children, but you don't see anyone. It could just be your imagination."
break
case 47:
encounter.text = "Broken pottery is littered all over this place."
break
case 48:
encounter.text = "This was once a prison. Iron bars and the ruins of gated walls abound."
break
case 49:
encounter.text = "It looks like there was a cave in. Through the rubble you hear the faint sound of someone calling out."
break
case 50:
encounter.text = "A vengeful spirit is disturbed by your presence!"
encounter.enemies = [
createEnemy("Specter", calculateRoll("5d8"), 12, 4, "1d10", "d20+2", "Life Drain1d10")
]
break
case 51:
encounter.text = "An awakened shrub shifts towards you. It seems like it was guarding this place."
encounter.enemies = [
createEnemy("Awakened Shrub", calculateRoll("2d+6"), 9, -3, "1d4-1", "d20-1")
]
break
case 52:
encounter.text = "Brigands are attacking!"
encounter.enemies = [
createEnemy("Brigand A", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20"),
createEnemy("Brigand B", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20"),
createEnemy("Brigand C", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20")
]
break
case 53:
encounter.text = "A wounded black bear is here. Baring its teeth, it seems to consider you as a threat."
encounter.enemies = [
createEnemy("Black Bear", calculateRoll("3d8+6"), 11, 6, "2d4+2", "d20")
]
break
case 54:
encounter.text = "An angry boar is charging at you!"
encounter.enemies = [
createEnemy("Boar", calculateRoll("2d8+2"), 11, 4, "1d6+1", "d20")
]
break
case 55:
encounter.text = "Two cockatrices have somehow flanked you on both sides. Prepare for battle!"
encounter.enemies = [
createEnemy("Cockatrice A", calculateRoll("6d6+6"), 11, 1, "1d4+1", "d20+1", "Petrifying Bite1d4+1"),
createEnemy("Cockatrice B", calculateRoll("6d6+6"), 11, 1, "1d4+1", "d20+1", "Petrifying Bite1d4+1")
]
break
case 56:
encounter.text = "You've fallen into a pit of snakes. A group of them slither up to you, winding up to strike."
encounter.enemies = [
createEnemy("Snake", calculateRoll("2d10+2"), 12, 6, "1d8+2", "d20+2", "Poison Bite2d4+1")
]
break
case 57:
encounter.text = "A dire wolf has caught your scent. It's going to make its move!"
encounter.enemies = [
createEnemy("Dire Wolf", calculateRoll("5d10+10"), 14, 8, "2d6+3", "d20+2")
]
break
case 58:
encounter.text = "The remarkable pattern on the skin of this giant frog is a clear indication that it's poisonous. Watch out!"
encounter.enemies = [
createEnemy("Giant Frog", calculateRoll("4d8"), 11, 4, "1d6+1", "d20+1", "Poison2d4+1")
]
break
case 59:
encounter.text = "A very intricate gargoyle statue has been placed here as a centerpiece in an altar. Without warning, it turns its head toward you and its lips turn upwards into an evil grin."
encounter.enemies = [
createEnemy("Gargoyle", calculateRoll("7d8+21"), 15, 6, "1d6+2", "d20")
]
break
case 60:
encounter.text = "You have disturbed an ancient burial ground! Ghouls have risen from their eternal sleep."
encounter.enemies = [
createEnemy("Ghoul A", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2"),
createEnemy("Ghoul B", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2"),
createEnemy("Ghoul C", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2"),
createEnemy("Ghoul D", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2")
]
break
case 61:
encounter.text = "A giant badger has claimed this plot of land and he'll be damned if he lets the likes of you tread all over it."
encounter.enemies = [
createEnemy("Giant Badger", calculateRoll("2d8+4"), 10, 4, "1d6+1", "d20")
]
break
case 62:
encounter.text = "A giant centipede crawls into view. Its terrifying shriek makes your skin crawl. Prepare yourself!"
encounter.enemies = [
createEnemy("Giant Centipede", calculateRoll("1d6+1"), 13, 1, "1d4+2", "d20+2")
]
break
case 63:
encounter.text = "A feral pack of hyenas have nested here. They smell your blood..."
encounter.enemies = [
createEnemy("Hyena A", calculateRoll("1d8+1"), 11, 2, "1d6", "d20+1"),
createEnemy("Hyena B", calculateRoll("1d8+1"), 11, 2, "1d6", "d20+1"),
createEnemy("Giant Hyena", calculateRoll("6d10+12"), 12, 5, "2d6+3", "d20+2")
]
break
case 64:
encounter.text = "A swarm of rats are sweeping across the ground and heading toward you!"
encounter.enemies = [
createEnemy("Swarm of Rats", calculateRoll("7d8-7"), 10, 1, "2d6", "d20")
]
break
case 65:
encounter.text = "A group of gnolls have coalesced here. Your arrival has activated their senses."
encounter.enemies = [
createEnemy("Gnoll A", calculateRoll("5d8"), 15, 6, "1d8+2", "d20+1"),
createEnemy("Gnoll B", calculateRoll("5d8"), 15, 6, "1d8+2", "d20+1"),
createEnemy("Gnoll C", calculateRoll("5d8"), 15, 6, "1d8+2", "d20+1")
]
break
case 66:
encounter.text = "The group of goblins are as surprised to see you as you are to see them. Get ready!"
encounter.enemies = [
createEnemy("Goblin A", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2"),
createEnemy("Goblin B", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2"),
createEnemy("Goblin C", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2"),
createEnemy("Goblin D", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2")
]
break
case 67:
encounter.text = "A sweet lullaby carries through the air. You are enchanted by its melody. But wait! It's a harpy's luring song. Resist!"
encounter.enemies = [
createEnemy("Harpy", calculateRoll("7d8+7"), 11, 4, "2d4+1", "d20+1", "Luring Song")
]
break
case 68:
encounter.text = "You spot a hobgoblin crouched over his battle axe. As he sharpens its blade, he menaces at you, \"You shouldn't have come here, meat.\""
encounter.enemies = [
createEnemy("Hobgoblin", calculateRoll("2d8+2"), 18, 4, "1d8+1", "d20+1")
]
break
case 69:
encounter.text = "A kobold sticks his head out from barrel. Aware of your presence, it signals to the rest of the pack."
encounter.enemies = [
createEnemy("Kobold A", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2"),
createEnemy("Kobold B", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2"),
createEnemy("Kobold C", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2"),
createEnemy("Kobold D", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2")
]
break
case 70:
encounter.text = "A magically entranced Satyr has come into view. Some dark force is compelling it to attack you!"
encounter.enemies = [
createEnemy("Satyr", calculateRoll("5d8"), 15, 4, "1d8+2", "d20+3")
]
break
case 71:
encounter.text = "A skeleton archer has the drop on you! It looses a bolt that very narrowly misses its mark. Get ready for a fight!"
encounter.enemies = [
createEnemy("Skeleton", calculateRoll("2d8+4"), 13, 5, "1d6+2", "d20+2")
]
break
case 72:
encounter.text = "A group of stirge descend on you. Their stink should have given them away."
encounter.enemies = [
createEnemy("Strige A", calculateRoll("1d4"), 14, 2, "1d4+3", "d20+1", "Blood Drain2d4+6"),
createEnemy("Strige B", calculateRoll("1d4"), 14, 2, "1d4+3", "d20+1", "Blood Drain2d4+6"),
createEnemy("Strige C", calculateRoll("1d4"), 14, 2, "1d4+3", "d20+1", "Blood Drain2d4+6")
]
break
case 73:
encounter.text = "You have never seen a Worg this large before. Its snarling teeth glint in the light."
encounter.enemies = [
createEnemy("Worg", calculateRoll("4d10+4"), 13, 8, "2d6+3", "d20+1")
]
break
case 74:
encounter.text = "Everywhere you look... Zombies! You are surrounded as they shuffle towards you, drawn by the heat of your flesh."
encounter.enemies = [
createEnemy("Zombie A", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2"),
createEnemy("Zombie B", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2"),
createEnemy("Zombie C", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2"),
createEnemy("Zombie D", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2"),
createEnemy("Zombie E", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2")
]
break
case 75:
encounter.text = "Your arrival has interrupted a dark acolyte's ritual. His eyes go steely as he plans his retribution."
encounter.enemies = [
createEnemy("Dark Acolyte", calculateRoll("2d8"), 10, 2, "1d4", "d20", "Sacred Flame2d6", "Bless")
]
break
case 76:
encounter.text = "A bandit duo eyes your gear. A combination of greed and desperation drive them to attack you."
encounter.enemies = [
createEnemy("Bandit A", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20"),
createEnemy("Bandit B", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20")
]
break
case 77:
encounter.text = "A crowd of cultists walk into view. Their aim is clear: to sacrifice you as an offering to their god. Attack before you are torn limb from limb!"
encounter.enemies = [
createEnemy("Cultist", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1")
]
break
case 78:
encounter.text = "A gnoll has strayed from its herd. You take the opportunity to attack before it alerts the others."
encounter.enemies = [
createEnemy("Gnoll", calculateRoll("5d8"), 15, 4, "1d4+2", "d20+1")
]
break
case 79:
encounter.text = "A majestic white wolf has been tracking you for some time. It can no longer ignore its hunger pangs and begins racing at you."
encounter.enemies = [
createEnemy("White Wolf", calculateRoll("2d8+2"), 13, 7, "2d4+2", "d20+2")
]
break
case 80:
encounter.text = "An orc looks up from his fire. You're spotted!"
encounter.enemies = [
createEnemy("Orc", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1")
]
break
case 81:
encounter.text = "A wyrmling is spotted circling around you. It's coming in for an attack!"
encounter.enemies = [
createEnemy("Wyrmling", calculateRoll("3d8+3"), 16, 4, "1d10+2", "d20")
]
break
case 82:
encounter.text = "A pack of death dogs are directly ahead. There is no avoiding their dark gaze. Prepare for combat!"
encounter.enemies = [
createEnemy("Death Dog A", calculateRoll("6d8+12"), 12, 6, "1d6+2", "d20+2", "Diseased Bite2d6+2"),
createEnemy("Death Dog B", calculateRoll("6d8+12"), 12, 6, "1d6+2", "d20+2", "Diseased Bite2d6+2"),
createEnemy("Death Dog C", calculateRoll("6d8+12"), 12, 6, "1d6+2", "d20+2", "Diseased Bite2d6+2"),
createEnemy("Death Dog D", calculateRoll("6d8+12"), 12, 6, "1d6+2", "d20+2", "Diseased Bite2d6+2")
]
break
case 83:
encounter.text = "The deep gnome mercenary looks at you with a sinister scowl. \"I don't very much like you.\" He brandishes a sword and prepares to attack you."
encounter.enemies = [
createEnemy("Deep Gnome Merc", calculateRoll("3d6+6"), 15, 6, "1d8+2", "d20+2", "Poison Dart1d4+2", "Stone Camoflage")
]
break
case 84:
encounter.text = "That stink! What is it? A dretch raises its head, clearly alarmed by your presence. It's going to rush you!"
encounter.enemies = [
createEnemy("Dretch", calculateRoll("4d6+4"), 11, 2, "2d4", "d20")
]
break
case 85:
encounter.text = "A drow raider! Take cover!"
encounter.enemies = [
createEnemy("Drow Raider", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2")
]
break
case 86:
encounter.text = "It's not your imagination: a gray ooze is seeping through the cracks. It coalesces into a hideous aberration before you."
encounter.enemies = [
createEnemy("Gray Ooze", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2", "Corrode Metal")
]
break
case 87:
encounter.text = "You catch a Grimlock tearing meat from the leg of some hapless victim. It drops it immediately once it spots you."
encounter.enemies = [
createEnemy("Grimlock", calculateRoll("2d8+2"), 11, 8, "1d4+3", "d20+1")
]
break
case 88:
encounter.text = "Who summoned these homunculi? They don't look friendly. Suddenly, a loud shriek!"
encounter.enemies = [
createEnemy("Homunculus A", calculateRoll("2d4"), 13, 1, "1d10", "d20+2", "Poison Bite1d10"),
createEnemy("Homunculus B", calculateRoll("2d4"), 13, 1, "1d10", "d20+2", "Poison Bite1d10"),
createEnemy("Homunculus C", calculateRoll("2d4"), 13, 1, "1d10", "d20+2", "Poison Bite1d10"),
createEnemy("Homunculus D", calculateRoll("2d4"), 13, 1, "1d10", "d20+2", "Poison Bite1d10")
]
break
case 89:
encounter.text = "A group of lemures block the way. Their hideous forms disgust you."
encounter.enemies = [
createEnemy("Lemure", calculateRoll("3d8"), 7, 3, "1d4", "d20-3")
]
break
case 90:
encounter.text = "The sulfur stink reaches your nose first, then the sight of them: a group of Magmin are on approach."
encounter.enemies = [
createEnemy("Magmin A", calculateRoll("2d6+2"), 14, 2, "2d6", "d20+2", "Fire Touch3d6"),
createEnemy("Magmin B", calculateRoll("2d6+2"), 14, 2, "2d6", "d20+2", "Fire Touch3d6"),
createEnemy("Magmin C", calculateRoll("2d6+2"), 14, 2, "2d6", "d20+2", "Fire Touch3d6")
]
break
case 91:
encounter.text = "Out of the corner of your eye you spot a dark figure moving. \"Quasits!\" You prepare for the ambush."
encounter.enemies = [
createEnemy("Quasit A", calculateRoll("3d4"), 13, 2, "1d4+3", "d20+", "Invisibility", "Scare"),
createEnemy("Quasit B", calculateRoll("3d4"), 13, 2, "1d4+3", "d20+", "Invisibility", "Scare"),
createEnemy("Quasit C", calculateRoll("3d4"), 13, 2, "1d4+3", "d20+", "Invisibility", "Scare"),
createEnemy("Quasit D", calculateRoll("3d4"), 13, 2, "1d4+3", "d20+", "Invisibility", "Scare"),
createEnemy("Quasit E", calculateRoll("3d4"), 13, 2, "1d4+3", "d20+", "Invisibility", "Scare")
]
break
case 92:
encounter.text = "A rust monster! A killing blow will corrode any common weapons."
encounter.enemies = [
createEnemy("Rust Monster", calculateRoll("5d8+5"), 14, 4, "1d8+1", "d20+1")
]
break
case 93:
encounter.text = "An undead specter reveals itself. It's malevolence can be felt in your bones."
encounter.enemies = [
createEnemy("Specter", calculateRoll("5d8"), 12, 4, "3d6", "d20+2", "Life Drain3d6", "Incorporeal Movement")
]
break
case 94:
encounter.text = "Enchanted weasels are commanded to attack you. Oh god, they're everywhere!"
encounter.enemies = [
createEnemy("Weasel A", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel B", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel C", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel D", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel E", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel F", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel G", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel H", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel I", calculateRoll("1d4-1"), 13, 1, "1", "d20+3"),
createEnemy("Weasel J", calculateRoll("1d4-1"), 13, 1, "1", "d20+3")
]
break
case 95:
encounter.text = "An awakened tree lumbers toward you. Apparently, this is sacred ground that you tarnish with your presence."
encounter.enemies = [
createEnemy("Awakened Tree", calculateRoll("7d12+14"), 13, 10, "3d6+4", "d20-2")
]
break
case 96:
encounter.text = "A centaur is leading up to you. It may have been tracking your for some time. Get ready!"
encounter.enemies = [
createEnemy("Centaur", calculateRoll("6d10+12"), 12, 10, "2d6+4", "d20+2", "Charge3d6")
]
break
case 97:
encounter.text = "A gelatinous cube occupies this space. It's wobbling hatefully toward you!"
encounter.enemies = [
createEnemy("Gelatinous Cube", calculateRoll("8d10+40"), 6, "3d6", "d20-4", "Engulf3d6")
]
break
case 98:
encounter.text = "A ghast curls its disgusting tongue, tasting the air. Suddenly, it turns toward you!"
encounter.enemies = [
createEnemy("Ghast", calculateRoll("8d8"), 13, 6, "2d8+3", "d20+3")
]
break
case 99:
encounter.text = "An orc war party comes upon you!"
encounter.enemies = [
createEnemy("Orc A", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1"),
createEnemy("Orc B", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1"),
createEnemy("Orc C", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1"),
createEnemy("Orc D", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1")
]
break
}
break
case "medium":
if (encounter.cr == null) encounter.cr = 5
multiplier = 1 + (encounter.cr - 5) / 10
switch (getRandomInteger(0, 99)) {
case 0:
encounter.text = "Wealth beyond imagining! The floors and shelves of this place are lined with golden trinkets and exquisite curiosities. However, a lone spectre resides in the center. Ominously, it states, \"You may choose one.\""
break
case 1:
encounter.text = "Three pillars stand tall here. You can barely make out the inscriptions on each."
break
case 2:
encounter.text = "A wheel of smelly cheese is seen here. Its owner is long gone."
break
case 3:
encounter.text = "Three bundles of hemp rope have been abandoned here. Someone must have been preparing for a climb."
break
case 4:
encounter.text = "A clay pot is spotted. Within it, a mysterious red powder."
break
case 5:
encounter.text = "A spade and shovel are embedded into the dirt here."
break
case 6:
encounter.text = "Sacks of useless goods are found here. Something does catch your eye, however. A quiver of glistening arrows shimmers in the light."
break
case 7:
encounter.text = "A tailor of some sort must have worked here because there is all manner of clothing lying about."
break
case 8:
encounter.text = "Shoes. Just a pair of shoes sitting on the ground in defiance of any reason or logical explanation."
break
case 9:
encounter.text = "The skeletal remains of what appears to be a dignified noble. His boisterous signet ring demands your attention as if it were calling out to you."
break
case 10:
encounter.text = "A random lever has been constructed here. What does it connect to?"
break
case 11:
encounter.text = "Gears are turning. Their mechnical clicking is fully apparent as you enter the area. Yet nothing is in sight."
break
case 12:
encounter.text = "A trap! You only have moments to react."
break
case 14:
encounter.text = "A crystaline structure pulses with an other-worldly light. It does so even more rapidly as you approach. The tell-tale signs of magical defenses coming online spell certain doom for the party."
break
case 15:
encounter.text = "A decaying bookshelf with numerless tomes rest here. One book seems to be made out of a strange leather unlike the others. Almost on cue, it falls off the shelf, pages flipping through the air dramatically."
break
case 16:
encounter.text = "There is nothing of note here. Or is there?"
break
case 17:
encounter.text = "A tear in space and time. It's as if the heavens crack open before you. A thunderous boom announces the opening of a portal. To where? No one could know. The portal does not look stable and could close at any time."
break
case 18:
encounter.text = "Spikes line this place. That could only mean one thing..."
break
case 19:
encounter.text = "A depression in the ground indicates something to you."
break
case 20:
encounter.text = "A thick mist covers this area. It defies any meteorlogical explanation."
break
case 21:
encounter.text = "An abundance of plants have grown here. A great variety of flora of all types overwhelm your senses."
break
case 22:
encounter.text = "A bard is playing a song in the distance. You recognize the tune."
break
case 23:
encounter.text = "A tiefling refugee races toward you. \"Please! Don't attack!\""
break
case 24:
encounter.text = "A magical darkness shrouds this place. No torch, no magic seems to be able to penetrate it."
break
case 25:
encounter.text = "An herbalist has set up shop here. \"Oh hello!\""
break
case 26:
encounter.text = "A magical adept is fooling around with a door. It seems that he is stressed out. \"Come on, damn you!\""
break
case 27:
encounter.text = "There is a great crevasse in blocking your path. It can be jumped over, but only just by your estimations."
break
case 28:
encounter.text = "A mold has overcome this place. It's thick with bulbous growths."
break
case 29:
encounter.text = "The ruins of someone's home are here."
break
case 30:
encounter.text = "The ground appears to be made out of some sort of glass. You can't see var far through it, but you can't shake the feeling that something malevolent resides here."
break
case 31:
encounter.text = "It looks like a refugee party was slain here. Who could have done such a heinous act?"
break
case 32:
encounter.text = "Did that chest just move now? You investigate."
break
case 33:
encounter.text = "There are so many trees here! Some of them even bear fruit."
break
case 34:
encounter.text = "A medical examination table and assorted surgeon's equipment reside here."
break
case 35:
encounter.text = "A warning is posted here: \"Wanted Criminal - Avoid direct contact\" A reward for information is listed."
break
case 36:
encounter.text = "A machine of unknown origin resides here. It's connections and implements are far beyond anything you've seen before."
break
case 37:
encounter.text = "Barrels of explosives are clearly labeled to be hazardous. It seems that they were rigged to explode all at once but failed to for some unknown reason."
break
case 38:
encounter.text = "A strange feeling comes upon you. There is something significant here."
break
case 39:
encounter.text = "Letters are drawn into the ground. What do they mean? Their placement does not immediately reach a logical explanation either."
break
case 40:
encounter.text = "What is that high above you?"
break
case 41:
encounter.text = "An armory of strange weapons and tools. These were not designed to be the size of the average man."
break
case 42:
encounter.text = "A robber's stash. It seems so obvious. Could it be booby-trapped."
break
case 43:
encounter.text = "A glass bottle filled with a mysterious liquid."
break
case 44:
encounter.text = "A tiny creature is here. It's so adorable! Its is injured. Awww! But something isn't quite right about it..."
break
case 45:
encounter.text = "Someone left a stack of books here. They're glowing."
break
case 46:
encounter.text = "A twisted riddle is etched into the stone."
break
case 47:
encounter.text = "A lone knight blocks your way. Can he be reasoned with?"
break
case 48:
encounter.text = "A company of actors and comedians are here. This is very unordinary."
break
case 49:
encounter.text = "A mundane chest with a thick padlock is here."
break
case 50:
encounter.text = "A room with no door is here. How do you get in?"
break
case 51:
encounter.text = "A berserker is arguing with his partner. \"No, I attack first and you sneak up on them!\""
encounter.enemies = [
createEnemy("Berserker A", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Berserker B", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1")
]
break
case 52:
encounter.text = "A cult is gathered here. They chant in an unfamiliar tongue. Their chants grow louder and louder as they approach you. Prepare for attack!"
encounter.enemies = [
createEnemy("Cultist A", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1"),
createEnemy("Cultist B", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1"),
createEnemy("Cultist C", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1"),
createEnemy("Cultist D", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1"),
createEnemy("Cultist E", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1")
]
break
case 53:
encounter.text = "Slime covers everything and you can see what has caused it. A gelatinous cube approaches. Its quivering mass looms over you."
encounter.enemies = [
createEnemy("Gelatinous Cube", calculateRoll("8d10+40"), 6, "3d6", "d20-4", "Engulf3d6")
]
break
case 54:
encounter.text = "\"Aye, trodden on my garden have ye'?\" The druid is greatly displeased. You see him reaching for his weapon."
encounter.enemies = [
createEnemy("Druid", calculateRoll("5d8+5"), 11, 4, "1d8", "d20+1", "Produce Flame3d6", "Bark Skin", "Entangle")
]
break
case 55:
encounter.text = "A ghastly apparition is on approach. Each whisper you hear is a promise of pain."
encounter.enemies = [
createEnemy("Ghost", calculateRoll("10d8"), 11, 5, "4d6+3", "d20+1", "Withering Touch4d6+3", "Horrifying Visage", "Possession")
]
break
case 56:
encounter.text = "The griffon guards its nest with fervent aggression. You are in its territory and its plenty pissed."
encounter.enemies = [
createEnemy("Griffon", calculateRoll("7d10+21"), 12, 6, "1d8+4", "d20+2")
]
break
case 57:
encounter.text = "\"Don't touch that!\" You hear a voice scream out. Too late. The mimic has transformed from an inviting treasure chest into a horrific monster. Its tongue has grasped your leg and is pulling you into its teethy maw."
encounter.enemies = [
createEnemy("Mimic", calculateRoll("9d8+18"), 12, 5, "1d8+3", "d20+1", "Grapple")
]
break
case 58:
encounter.text = "The ogre just grunts as it grabs its club. You all know what's coming. It's just a matter of who comes out on top now."
encounter.enemies = [
createEnemy("Ogre", calculateRoll("7d10+21"), 11, 6, "2d8+4", "d20-1")
]
break
case 59:
encounter.text = "Don't let the beauty of the Pegasus entrap you. It is as deadly as it is magnificent. It comes into attack position."
encounter.enemies = [
createEnemy("Pegasus", calculateRoll("7d10+21"), 12, 6, "2d6+4", "d20+2")
]
break
case 60:
encounter.text = "\"Stay back! I can't control it. I can't...\" His voice breaks off as he transforms into a hideous Wererat. Attack!"
encounter.enemies = [
createEnemy("Wererat", calculateRoll("6d8+6"), 12, 4, "1d4+2", "d20+2")
]
break
case 61:
encounter.text = "Beware the Will-o'-Wisp! It has tricked many an adventurer into their deaths and one threatens to do the same here."
encounter.enemies = [
createEnemy("Wisp", calculateRoll("9d4"), 19, 4, "2d8", "d20+9")
]
break
case 62:
encounter.text = "Its form is unreal. The xorn is an almagamation of claws, scales, and fangs. It somehow negotiates its confusion of limbs towards you with incredible speed."
encounter.enemies = [
createEnemy("Xorn", calculateRoll("7d8+42"), 19, 6, "1d6+3", "d20")
]
break
case 63:
encounter.text = "The gaze of the Basilisk is even more deadly than its tooth and claw. This is evidenced by the petrified bodies strewn about the room."
encounter.enemies = [
createEnemy("Basilisk", calculateRoll("8d8+16"), 15, 5, "2d6+3", "d20-1")
]
break
case 64:
encounter.text = "You try not to stare at the bearded devil, but his otherworldly appearance captures your gaze. He clearly has plans for you, and they aren't good."
encounter.enemies = [
createEnemy("Bearded Devil", calculateRoll("8d8+16"), 13, 5, "1d10+3", "d20+2")
]
break
case 65:
encounter.text = "Your sudden arrival has caught the doppelganger off guard. It quickly transforms into a familiar form, but its too late. With its secret revealed, it has no choice but to attack."
encounter.enemies = [
createEnemy("Doppelganger", calculateRoll("8d8+16"), 14, 6, "1d6+4", "d20+4", "Shapechange")
]
break
case 66:
encounter.text = "By some unholy ritual, hell hounds have arrived into the world. They scour the surface for any trace of you. Who sent them?"
encounter.enemies = [
createEnemy("Hell Hound A", calculateRoll("7d8+14"), 15, 5, "1d8+3", "d20+1", "Fire Breath6d6"),
createEnemy("Hell Hound B", calculateRoll("7d8+14"), 15, 5, "1d8+3", "d20+1", "Fire Breath6d6"),
createEnemy("Hell Hound C", calculateRoll("7d8+14"), 15, 5, "1d8+3", "d20+1", "Fire Breath6d6")
]
break
case 67:
encounter.text = "The manticore is an unholy union of a human, lion, and a dragon. It sets its sights on you."
encounter.enemies = [
createEnemy("Manticore", calculateRoll("8d10+24"), 14, 5, "1d8+3", "d20+3")
]
break
case 68:
encounter.text = "A cauldron and the unmistakable stink of cooking flesh. It's a green hag and she has a fixing for her next meal."
encounter.enemies = [
createEnemy("Green Hag", calculateRoll("11d8+33"), 17, 6, "2d8+4", "d20+1", "Minor Illusion", "Invisible Passage")
]
break
case 69:
encounter.text = "The minotaur is here. No maze. Just the promise of your death. Defend yourself!"
encounter.enemies = [
createEnemy("Minotaur", calculateRoll("9d10+27"), 14, 6, "2d12+4", "d20")
]
break
case 70:
encounter.text = "An ancient tomb. In it, a variety of bodies prepared for the afterlife. Meticulous care was put into protecting them from decay. Unfortunately for you, a curse has reanimated them. They only remember their taste for flesh now."
encounter.enemies = [
createEnemy("Mummy", calculateRoll("9d8+18"), 11, 5, "2d6+3", "d20-1")
]
break
case 71:
encounter.text = "A nightmare trots into view. At first, it doesn't seem to be as terrifying as the stories you've heard. But once it draws close, you can see its rotting flesh, the flaming hooves and mane. It's built like a tank and it aims to tear you apart."
encounter.enemies = [
createEnemy("Nightmare", calculateRoll("8d10+24"), 13, 6, "2d8+4", "d20+2", "Ehtereal Stride")
]
break
case 72:
encounter.text = "The fabled owlbear. There is no time to ponder how such a creation can come into being. You can only worry for your own safety as the owlbear is easily provoked."
encounter.enemies = [
createEnemy("Owlbear", calculateRoll("7d10+21"), 13, 7, "1d10+5", "initiative")
]
break
case 73:
encounter.text = "A spectator! Run or fight. Either way, you'll never escape its deadly gaze."
encounter.enemies = [
createEnemy("Spectator", calculateRoll("6d8+12"), 14, 1, "1d6-1", "d20+2", "Confusion Ray", "Paralyzing Ray", "Fear Ray", "Wounding Ray3d10")
]
break
case 74:
encounter.text = "This werewolf is not even trying to hide his transformation. He is proud of his feral proclivities and will bathe in your blood after he drives his clawed hand through your torso. Get ready!"
encounter.enemies = [
createEnemy("Werewolf", calculateRoll("9d8+18"), 12, 4, "18+2", "d20+1")
]
break
case 75:
encounter.text = "Wights are not dangerous because they are some of the most powerful undead creatures. No. It's their intelligence and their endless campaign against the living. Fight!"
encounter.enemies = [
createEnemy("Wight", calculateRoll("6d8+18"), 14, 4, "1d8+2", "d20+1", "Life Drain4d6+3")
]
break
case 76:
encounter.text = "This is not the natural habitat for the Yeti, and yet it is here. You can see your fate drawn in the blood of its other victims. There is no backing out now."
encounter.enemies = [
createEnemy("Yeti", calculateRoll("6d10+18"), 12, 6, "1d6+4", "d20+1")
]
break
case 77:
encounter.text = "Wights are not dangerous because they are some of the most powerful undead creatures. No. It's their intelligence and their endless campaign against the living. Fight!"
encounter.enemies = [
createEnemy("Wight", calculateRoll("6d8+18"), 14, 4, "1d8+2", "d20+1", "Life Drain4d6+3")
]
break
case 78:
encounter.text = "The blood-curdling scream is a clear sign: BANSHEES! Your death is soon to be added to the ledger."
encounter.enemies = [
createEnemy("Banshee A", calculateRoll("13d8"), 12, 4, "3d6+2", "d20+2", "Corrupting Touch3d6+2", "Wail3d6"),
createEnemy("Banshee B", calculateRoll("13d8"), 12, 4, "3d6+2", "d20+2", "Corrupting Touch3d6+2", "Wail3d6")
]
break
case 79:
encounter.text = "This hideous aberration before you must be a Chuul. Kill or be killed. There is no other choice here."
encounter.enemies = [
createEnemy("Chuul", calculateRoll("11d10+33"), 16, 6, "2d6+4", "d20", "Tentacles")
]
break
case 80:
encounter.text = "The Incubus is a shapechanger. It has selected a shape that is pleasing to your eye, but you should know better than to fall for its wiles. Attack!"
encounter.enemies = [
createEnemy("Incubus", calculateRoll("12d8+12"), 15, 5, "1d6+3", "d20+3")
]
break
case 81:
encounter.text = "The ghost is capable of possessing its victims. Be careful because you feel its control gripping at the edges of your mind now."
encounter.enemies = [
createEnemy("Ghost", calculateRoll("10d8"), 11, 5, "4d6+3", "d20+1", "Withering Touch4d6+3", "Horrifying Visage", "Possession")
]
break
case 82:
encounter.text = "Succubus is a shapechanger. It has selected a shape that is pleasing to your eye, but you should know better than to fall for its wiles. Attack!"
encounter.enemies = [
createEnemy("Succubus", calculateRoll("12d8+12"), 15, 6, "1d6+3", "d20+3")
]
break
case 83:
encounter.text = "The wereboar here has mastered control of his beastly transformation. He just doesn't like you. He's trotting at you at full speed."
encounter.enemies = [
createEnemy("Wereboar", calculateRoll("12d8+24"), 10, 5, "2d6+3", "d20", "Charge", "Tusk2d6+3")
]
break
case 84:
encounter.text = "A red dragon wyrmling stalks this area. You were a fool to enter its domain."
encounter.enemies = [
createEnemy("Red Dragon Wyrmling", calculateRoll("10d8+30"), 17, 6, "1d10+4", "d20")
]
break
case 85:
encounter.text = "Woosh! A flameskull whips into view. You feel the heat on your face even at this distance. It's time to attack!"
encounter.enemies = [
createEnemy("Flameskull", calculateRoll("9d4+18"), 13, 5, "3d6", "d20+3", "Fire Ray3d6", "Magic Missile3d10+2", "Fire Ball2d20")
]
break
case 86:
encounter.text = "The pressure in the area suddenly changes. An air elemental materializes in front of you. It must be guarding this place from intruders."
encounter.enemies = [
createEnemy("Air Elemental", calculateRoll("12d10+24"), 15, 8, "2d8+5", "d20+5", "Whirlwind3d8+2")
]
break
case 87:
encounter.text = "The barbed devil is quite deadly. It damages anyone that grapples with it. Beware, for one such creature stalks the land here."
encounter.enemies = [
createEnemy("Barbed Devil", calculateRoll("13d8+52"), 15, 6, "1d6+3", "d20+3", "Hurl Flame3d6")
]
break
case 88:
encounter.text = "You hear a rumbling and the earth beneath you quakes. You dive out of the way as an earth elemental emerges right where you were standing. Fight!"
encounter.enemies = [
createEnemy("Earth Elemental", calculateRoll("12d10+60"), 17, 8, "2d8+5", "d20-1", "Earth Glide")
]
break
case 89:
encounter.text = "Is it getting hot in here or is it just you? No, it's a Fire Elemental and it's ready to attack."
encounter.enemies = [
createEnemy("Fire Elemental", calculateRoll("12d10+36"), 13, 6, "2d6+3", "d20+3", "Fire Form")
]
break
case 90:
encounter.text = "The flesh golem stumbles into your sight. It howls an unearthly cry. Every movement is pain. You would feel pity if you didn't have to fight for you life."
encounter.enemies = [
createEnemy("Flesh Golem", calculateRoll("11d8+44"), 9, 7, "2d8+4", "d20-1", "Berserk")
]
break
case 91:
encounter.text = "The petrifying breath of the gorgon is well known. What isn't known is how one tracked you down here. Prepare to fight!"
encounter.enemies = [
createEnemy("Gorgon", calculateRoll("12d8+48"), 19, 8, "2d12+5", "d20", "Petrifying Breath")
]
break
case 92:
encounter.text = "The hill giant lowers his club with a loud thud. Looking to your direction, he is amused by the new playthings he discovered. Be careful, he plays rough!"
encounter.enemies = [
createEnemy("Hill Giant", calculateRoll("10d12+40"), 13, 8, "3d8+5", "d20-1", "Throw Rock3d10+5")
]
break
case 93:
encounter.text = "You were foolish to enter this place. A night hag has established a den here and she really doesn't like being disturbed. It's time to get into combat formation!"
encounter.enemies = [
createEnemy("Night Hag", calculateRoll("15d8+45"), 17, 7, "2d8+4", "Shape Change", "Nightmare Haunting")
]
break
case 94:
encounter.text = "The appearance of the Salamander spells certain doom for the party. Its heated weapons could cleave through even the hardest armor."
encounter.enemies = [
createEnemy("Salamander", calculateRoll("12d10+24"), 15, 7, "2d6+4", "d20+2", "Heated Body")
]
break
case 95:
encounter.text = "First it was the sucking sounds as it moved across the landscape. Then it was the stench. Finally, the the shambling mound reveals itself."
encounter.enemies = [
createEnemy("Shambling Mound", calculateRoll("16d10+48"), 15, 7, "2d8+4", "d20-1", "Engulf")
]
break
case 96:
encounter.text = "\"No pass! Pay toll! Hur hur.\" The troll grips his giant club effortlessly as if it was a twig. He is beckoning for a fight."
encounter.enemies = [
createEnemy("Troll", calculateRoll("8d10+40"), 15, 7, "1d6+4", "d20+1")
]
break
case 97:
encounter.text = "The Werebear attacks! There seems to be no reasoning with it."
encounter.enemies = [
createEnemy("Werebear", calculateRoll("18d8+54"), 10, 7, "2d10+4", "d20")
]
break
case 98:
encounter.text = "A mound of bone stands tall before you. Just beyond it is a wraith. Its deathly stare cuts right through you."
encounter.enemies = [
createEnemy("Wraith", calculateRoll("9d8+27"), 13, 6, "4d8+3", "d20+3", "Life Drain4d8+3", "Create Specter")
]
break
case 99:
encounter.text = "A vampire spawn steps out of the shadows. Perhaps he's here to capture new slaves for its master."
encounter.enemies = [
createEnemy("Vampire Spawn", calculateRoll("11d8+33"), 15, 6, "2d4+3", "d20+3", "Bite3d6+3")
]
break
}
break
case "hard":
if (encounter.cr == null) encounter.cr = 9
multiplier = 1 + (encounter.cr - 9) / 10
switch (getRandomInteger(0, 60)) {
case 0:
encounter.text = "\"What is the meaning of life?\" The stone demands to know the answer. It seems very serious about this."
break
case 1:
encounter.text = "\"Kill.\" The thought races through your head. \"Kill!\" It's searing an image you murdering your allies. You must stop these dark urges at once. You are losing yourself. At any moment you're going to pop off...\"Kill!\""
break
case 2:
encounter.text = "A pressure in character's chest is building. A cracking sound of some kind. \"Oh god no!\""
break
case 3:
encounter.text = "The temperature in the local area is rising dramatically. Something must be done immediately!"
break
case 4:
encounter.text = "You are caught in a dream. In this dream there is no escape. You must have triggered some magical safeguard and you need to find some way to escape the prison of your mind."
break
case 5:
encounter.text = "You encounter a giant chest overflowing with coins and valuables."
break
case 6:
encounter.text = "Through your mind's eye, you can see the sky. But it's not right. The stars are not of any constellation you can recognize. Indeed this is the night sky of some other world. But why are seeing these images?"
break
case 7:
encounter.text = "Columns as far as the eye can see. What are they supposed to mean? There is no time to think about that as you realize that one is falling over, causing a chain reaction. Run!"
break
case 8:
encounter.text = "An arsenal of anointed weapons are here. It seems to have been delivered by some divine intervention. You must have caught the favor of some god on this day."
break
case 9:
encounter.text = "A ghostly butcher appears. He seems to have no ill-intent.\"Would you like a taste of my *special* meats?\""
break
case 10:
encounter.text = "A devil steps through a portal and beckons toward you. It announces that it does not wish to fight. Instead, it has come to bargain. A contract is gripped tightly in its hand."
break
case 11:
encounter.text = "A grave with an elaborate headstone and sarcophagus. You recognize the poetry of its words."
break
case 12:
encounter.text = "The skull of vampire resides on a pedestal. What secrets does it hold?"
break
case 14:
encounter.text = "A contraption unlike anything you've seen before is here. It appears to have chambers of liquids and gases along its surface."
break
case 15:
encounter.text = "A giant mirror has been hoisted on a wall here. You start to hear an eerily familiar voice emanate from its image."
break
case 16:
encounter.text = "A sudden rush of ennui washes over you."
break
case 17:
encounter.text = "The floor is sinking beneath you! A hole appears before you and you do not want to know where it leads."
break
case 18:
encounter.text = "A field of flowers. So many that you can't see the ground beneath them."
break
case 19:
encounter.text = "A magical portal is here. Through it, you can see an endless library. All of the world's knowledge and then some could be captured in those tomes..."
break
case 20:
encounter.text = "A curious book bound in the flesh of a man seems to have been discarded here. It starts quivering all of a sudden..."
break
case 21:
encounter.text = "Gnomes have set up a taxation booth here. In order to pass, you must pay an outrageous price. \"NEXT!\""
break
case 22:
encounter.text = "There is a supernatural rain here. But it's not just any rain. \"ACID!\""
break
case 23:
encounter.text = "Remarkable frescoes and portraits line the ruined walls."
break
case 24:
encounter.text = "A demonic artist resides here. Care for a portrait?"
break
case 25:
encounter.text = "A friendly Stone Golem greets you. It cannot speak, but it's trying to communicate something to you."
break
case 26:
encounter.text = "An intricate puzzle is presented to you."
break
case 27:
encounter.text = "A great history is etched in the stone here. Some of the words are emboldened, possibly indicating a pattern."
break
case 28:
encounter.text = "An automoton walks the perimeter of this place. It is powered by some unknown force. Its path is etched deeply into the ground. Who knows how long it has been doing this."
break
case 29:
encounter.text = "A simple shrine is here. A chalice filled with blood is placed in front of it."
break
case 30:
encounter.text = "A person is strung up on the wall. There are signs of life, but they are fading fast!"
break
case 31:
encounter.text = "A chimera. Will you accept grim fate now that you have come across such a foe?"
encounter.enemies = [
createEnemy("Chimera", calculateRoll("12d10+48"), 14, 7, "2d6+4", "d20", "Fire Breath7d8")
]
break
case 32:
encounter.text = "The cyclops eyes you closely. It ponders for a time, but then finally decides that you must die."
encounter.enemies = [
createEnemy("Cyclops", calculateRoll("12d12+60"), 14, 9, "3d8+6", "d20")
]
break
case 33:
encounter.text = "You've walked right into the drider's web. This was her plan all along."
encounter.enemies = [
createEnemy("Drider", calculateRoll("13d10+52"), 19, 6, "3d8", "1d10+3", "Poison Bite2d8")
]
break
case 34:
encounter.text = "A group of cultists are gathering here. It appears that they are trying to open a portal through to the hells. They must be stopped!"
encounter.enemies = [
createEnemy("Insane Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Cone of Cold8d8", "Greater Invisibility", "Fireball8d6", "Shield"),
createEnemy("Cultist A", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1"),
createEnemy("Cultist B", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1")
]
break
case 35:
encounter.text = "The statues of men in terror, all cowering from some horrible sight. This can only mean one thing: Medusa."
encounter.enemies = [
createEnemy("Medusa", calculateRoll("17d8+51"), 15, 5, "1d6+2", "d20+2", "Petrifying Gaze", "Snake Hair5d6")
]
break
case 36:
encounter.text = "Dragon fight! A young brass dragon descends upon you."
encounter.enemies = [
createEnemy("Young Brass Dragon", calculateRoll("13d10+39"), 17, 7, "2d10+4", "d20", "Fire Breath12d6", "Sleep Breath")
]
break
case 37:
encounter.text = "The young white dragon has sized you up. It considers you a worth adversary"
encounter.enemies = [
createEnemy("Young White Dragon", calculateRoll("14d10+56"), 17, 7, "2d10+4", "d20", "Cold Breath10d8", "Ice Walk")
]
break
case 38:
encounter.text = "An orc death squad!"
encounter.enemies = [
createEnemy("Orc A", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Orc B", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Orc C", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Orc D", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Orc E", calculateRoll("2d8+6"), 13, 5, "1d12+3", "d20+1"),
createEnemy("Orc Leader", calculateRoll("5d8+6"), 13, 5, "3d12+3", "d20+1", "Rally Cry")
]
break
case 39:
encounter.text = "Drow raiders are in full force here!"
encounter.enemies = [
createEnemy("Drow Raider A", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2"),
createEnemy("Drow Raider B", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2"),
createEnemy("Drow Raider C", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2"),
createEnemy("Drow Raider D", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2"),
createEnemy("Drow Raider E", calculateRoll("3d8"), 15, 4, "1d6+2", "d20+2")
]
break
case 40:
encounter.text = "The stone giant has been building pillars of rocks and then watches them fall over. He's kinda pissed that you've interrupted his game."
encounter.enemies = [
createEnemy("Stone Giant", calculateRoll("11d12+55"), 17, 9, "3d8+6", "d20+2", "Throw Rock4d10+6")
]
break
case 41:
encounter.text = "The shield guardian is blocking path. Some mage from eons ago must have absent mindedly placed this here and forgot about it."
encounter.enemies = [
createEnemy("Shield Guardian", calculateRoll("15d10+60"), 17, 7, "4d6+4", "d20-1", "Shield")
]
break
case 42:
encounter.text = "You have caught the focus of a young black dragon. It's positioning itself for attack!"
encounter.enemies = [
createEnemy("Young Black Dragon", calculateRoll("15d10+45"), 18, 7, "4d6+8", "d20+2", "Acid Breath11d8")
]
break
case 43:
encounter.text = "You suddenly fall into a pit! This is no ordinary trap, for a Young Copper Dragon has been enslaved here."
encounter.enemies = [
createEnemy("Young Copper Dragon", calculateRoll("14d10+42"), 17, 7, "4d6+8", "d20+1", "Acid Breath9d8", "Slowing Breath")
]
break
case 44:
encounter.text = "The assassin finally reveals himself. \"You have no idea how long I have been waiting for this moment!\""
encounter.enemies = [
createEnemy("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3")
]
break
case 45:
encounter.text = "You feel an unexpected cold wind. It can only be one thing: the frost giant approaches!"
encounter.enemies = [
createEnemy("Frost Giant", calculateRoll("12d12+60"), 15, 9, "6d12+12", "d20-1")
]
break
case 46:
encounter.text = "\"How do you actually kill a hydra?\" No one seems to know the answer, but here you are facing one anyway. Tough luck!"
encounter.enemies = [
createEnemy("Hydra", calculateRoll("15d12+75"), 15, 8, "3d10+15", "d20+1")
]
break
case 47:
encounter.text = "You've had no qualms killing its snake bretheren before. Perhaps that is why the Spirit Naga holds such malice towards you. Prepare for the fight of your life!"
encounter.enemies = [
createEnemy("Spirit Naga", calculateRoll("10d10+20"), 15, 7, "8d8+4", "d20+3", "Dominate Person", "Lightning Bolt9d6")
]
break
case 48:
encounter.text = "The hairs on your arm raise as if you've entered a static field. Yes, it's a young bronze dragon preparing to strike. Dodge out of the way if you can!"
encounter.enemies = [
createEnemy("Young Bronze Dragon", calculateRoll("15d10+60"), 18, 8, "4d6+10", "d20+1", "Lightning Breath10d10", "Repulsion Breath")
]
break
case 49:
encounter.text = "There was once a great forest here. It has all been destroyed by the ambition of man. All that is left is the vengeance of the young green dragon that stands in your way."
encounter.enemies = [
createEnemy("Young Green Dragon", calculateRoll("16d10+48"), 18, 7, "4d6+8", "d20+1", "Poison Breath12d6")
]
break
case 50:
encounter.text = "My god, that must be why they call them cloud giants. It's as if they can reach the clouds. Defend yourself!"
encounter.enemies = [
createEnemy("Cloud Giant", calculateRoll("16d12+96"), 14, 12, "6d8+16", "d20", "Throw Rock4d10+8", "Control Weather")
]
break
case 51:
encounter.text = "A treant has stationed itself here. It's the protector of the natural world. You upset the balance."
encounter.enemies = [
createEnemy("Treant", calculateRoll("12d12+60"), 16, 10, "6d6+12", "d20-1"),
createEnemy("Awakened Tree", calculateRoll("7d12+14"), 13, 10, "3d6+4", "d20-2")
]
break
case 52:
encounter.text = "A young blue dragon. What else can be said? You know you're in trouble now."
encounter.enemies = [
createEnemy("Young Blue Dragon", calculateRoll("16d10+64"), 18, 9, "12d6+10", "d20", "Lightning Breath10d10")
]
break
case 53:
encounter.text = "A young silver dragon. It's over for you. Make your peace."
encounter.enemies = [
createEnemy("Young Silver Dragon", calculateRoll("16d10+8-"), 18, 10, "4d6+12", "d20", "Cold Breath12d8", "Paralyzing Breath")
]
break
case 54:
encounter.text = "This is the lair of the Aboleth. You say your prayers to any god that would listen. It's time to prove your worth."
encounter.enemies = [
createEnemy("Aboleth", calculateRoll("18d10"), 17, 9, "6d6+15", "d20-1", "Enslave", "Psychic Drain3d6")
]
break
case 55:
encounter.text = "A deva is here. There must have been some mistake, but you are marked for death."
encounter.enemies = [
createEnemy("Deva", calculateRoll("16d8+64"), 17, 8, "2d6+8", "d20+4")
]
break
case 56:
encounter.text = "The stone golem is against the natural order. It does not fill any niche in the animal kingdom. It must be destroyed."
encounter.enemies = [
createEnemy("Stone Golem", calculateRoll("17d10+85"), 17, 10, "6d8+12", "d20-1")
]
break
case 57:
encounter.text = "You've gotten on the wrong side of the young gold dragon. Perhaps you should not have trespassed on its sovereign land."
encounter.enemies = [
createEnemy("Young Gold Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20+2", "Fire Breath10d10", "Weakening Breath")
]
break
case 58:
encounter.text = "The young red dragon wastes no time in its pursuit of victims. It has selected you for its next meal."
encounter.enemies = [
createEnemy("Young Red Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20", "Fire Breath16d6")
]
break
case 59:
encounter.text = "The guardian naga is against the natural order. It does not fill any niche in the animal kingdom. It must be destroyed."
encounter.enemies = [
createEnemy("Guardian Naga", calculateRoll("15d10+45"), 18, 8, "1d8+4", "d20+4", "Spit Poison10d8", "Flame Strike4d6", "Bestow Curse", "Hold Person", "Geas")
]
break
case 60:
encounter.text = "The fire giants were born in environments that would burn others to a crisp instantly. This means this one is immune to fire damage. It's ready to fight now."
encounter.enemies = [
createEnemy("Fire Giant", calculateRoll("13d12+78"), 18, 11, "6d6+7", "d20-1", "Throw Rock4d10+7")
]
break
}
break
case "boss":
if (encounter.cr == null) encounter.cr = 13
multiplier = 1 + (encounter.cr - 13) / 10
switch (getRandomInteger(0, 60)) {
case 0:
encounter.text = "The earth is opening up around you! A giant crack in the ground bursts wide and magma spews forth."
break
case 1:
encounter.text = "A statue with glowing blue eyes watches over you. Its intensity is ever increasing until it final begins to shoots beams of electrical energy at you."
break
case 2:
encounter.text = "You all ate bad mushrooms. This is terrible news because you're starting to have a bad trip. This is when you notice that you are not alone."
break
case 3:
encounter.text = "A massive rock slide! Watch out!! You only have moments to react."
break
case 4:
encounter.text = "You can't move! Your muscles are paralyzed. You can only move your eyes which dart around feverishly looking for the cause of this malady."
break
case 5:
encounter.text = "A series of blocks are laid out before you in a shallow, rectangular pit. A voice is heard in your head \"Solve it or die!\""
break
case 6:
encounter.text = "Suddenly, you are lifted high up into the air! An area of land extending around you by about 5 feet shoots up skyward. You notice other clumps of earth being levitated by the same magical force."
break
case 7:
encounter.text = "Your leg is caught on a wire. Viewing its path around your leg and across the ground, you see that it is connected to a contraption of some kind. It has the semblance of a bomb. It is very large and would spell certain doom for the party, if not the entire local area, if it were to explode."
break
case 8:
encounter.text = "The banner of a great king marks a field pockmarked with craters. Mines!"
break
case 9:
encounter.text = "Large rings descend around you. As you try to escape them, you relize that an unseeable forcefield prevents you from escaping. You're trapped!"
break
case 10:
encounter.text = "A satyr stands before you and offers you a treat. You see an array of pots and pans and the likeness of an unkempt kitchen. Something alludes you about this situation. Something sinister."
break
case 11:
encounter.text = "A ruined statue dedicated to the goddess of magic, Mystra, is here. It is split in two with the torso laying haphazardly to the side. You sense a faint voice coming from within it."
break
case 12:
encounter.text = "A shrine dedicated to Shar, goddess of darkness, stands tall before you. A challenge is posted on a brass placard attached to the podium."
break
case 14:
encounter.text = "An offering plate named for Waukeen, the goddess of trade, is placed on an ornate, golden pedestal. It seems to beckon for you to make a trade."
break
case 15:
encounter.text = "You close your eyes only to find yourself transported to a cavern lined with skulls and mounds of the dead. In it: a leatherbound tome with a symbol of the Spider Queen, Lolth."
break
case 16:
encounter.text = "A crown made of bone and sinew rests on the skull of a dead king. You have the uncontrollable urge to place it upon your head."
break
case 17:
encounter.text = "A cosmic display of lights and magic play before you. A great power is being held here, trapped by the gravity of some celestial object."
break
case 18:
encounter.text = "You finally see it. A dark figure at your side. It seems like it has always been there, and yet you never noticed it. It poses a question to you: more like a riddle than anything else. You are compelled to answer or face existential consequences."
break
case 19:
encounter.text = "A sigil of a great house is emblazoned into a wall ahead of you. Underneath, it commands, \"Name your champion\" The walls begin to shudder violently."
break
case 20:
encounter.text = "A group of corpses are gathered in the center here. Their appearance shares an uncanny resemblance to your own. Never minding that, you notice that they have incredible weapons and artifacts amongst their bodies."
break
case 21:
encounter.text = "You see what could only be the giant tooth of a dragon. You hesitate from touching it, knowing that something as valuable as this wouldn't be so carelessly left behind in this manner."
break
case 22:
encounter.text = "Lord Gond smiles upon you. Choose a boon for a single item in your inventory."
break
case 23:
encounter.text = "You have displeased the god Bhaal, the god of murder. He demands a sacrifice as recompense."
break
case 24:
encounter.text = "A blank scroll, unblemished by its surroundings, is drawn open before you. An ink pen floats to its side. Will you add your name to it?"
break
case 25:
encounter.text = "Your feet are sinking into the floor. The false surface must have been an elaborate illusion. You must act quickly before you are completely engulfed by what lies below."
break
case 26:
encounter.text = "Chief among your concerns is the taste of the air which has suddenly turned sour. Poison gas!"
break
case 27:
encounter.text = "You can sense that this place has some greater importance. Magical leylines meet here. In this place of power, anything is possible."
break
case 28:
encounter.text = "A mummified monkey's paw resides here. Three fingers are extended outward. According to lore, such a powerful item can grant you three wishes. However, you should be wary of what you wish for..."
break
case 29:
encounter.text = "A small bell attached to a simple, wooden handle can be found here. What does it do? Or should you be afraid of what it may summon?"
break
case 30:
encounter.text = "A pristine wand is held in a glass case. A blue aura surrounds it. It is clear that it is protected by practical and magical means, but why?"
break
case 31:
encounter.text = "An adult black dragon approaches. You are not ready for this."
encounter.enemies = [
createEnemy("Adult Black Dragon", calculateRoll("17d12+85"), 19, 11, "6d6+18", "d20+2", "Acid Breath12d8", "Frightful Presence", "Wing Attack2d6+6")
]
break
case 32:
encounter.text = "An adult brass dragon is here. It's pissed!"
encounter.enemies = [
createEnemy("Adult Brass Dragon", calculateRoll("15d12+75"), 18, 11, "6d6+18", "d20", "Fire Breath13d6", "Sleep Breath", "Frightful Presence", "Wing Attack2d6+6")
]
break
case 33:
encounter.text = "An adult bronze dragon is charging up for an attack!"
encounter.enemies = [
createEnemy("Adult Bronze Dragon", calculateRoll("17d12+102"), 19, 12, "6d6+21", "d20", "Repulsion Breath", "Lightning Breath12d10", "Wing Attack2d6+6")
]
break
case 34:
encounter.text = "An adult copper dragon shifts its gaze at you. You are doomed."
encounter.enemies = [
createEnemy("Adult Copper Dragon", calculateRoll("16d12+80"), 18, 11, "6d6+18", "d20+1", "Acid Breath12d8", "Slowing Breath", "Wing Attack2d6+6")
]
break
case 35:
encounter.text = "An adult green dragon is nesting here. You really shouldn't have disturbed it."
encounter.enemies = [
createEnemy("Adult Green Dragon", calculateRoll("18d12+90"), 19, 11, "6d6+18", "d20+1", "Poison Breath16d6")
]
break
case 36:
encounter.text = "An adult white dragon is here. The wrong place and wrong time, unfortunately for you."
encounter.enemies = [
createEnemy("Adult White Dragon", calculateRoll("16d12+96"), 18, 11, "6d6+18", "d20", "Cold Breath12d8", "Wing Attack2d6+6")
]
break
case 37:
encounter.text = "The arch mage descends from the high altar. He says no words, but raises his hands as if he is going to prepare a spell. Get ready!"
encounter.enemies = [
createEnemy("Arch Mage", calculateRoll("18d8+18"), 12, 4, "1d4+2", "d20+14", "Time Stop", "Globe of Invulnerability", "Lightning Bolt8d6", "Banishment", "Cone of Cold8d8", "Teleport"),
createEnemy("Disciple A", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Ice Storm4d6+8", "Fireball8d6", "Mage Armor", "Fire Bolt1d10"),
createEnemy("Disciple B", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Ice Storm4d6+8", "Fireball8d6", "Mage Armor", "Fire Bolt1d10")
]
break
case 38:
encounter.text = "The djinni mocks you as you enter its domain. It seems like it wants to pick a fight with you."
encounter.enemies = [
createEnemy("Djinni", calculateRoll("14d10+84"), 17, 9, "2d6+8", "d20+2")
]
break
case 39:
encounter.text = "They are as beautiful as they are wicked. The Erinyes approach with cruel intentions. Their winged visages come into clear view."
encounter.enemies = [
createEnemy("Erinyes A", calculateRoll("18d8+72"), 18, 8, "1d10+4", "d20+3"),
createEnemy("Erinyes B", calculateRoll("18d8+72"), 18, 8, "1d10+4", "d20+3"),
createEnemy("Erinyes C", calculateRoll("18d8+72"), 18, 8, "1d10+4", "d20+3")
]
break
case 40:
encounter.text = "The horned devil stalks the land before you. It grins, revealing a hideous set of teeth."
encounter.enemies = [
createEnemy("Horned Devil", calculateRoll("17d10+85"), 18, 10, "6d8+18", "d20+3", "Hurl Flame4d6")
]
break
case 41:
encounter.text = "The temperature of the local area has cooled significantly. You see it now: an ice devil makes its presence known."
encounter.enemies = [
createEnemy("Ice Devil", calculateRoll("19d10+76"), 18, 10, "6d4+15", "d20+2", "Wall of Ice")
]
break
case 42:
encounter.text = "The mummy lord resides here. It has summoned its followers and directs the assault with his decayed finger pointed at you."
encounter.enemies = [
createEnemy("Mummy Lord", calculateRoll("13d8+39"), 17, 9, "3d6+4", "d20", "Hold Person", "Silence", "Harm14d6", "Blinding Dust", "Whirlwind of Sand"),
createEnemy("Mummy A", calculateRoll("9d8+18"), 11, 5, "2d6+3", "d20-1"),
createEnemy("Mummy B", calculateRoll("9d8+18"), 11, 5, "2d6+3", "d20-1"),
createEnemy("Mummy C", calculateRoll("9d8+18"), 11, 5, "2d6+3", "d20-1")
]
break
case 43:
encounter.text = "The signs are clear: mounds of disturbed earth, pools of slime, and the digested remains of those foolish to face the creature. Yes, it's the Purple Worm. The earth rumbles, announcing its entry into the fray."
encounter.enemies = [
createEnemy("Purple Worm", calculateRoll("15d20+90"), 18, 14, "6d6+18", "d20-2", "Tail Stinger12d6+19")
]
break
case 44:
encounter.text = "The only way to describe it is that it's a twisted combination of a dragon and a giant millipede. The Remorhaz makes a sickening path through the debris toward you."
encounter.enemies = [
createEnemy("Remorhaz", calculateRoll("17d12+85"), 17, 11, "6d10+7", "d20+1", "Swallow6d6")
]
break
case 45:
encounter.text = "The storm giant pays little heed toward you. Yet, you are in its path. Hearing the sudden crack of lightning jolts you."
encounter.enemies = [
createEnemy("Storm Giant", calculateRoll("20d12+100"), 16, 14, "12d6+18", "d20+2", "Control Weather", "Lightning Strike12d8")
]
break
case 46:
encounter.text = "You have entered the realm of a powerful vampire. Its coven is poised to strike!"
encounter.enemies = [
createEnemy("Vampire", calculateRoll("17d8+68"), 16, 9, "3d8+8", "d20+4", "Charm", "Shape Change"),
createEnemy("Vampire Spawn", calculateRoll("11d8+33"), 15, 6, "2d4+3", "d20+3", "Bite3d6+3"),
createEnemy("Vampire Spawn", calculateRoll("11d8+33"), 15, 6, "2d4+3", "d20+3", "Bite3d6+3")
]
break
case 47:
encounter.text = "The hulking mass of the Behir enters the scene. It rears up revealing its many clawed feet. The tail whips around haphazardly throwing rubble around like they were pebbles."
encounter.enemies = [
createEnemy("Behir", calculateRoll("16d12+64"), 17, 10, "5d10+12", "d20+3", "Lightning Breath12d10", "Swallow6d6", "Constrict2d10+6")
]
break
case 48:
encounter.text = "The efreeti are the genies of the elemental fire plane. One such warrior is here and is bent on exacting revenge on some forgotten slight."
encounter.enemies = [
createEnemy("Efreeti", calculateRoll("16d10+112"), 17, 10, "4d6+12", "d20+1", "Hurl Flame5d6")
]
break
case 49:
encounter.text = "The nalfeshnee are winged demons that are like a cross between an ape and a boar. This one angles its terrible snout you and makes a menacing grunt."
encounter.enemies = [
createEnemy("Nalfeshnee", calculateRoll("16d10+96"), 18, 10, "8d6+5", "d20", "Horror Nimbus")
]
break
case 50:
encounter.text = "The roc attacks! This gargantuan bird swoops in and engulfs the combat area with its awesome wings."
encounter.enemies = [
createEnemy("Roc", calculateRoll("16d20+80"), 15, 13, "8d8+18", "d20")
]
break
case 51:
encounter.text = "It wasn't your imagination. It moved. You swear it. Indeed, the animated statue is poised for attack!"
encounter.enemies = [
createEnemy("Animated Statue", calculateRoll("10d12+20"), 17, 7, "2d10+4", "d20-2")
]
break
case 52:
encounter.text = "You've never seen the undead quite like this. The bone claw raises its outrageous talons. It is going to strike at any moment!"
encounter.enemies = [
createEnemy("Boneclaw", calculateRoll("17d10+34"), 16, 8, "6d10+8", "d20+3", "Shadow Jump5d12+2", "Deadly Reach")
]
break
case 53:
encounter.text = "A werewolf is already a formidable opponent. The deathwolf is the ungodly undead version of that. It bears down on you with great ill intent."
encounter.enemies = [
createEnemy("Deathwolf", calculateRoll("18d8+72"), 15, 10, "6d8+15", "d20+3", "Phantom Deathwolf6d6")
]
break
case 54:
encounter.text = "A drow inquisitor is here. She expected you to come this way. You're going to pay for that mistake."
encounter.enemies = [
createEnemy("Drow Inquisitor", calculateRoll("23d8+46"), 16, 10, "12d8+24", "d20+2", "Spectral Dagger1d8+5")
]
break
case 55:
encounter.text = "This is proof that fate has it in for you. You somehow have stumbled upon the chamber of an Elder Brain. It rises out of its brine pool to summon its minions."
encounter.enemies = [
createEnemy("Elder Brain", calculateRoll("20d10+100"), 10, 7, "5d8+7", "d20", "Mind Blast5d10+5")
]
break
case 56:
encounter.text = "You see the Jabberwock. It's a horrific creature, born of pure hatred and evil. It has the wings of a dragon, but crawls around on four legs like a bastard insect."
encounter.enemies = [
createEnemy("Jabberwock", calculateRoll("10d12+50"), 18, 10, "6d10+10", "d20+1", "Regenderation")
]
break
case 57:
encounter.text = "Acid drips onto the floor from its nasty maw. The massive megapede marks its territory with the bodies of those foolish enough to challenge it. You may count yourself among them in short order."
encounter.enemies = [
createEnemy("Megapede", calculateRoll("13d20+39"), 15, 10, "6d10+12", "d20", "LifeDrain3d10", "Psychic Bomb5d8")
]
break
case 58:
encounter.text = "The skull lord holds dominion over this lair. It turns its three heads for you to plainly see its horrific visage."
encounter.enemies = [
createEnemy("Skull Lord", calculateRoll("15d8+45"), 18, 8, "24d6", "d20+3", "Deathly Ray5d8+5")
]
break
case 59:
encounter.text = "The zikran has the blood of genies and their power too. It harnesses the power of water. Pools of which gather at its feet. It's ready to defend itself."
encounter.enemies = [
createEnemy("Zikran", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Cone of Cold 8d8", "Lightning Bolt 8d6")
]
break
case 60:
encounter.text = "You have never seen a monstrosity of this magnitude. The eight legs of the spider dragon crash into the ground one after the other like a symphony of massive hammers. This is the fight of your life."
encounter.enemies = [
createEnemy("Spider Dragon", calculateRoll("15d10+5"), 23, 9, "3d12+12", "d20+8", "Silk Spit", "Spider Breath7d10")
]
break
}
break
case "god":
if (encounter.cr == null) encounter.cr = 17
multiplier = 1 + (encounter.cr - 17) / 10
switch (getRandomInteger(0, 60)) {
case 0:
encounter.text = "You have discovered a tunnel encased entirely with geodes and crystals of great value!"
break
case 1:
encounter.text = "A mighty steed is spotted here. As it breathes, it exhales flames from its nostrils. Despite this, it looks friendly and comes up to you with a slightly tilted head. It presses against your arm cautiously."
break
case 2:
encounter.text = "A thought enters your mind. A thought that no man should know. A thought that could change the world. And yet, it starts to escape you as fast as it came to you."
break
case 3:
encounter.text = "The grand master of the martial way stands before you. He will teach you one technique, but it will cost you dearly. A cost that is worth much more than mere coin."
break
case 4:
encounter.text = "An exceptional weapon is presented to you by one claiming to be your loyal servant. It lowers its eyes as you approach. \"Master, I do not aim to offend with such a paltry gift, but it is all I have.\""
break
case 5:
encounter.text = "A portal is spotted. Through it, you see a vast library with shelves and shelves of books with no end. It is a truly spectacular sight."
break
case 6:
encounter.text = "The way ahead is covered with fine sand. It shifts and twists, indicating that something resides underneath."
break
case 7:
encounter.text = "Molten hot magma leaks through the walls of this place. You hear a sudden cracking and all hell breaks loose."
break
case 8:
encounter.text = "Whatever is entombed here must be significantly important because there are various traps of unusual complexity laid through the path before you."
break
case 9:
encounter.text = "Giant axes swing like pendulums. Each blocks your way and can destroy you in a single blow."
break
case 10:
encounter.text = "A giant rock face is in front of you. It must be traversed in order for you to proceed. Unfortunately, the hand holds are incredibly unstable and will not hold your weight for long."
break
case 11:
encounter.text = "A bridge in disrepair is before you. It was sabotaged to prevent passage through this way, but you must get through. There planks placed intermitentally across its length. You imagine that it could be crossed successfully with some focus and a lot of luck."
break
case 12:
encounter.text = "\"You must choose wisely.\" An old sage presents two cups on a table before you. Their mouths are faced downward, hiding their contents. You can tell something is wrong from his snickering, mocking lips."
break
case 14:
encounter.text = "An elaborate illusion is here, hiding the way forward. Pressing ahead cautiously with your foot, you can tell that there is a significant drop where the illusion is. Safe passage is obscured from your senses."
break
case 15:
encounter.text = "A set of giant scales are ahead. Ball bearings of immense size are all placed on one side of the scale. They are too heavy to move by any normal person's strength. Suddenly, the walls start pushing in."
break
case 16:
encounter.text = "Death is playing with a set of dice. He looks bored. Then he suddenly notices you..."
break
case 17:
encounter.text = "A tablet is here. Written in large text, it seems to be the words to a curse or spell of some sort. At first glance, it makes no sense to you. Then you realize that each line is written in a differnt language found in the known world. That is when you started to smell the gas..."
break
case 18:
encounter.text = "It's starting to rain, but you realize that this is no normal rain. Each drop sizzles as it strikes an object. Acid! It is truly the end times."
break
case 19:
encounter.text = "The flesh wall demands a sacrifice! It quivers at you. \"Feeeeeeeed meeeeeeeeee\""
break
case 20:
encounter.text = "A giant hedge maze! You do not wish to participate, however you notice a mystical blue flame follow your path. It does not move swiftly, but it does block the way back. You have no choice but to play this game."
break
case 21:
encounter.text = "Bottles of an illuminated, golden liquid are strung up in something like a wire lattice. Upon further investigation, you can see that they are all interconnected and may fall easily if disturbed. It's too bad because this is the only way out."
break
case 22:
encounter.text = "You keep walking forward, but the door ahead seems to remain ever distant. This goes on for awhile, so you assume something magical is at play here."
break
case 23:
encounter.text = "\"Stop the rabbits! They're getting away!\" You hear somebody yell, but it's too late. Your party is surrounded by rabbits just hopping around aimlessly. Humorous at first, but it becomes dangerous as the number of rabbits increase to an unforseen number."
break
case 24:
encounter.text = "All your actions are being judged by a mysterious figure sitting atop a dark throne. Only the sounds of disappointment can be heard as the shady figure flips through the pages of your exploits."
break
case 25:
encounter.text = "You see a mirror, however its image is not a reflection of you but that of your past and possible futures."
break
case 26:
encounter.text = "The sphinx asks you the unsolvable riddle. Your life hangs in the balance as you try to interpret its words."
break
case 27:
encounter.text = "Your vision begins to blacken. All noises are muted. Your senses are ripped from you. You are completely cut off from the world. You are now trapped in the void."
break
case 28:
encounter.text = "A pit seemingly with no bottom can be seen here. You look back: the entrance to this pace is gone! What will you do?"
break
case 29:
encounter.text = "A test of faith. Prove your worth or be struck down by the power of the gods!"
break
case 30:
encounter.text = "A sea of bones. You try to step in, but your feet have no purchase."
break
case 31:
encounter.text = "The adult blue dragon claws at the ground just before it. It's bored. It has decided that you will be its new play thing."
encounter.enemies = [
createEnemy("Adult Blue Dragon", calculateRoll("18d12+108"), 19, 12, "6d10+21", "d20", "Lightning Breath12d10", "Wing Attack2d6+7")
]
break
case 32:
encounter.text = "The gold dragon has deemed you unworthy. Prove it wrong."
encounter.enemies = [
createEnemy("Adult Gold Dragon", calculateRoll("19d12+133"), 19, 12, "6d10+21", "d20+2", "Fire Breath 12d10", "Weakening Breath")
]
break
case 33:
encounter.text = "The adult red dragon has broken its chains and now stands before you. Whatever events have ocurred to bring it to this place, it places the blame on you for its centuries of torture."
encounter.enemies = [
createEnemy("Adult Red Dragon", calculateRoll("19d12+133"), 19, 12, "6d10+21", "d20", "Fire Breath18d6", "Wing Attack2d6+8")
]
break
case 34:
encounter.text = "You didn't do anything wrong. The adult silver dragon just doesn't like you."
encounter.enemies = [
createEnemy("Adult Silver Dragon", calculateRoll("18d12+126"), 19, 13, "6d10+24", "d20", "Cold Breath13d8", "Paralyzing Breath")
]
break
case 35:
encounter.text = "The ancient black dragon goads you into a fight."
encounter.enemies = [
createEnemy("Ancient Black Dragon", calculateRoll("21d20+147"), 22, 15, "6d10+24", "d20+2", "Acid Breath15d8", "Wing Attack2d6+8")
]
break
case 36:
encounter.text = "From the ashes, the phoenix!"
encounter.enemies = [
createEnemy("Phoenix", calculateRoll("10d20+70"), 18, 13, "4d6+16", "d20+8", "Fiery Talons4d8+16", "Swoop4d8+16")
]
break
case 37:
encounter.text = "The demogorgon is quite the curiosity. Your wish to study its biology is stymied by the fact that your life is in jeapodary."
encounter.enemies = [
createEnemy("Demogorgon", calculateRoll("32d12+256"), 22, 17, "6d12+18", "d20+2", "Beguiling Gaze", "Hypnotic Gaze")
]
break
case 38:
encounter.text = "You stand there, mouth agape trying to understand it. You can't. The cosmic horror attacks!"
encounter.enemies = [
createEnemy("Cosmic Horror", calculateRoll("16d20+112"), 15, 14, "6d6+16", "d20", "Poison Jet4d6", "Psychic Whispers6d10")
]
break
case 39:
encounter.text = "The ancient red dragon has lived to see entire civilizations rise and fall. You are nothing in its presence."
encounter.enemies = [
createEnemy("Ancient Red Dragon", calculateRoll("21d20+147"), 22, 15, "6d8+30", "d20+2", "Fire Breath26d6", "Wing Attack2d6+10")
]
break
case 40:
encounter.text = "The ancient gold dragon holds on to a dark secret that jeopordizes everything that you know. Perhaps it will impart its knowledge onto you once you prove you're worthy. Many have tried and failed as evidenced by the bones laid asunder."
encounter.enemies = [
createEnemy("Ancient Gold Dragon", calculateRoll("28d20+252"), 22, 15, "6d10+24", "d20+2", "Fire Breath13d10", "Weakening Breath")
]
break
case 41:
encounter.text = "Zariel, the arch duchess of Avernus, stands at the ready. You have displeased her and now she will exact her revenge on you personally. Burning crown above her head and wings red like fire, she is ready for you."
encounter.enemies = [
createEnemy("Zariel", calculateRoll("40d10+360"), 21, 16, "4d8+16", "d20+7", "Horrid Touch8d10", "Immolating Gaze4d10", "Teleport")
]
break
case 42:
encounter.text = "Bael emerges from the deepest pits of the nine hells. You are stricken by his warrior-like, bovine appearance. A wicked grin betrays his truly diabolical plans for you."
encounter.enemies = [
createEnemy("Bael", calculateRoll("18d10+90"), 18, 13, "4d8+27", "d20+3", "Awaken Greed", "Teleport", "Regenerate", "Inflict Wounds4d8+27", "Invisibility")
]
break
case 43:
encounter.text = "The demon lord of the abyss, Baphomet, has waited eons for his chance to lead his assault onto the material world. He does not see you as a threat. Only an inconvenience."
encounter.enemies = [
createEnemy("Baphomet", calculateRoll("22d12+176"), 22, 17, "3d10+30", "d20+2", "Curse of Brutality", "Desecration Breath20d8", "Gouging Toss2d8", "Raise Labyrinth")
]
break
case 44:
encounter.text = "You're miles from the sea and yet its here: the leviathan. It charges at you across the water with full force!"
encounter.enemies = [
createEnemy("Leviathan", calculateRoll("16d20+160"), 17, 16, "4d10+40", "d20+7", "Tidal Wave6d10")
]
break
case 45:
encounter.text = "Dripping. Disgusting. You are acosted by the flesh colossus! All is lost."
encounter.enemies = [
createEnemy("Flesh Colossus", calculateRoll("16d20+112"), 14, 13, "6d6+14", "d20-1", "Elemental Breath9d8")
]
break
case 46:
encounter.text = "You've never seen a creature as beautiful or as regal as the androsphinx. Unfortunately, it regards you with disdain."
encounter.enemies = [
createEnemy("Androsphinx", calculateRoll("19d10+95"), 17, 12, "4d10+12", "d20", "Flame Strike8d6", "Roar", "Teleport")
]
break
case 47:
encounter.text = "Balor is a fiend. A huge, demonic fiend bent on destroying you and all you represent."
encounter.enemies = [
createEnemy("Balor", calculateRoll("21d12+136"), 19, 14, "6d8+16", "d20+2", "Fire Whip 5d6+8", "Teleport")
]
break
case 48:
encounter.text = "There is no explaining how you are face to face with a Dragon Turtle and yet... here you are. Fight!"
encounter.enemies = [
createEnemy("Dragon Turtle", calculateRoll("22d20+110"), 20, 13, "6d8+21", "d20", "Steam Breath15d6")
]
break
case 49:
encounter.text = "An echoing boom reverberates across the area, shaking you to your core. The Iron Golem is activated."
encounter.enemies = [
createEnemy("Iron Golem", calculateRoll("20d10+100"), 20, 13, "6d8+14", "d20=1", "Poison Breath10d8", "Slam3d8+7")
]
break
case 50:
encounter.text = "A lake with unkown depths is before you. The kraken's lair. It emerges, ready to strike out at you."
encounter.enemies = [
createEnemy("Kraken", calculateRoll("27d20+189"), 18, 17, "9d6+30", "d20", "Lightning Storm12d10", "Ink Cloud3d10", "Fling1d6")
]
break
case 51:
encounter.text = "The lich commands an incredible army of the dead. Strike now for the good of the realm!"
encounter.enemies = [
createEnemy("Lich", calculateRoll("18d8+54"), 17, 12, "3d6", "Acid Arrow4d4", "Fireball8d6", "Dimension Door", "Animate Dead", "Ray of Frost3d8", "Disrupt Life6d6", "Frightening Gaze", "Paralyzing Touch")
]
break
case 52:
encounter.text = "So many arms. The marilith slithers into view, waving its longswords all around."
encounter.enemies = [
createEnemy("Marilith", calculateRoll("18d10+90"), 18, 9, "12d8+24", "d20+5", "Teleport", "Parry")
]
break
case 53:
encounter.text = "The pit fiend is protecting something of great value. Put that out of your mind because you should be preparing for one hell of a fight."
encounter.enemies = [
createEnemy("Pit Fiend", calculateRoll("24d10+168"), 19, 14, "8d8+32", "d20+2", "Fireball8d6", "Wall of Fire")
]
break
case 54:
encounter.text = "The planetar is a celestial in true form. This one, fallen and disgraced, shall now vanquish you in the name of some forgotten god."
encounter.enemies = [
createEnemy("Planetar", calculateRoll("16d10+112"), 19, 12, "4d6+7", "d20+5", "Insect Plague4d10", "Blade Barrier", )
]
break
case 55:
encounter.text = "Angelic is the least you can say about the solar. Beautiful, powerful. All fear the mighty solar!"
encounter.enemies = [
createEnemy("Solar", calculateRoll("18d10+144"), 21, 15, "8d6+16", "d20+6", "Flying Sword", "Searing Burst8d6", "Blinding Gaze")
]
break
case 56:
encounter.text = "The tarrasque laid dormant for unknowable eons. Your arrival, however, triggered a series of events leading to its awakening. Its massive form stirs, sending the earth crumbling before you."
encounter.enemies = [
createEnemy("Tarrasque", calculateRoll("33d20+330"), 25, 19, "20d8+50", "d20")
]
break
case 57:
encounter.text = "The bore worm is much like the purple worm, yet it is much more dangerous. A construct made of nigh unbreakable metals, its singular goal is clear: your destruction."
encounter.enemies = [
createEnemy("Bore Worm", calculateRoll("15d20+90"), 18, 9, "6d8+18", "d20-2")
]
break
case 58:
encounter.text = "The raeleus decides to finally make his presence known. He's been watching. Waiting. This half human, half zebra amalgamation is quite the inventor. It employs its grand arsenal on you. Run or fight, he's going to get you."
encounter.enemies = [
createEnemy("Raeleus", calculateRoll("19d12+190"), 22, 17, "6d6+10", "d20+5", "Musket Blast6d10+10", "Auto Pistolero10d6", "Canister Grenada4d10", "Stun Grenada", "Magic Chaff Grenada")
]
break
case 59:
encounter.text = "The death knight is not one to toil with. It's too late for you, but consider this a warning to the next group of fools who think they could stand toe to toe with this undead warrior."
encounter.enemies = [
createEnemy("Death Knight", calculateRoll("19d8+95"), 20, 11, "3d8+15", "d20+2", "Hellfire Orb10d6", "Parry", "Destructive Wave5d6")
]
break
case 60:
encounter.text = "This is getting out of hand. Demons, monsters, and now the Drow Matron Mother is on attack! She's a very powerful elven caster that commands a vast network of fiends and slaves."
encounter.enemies = [
createEnemy("Drow Matron Mother", calculateRoll("35d8+105"), 17, 10, "2d6+8", "d20+4", "Levitate", "Plane Shift", "Gate", "Geas5d10", "Guardian of Faith", "Tentacle Rod3d6", "Summon Servant")
]
break
}
break
}
var characterName = toTitleCase(state.characters[getRandomInteger(0, state.characters.length-1)].name)
var characterNameAdjustedCase = characterName == "You" ? "you" : characterName
var possessiveName = getPossessiveName(characterName)
encounter.text = encounter.text.replaceAll("Character", characterName)
encounter.text = encounter.text.replaceAll("character", characterNameAdjustedCase)
encounter.text = encounter.text.replaceAll("character's", possessiveName)
encounter.text = encounter.text.replaceAll("Character's", toTitleCase(possessiveName))
for (var enemy of encounter.enemies) {
enemy.health = Math.floor(enemy.health * multiplier)
enemy.ac = Math.floor(enemy.ac * multiplier)
damagePrefix = enemy.damage.match(/^\d*d\d*/gi)[0]
damageSuffix = enemy.damage.match(/(?<=^\d*d\d*)(\+|-).*$/gi)
damageSuffix = damageSuffix != null ? parseInt(damageSuffix[0]) : 0
damageSuffix += Math.floor(3 * (multiplier - 1))
damageSuffix = `${damageSuffix > 0 ? "+" : ""}${damageSuffix}`
enemy.damage = `${damagePrefix}${damageSuffix == 0 ? "" : damageSuffix}`
initiativePrefix = enemy.initiative.match(/^\d*d\d*/gi)[0]
initiativeSuffix = enemy.initiative.match(/(?<=^\d*d\d*)(\+|-).*$/gi)
initiativeSuffix = initiativeSuffix != null ? parseInt(initiativeSuffix[0]) : 0
initiativeSuffix += Math.floor(3 * (multiplier - 1))
initiativeSuffix = `${initiativeSuffix > 0 ? "+" : ""}${initiativeSuffix}`
enemy.initiative = `${initiativePrefix}${initiativeSuffix == 0 ? "" : initiativeSuffix}`
}
return encounter
}
function createEnemy(name, health, ac, hitModifier, damage, initiative, ...spells) {
var enemy = {
name: name,
health: health,
ac: ac,
hitModifier: hitModifier,
damage: damage,
initiative: initiative,
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
var newName
var enemyMatches
do {
newName = `${name} ${letters[letterIndex++]}`
enemyMatches = state.enemies.filter(x => x.name.toLowerCase() == newName.toLowerCase())
} while (enemyMatches.length > 0 && letterIndex < letters.length)
return newName
}
function createInitiativeOrder() {
state.initiativeOrder = []
for (var character of state.characters) {
if (character.health <= 0) continue
state.initiativeOrder.push(character)
}
for (var enemy of state.enemies) {
if (enemy.health <= 0) continue
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;
});
}
const levelSplits = [0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]
function getLevel(experience) {
if (experience < 0) experience = 0
var level
for (level = 0; level < levelSplits.length; level++) {
if (experience < levelSplits[level]) break
}
return level
}
function getNextLevelXp(experience) {
if (experience < 0) experience = 0
var level
for (level = 0; level < levelSplits.length; level++) {
if (experience < levelSplits[level]) return levelSplits[level]
}
return -1
}
function addXpToAll(experience) {
if (experience == 0) return ""
var leveledUp = `\n[The party has gained ${experience} experience!]`
state.characters.forEach(x => {
var haveWord = x.name == "You" ? "have" : "has"
const oldLevel = getLevel(x.experience)
x.experience += experience
const newLevel = getLevel(x.experience)
if (newLevel > oldLevel) leveledUp += `\n[${x.name} ${haveWord} leveled up to ${newLevel}!]`
})
return leveledUp
}
function getHealthMax(character) {
if (character == null) character = getCharacter()
var modifier = 0
var stat = character.stats.find((element) => element.name.toLowerCase() == "constitution")
if (stat != null) modifier = getModifier(stat.value)
var level = getLevel(character.experience)
return 10 + level * (6 + modifier)
}
function getModifier(statValue) {
return Math.floor((statValue - 10) / 2)
}
function findSpellCardIndex(name) {
return storyCards.findIndex((element) => element.type == "spell" && element.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() {
state.stragedyEnemyScore = 0
state.stragedyPlayerScore = 0
var playerHasJoker = false
var enemyHasJoker = false
var playerBlessedPoints = 0
var enemyBlessedPoints = 0
var doubledPoints = []
//check for kings
for(card of state.stragedyPlayerBattlefield) {
var points = parseInt(card.match(/(?<=.*)\d+/gi)[0])
if (card.includes("k")) {
doubledPoints.push(points)
}
}
for(card of state.stragedyEnemyBattlefield) {
var points = parseInt(card.match(/(?<=.*)\d+/gi)[0])
if (card.includes("k")) {
doubledPoints.push(points)
}
}
//enemy
for(card of state.stragedyEnemyBattlefield) {
var points = parseInt(card.match(/(?<=.*)\d+/gi)[0])
if (doubledPoints.includes(points)) {
points *= 2
}
if (card.includes("q")) {
state.stragedyPlayerScore += points
points = 0
}
if (card.includes("?")) {
enemyHasJoker = true
}
if (card.includes("p")) {
enemyBlessedPoints += points
}
state.stragedyEnemyScore += points
}
if (enemyHasJoker && state.stragedyEnemyScore < 30) {
state.stragedyEnemyScore = 30
} else if (state.stragedyEnemyScore > 30) {
state.stragedyEnemyScore = Math.max(30, state.stragedyEnemyScore - enemyBlessedPoints)
}
//player
for(card of state.stragedyPlayerBattlefield) {
var points = parseInt(card.match(/(?<=.*)\d+/gi)[0])
if (doubledPoints.includes(points)) {
points *= 2
}
if (card.includes("q")) {
state.stragedyEnemyScore += points
points = 0
}
if (card.includes("?")) {
playerHasJoker = true
}
if (card.includes("p")) {
playerBlessedPoints += points
}
state.stragedyPlayerScore += points
}
if (playerHasJoker && state.stragedyPlayerScore < 30) {
state.stragedyPlayerScore = 30
} else if (state.stragedyPlayerScore > 30) {
state.stragedyPlayerScore = Math.max(30, state.stragedyPlayerScore - playerBlessedPoints)
}
}
function stragedyEnemyTurn() {
state.stragedyEnemySkipTurn = false
state.stragedyEnemyTurnText = ""
if (state.stragedyPlayerScore > 30) {
state.stragedyEnemyTurnText = null
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
return
}
var score = state.stragedyEnemyScore
var hand = state.stragedyEnemyHand
var deck = state.stragedyEnemyDeck
var discard = state.stragedyEnemyDiscard
var battlefield = state.stragedyEnemyBattlefield
var playerScore = state.stragedyPlayerScore
var playerHand = state.stragedyPlayerHand
var playerDeck = state.stragedyPlayerDeck
var playerDiscard = state.stragedyPlayerDiscard
var playerBattlefield = state.stragedyPlayerBattlefield
var playerRetired = state.stragedyPlayerRetired
var kingCards = new Set()
var hasJokerOnBattlefield = false
for (var card of battlefield) {
if (card.includes("?")) {
hasJokerOnBattlefield = true
break
}
}
var battlefieldNumbersOnly = []
for (var card of battlefield) {
var value = parseInt(card.replaceAll(/\D/g, ""))
if (value != null) battlefieldNumbersOnly.push(value)
}
var playerBattlefieldNumbersOnly = []
for (var card of playerBattlefield) {
var value = parseInt(card.replaceAll(/\D/g, ""))
if (value != null) playerBattlefieldNumbersOnly.push(value)
}
for(var card of state.stragedyPlayerBattlefield) {
if (card.includes("k")) {
kingCards.add(card.match(/(?<=.*)\d+/gi)[0])
}
}
for(var card of state.stragedyEnemyBattlefield) {
if (card.includes("k")) {
kingCards.add(card.match(/(?<=.*)\d+/gi)[0])
}
}
var hasNumberedCards = hand.filter(x => /^\d+$/gi.test(x)).length > 0
var sortedNumberedBattlefieldCards = battlefield.filter(x => /^[kpw\?]*\d+$/gi.test(x)).sort((a, b) => parseInt(a.replaceAll(/\D/gi, "")) - parseInt(b.replaceAll(/\D/gi, "")))
var highestNumberedBattlefieldCard = sortedNumberedBattlefieldCards.length > 0 ? sortedNumberedBattlefieldCards[sortedNumberedBattlefieldCards.length - 1] : null
var lowestNumberedBattlefieldCard = sortedNumberedBattlefieldCards.length > 0 ? sortedNumberedBattlefieldCards[0] : null
var hasAce = hand.filter(x => /^.*a.*$/gi.test(x)).length > 0
var hasJack = hand.filter(x => /^.*j.*$/gi.test(x)).length > 0
var hasQueen = hand.filter(x => /^.*q.*$/gi.test(x)).length > 0
var hasKing = hand.filter(x => /^.*k.*$/gi.test(x)).length > 0
var hasJoker = hand.filter(x => /^.*\?.*$/gi.test(x)).length > 0
var hasWitch = hand.filter(x => /^.*w.*$/gi.test(x)).length > 0
var hasPriest = hand.filter(x => /^.*p.*$/gi.test(x)).length > 0
var hasBrigand = hand.filter(x => /^.*b.*$/gi.test(x)).length > 0
var faceCardHandCount = hand.filter(x => /.*\D.*/gi.test(x)).length
var highestNumberedHandCardToReach30 = null
var highestNumberedHandCardToReach30Value = 0
for (var card of hand) {
if (isNaN(card)) continue
var value = parseInt(card)
if (kingCards.has(value.toString())) value *= 2
if (value > highestNumberedHandCardToReach30Value && score + value <= 30) {
highestNumberedHandCardToReach30 = card
highestNumberedHandCardToReach30Value = value
}
}
var highestNumberedHandCardToReach20 = null
var highestNumberedHandCardToReach20Value = 0
for (var card of hand) {
if (isNaN(card)) continue
var value = parseInt(card)
if (kingCards.has(value.toString())) value *= 2
if (value > highestNumberedHandCardToReach20Value && score + value <= 20) {
highestNumberedHandCardToReach20 = card
highestNumberedHandCardToReach20Value = value
}
}
var kingNumberedCardsInHand = []
for (var card of hand) {
if (kingCards.has(card)) kingNumberedCardsInHand.push(card)
}
kingNumberedCardsInHand.sort((a, b) => parseInt(a) - parseInt(b))
var bestAceCard = null
var bestAceCardTotal = 0
for (var card of battlefield) {
var number = card.replaceAll(/\D/gi, "")
var playerTotal = 0
for (var playerCard of playerBattlefield) {
var playerNumber = playerCard.replaceAll(/\D/gi, "")
if (playerNumber == number) playerTotal += parseInt(playerNumber)
}
if (playerTotal > bestAceCardTotal) bestAceCard = card
}
var bestKingCardToBustPlayer = null
var bestKingCardToBustPlayerValue = 0
for (var card of battlefield) {
var number = card.replaceAll(/\D/gi, "")
var value = parseInt(number)
if (card.includes("q")) continue
if (kingCards.has(number)) continue
var count = 0
for (var testCard of battlefield) {
if (testCard.replaceAll(/\D/gi, "") == number) count++
}
if (value * count > bestKingCardToBustPlayerValue && score + value * count <= 30) {
bestKingCardToBustPlayer = card
bestKingCardToBustPlayerValue = value
}
}
var bestKingCardToReach30 = null
var bestKingCardToReach30Value = 0
for (var card of battlefield) {
var number = card.replaceAll(/\D/gi, "")
var value = parseInt(number)
if (card.includes("q")) continue
if (kingCards.has(number)) continue
var count = 0
for (var testCard of battlefield) {
if (testCard.replaceAll(/\D/gi, "") == number) count++
}
if (value * count > bestKingCardToReach30Value && score + value * count <= 30) {
bestKingCardToReach30 = card
bestKingCardToReach30Value = value
}
}
var bestJackCardToSave = null
var bestJackCardToSaveValue = 0
for (var card of battlefield) {
if (card.includes("q")) continue
var value = parseInt(card.replaceAll(/\D/gi, ""))
if (kingCards.has(value.toString())) value *= 2
if (value > bestJackCardToSaveValue && score - value <= 30) {
bestJackCardToSave = card
bestJackCardToSaveValue = value
}
}
var bestQueenCardToBustPlayer = null
var bestQueenCardToBustPlayerValue = 0
for (var card of battlefield) {
if (card.includes("q")) continue
var value = parseInt(card.replaceAll(/\D/gi, ""))
if (kingCards.has(value.toString())) value *= 2
if (value > bestQueenCardToBustPlayerValue && playerScore + value > 30) {
bestQueenCardToBustPlayer = card
bestQueenCardToBustPlayerValue = value
}
}
var bestQueenCardToSave = null
var bestQueenCardToSaveValue = 0
for (var card of battlefield) {
if (card.includes("q")) continue
var value = parseInt(card.replaceAll(/\D/gi, ""))
if (kingCards.has(value.toString())) value *= 2
if (value > bestQueenCardToSaveValue && score - value <= 30) {
bestQueenCardToSave = card
bestQueenCardToSaveValue = value
}
}
var bestPriestCardToSave = null
var bestPriestCardToSaveValue = 0
for (var card of battlefield) {
if (card.includes("p")) continue
var value = parseInt(card.replaceAll(/\D/gi, ""))
if (kingCards.has(value.toString())) value *= 2
if (value > bestPriestCardToSaveValue && score - value <= 30) {
bestPriestCardToSave = card
bestPriestCardToSaveValue = value
}
}
var bestPriestCard = null
var bestPriestCardValue = 0
for (var card of battlefield) {
if (card.includes("p")) continue
var value = parseInt(card.replaceAll(/\D/gi, ""))
if (kingCards.has(value.toString())) value *= 2
if (value > bestPriestCardValue) {
bestPriestCard = card
bestPriestCardValue = value
}
}
if (hand.length == 0) {
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) {
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)
else if (hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard)
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) {
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)
else if (hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer)
else if (hasKing && bestKingCardToReach30 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToReach30)
else if (highestNumberedHandCardToReach30 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, highestNumberedHandCardToReach30)
else if (hasJoker && playerScore == 30) state.stragedyEnemyTurnText = stragedyPlayCard(false, "?" + lowestNumberedBattlefieldCard)
else state.stragedyEnemyTurnText = stragedyEnemyRetire()
} else if (playerRetired && score > playerScore && !hasJokerOnBattlefield) {
state.stragedyEnemyTurnText = stragedyEnemyRetire()
} else if (playerRetired && score == playerScore) {
if (highestNumberedHandCardToReach30 != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, highestNumberedHandCardToReach30)
else state.stragedyEnemyTurnText = stragedyEnemyRetire()
} else if (score - playerScore > 20 && !hasJokerOnBattlefield) {
state.stragedyEnemyTurnText = stragedyEnemyRetire()
} else if (deck.length > 0 && hand.length == 1) {
state.stragedyEnemyTurnText = stragedyEnemyDiscardCard()
} else if (hasNumberedCards && (score < playerScore || score < 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)
else if (faceCardHandCount > 1 && hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer)
else if (faceCardHandCount > 1 && hasQueen && highestNumberedBattlefieldCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + highestNumberedBattlefieldCard)
else if (deck.length > 0) state.stragedyEnemyTurnText = stragedyEnemyDiscardCard()
else if (hasQueen && highestNumberedBattlefieldCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + highestNumberedBattlefieldCard)
else if (hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard)
else if (hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer)
else stragedyEnemyRandom()
} else if (score >= playerScore && hasWitch) {
state.stragedyEnemyTurnText = stragedyPlayCard(false, "w")
} else if (score >= playerScore && hasBrigand) {
state.stragedyEnemyTurnText = stragedyPlayCard(false, "b")
} else if (highestNumberedHandCardToReach20 == null && hand.length > 0) {
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)
else if (deck.length > 0) state.stragedyEnemyTurnText = stragedyEnemyDiscardCard()
else if (hasQueen && bestQueenCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "q" + bestQueenCardToBustPlayer)
else if (hasKing && bestKingCardToBustPlayer != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "k" + bestKingCardToBustPlayer)
else if (hasPriest && bestPriestCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "p" + bestPriestCard)
else if (hasAce && bestAceCard != null) state.stragedyEnemyTurnText = stragedyPlayCard(false, "a" + bestAceCard)
else state.stragedyEnemyTurnText = stragedyEnemyRetire()
} else {
state.stragedyEnemyTurnText = stragedyEnemyRandom()
}
stragedyCalculateScores()
if (state.stragedyEnemyScore > 30) {
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
}
}
function stragedyEnemyDrawCard() {
var card = state.stragedyEnemyDeck.pop()
state.stragedyEnemyHand.push(card)
return `\nThe opponent has drawn a card.\n`
}
function stragedyEnemyDiscardCard() {
var hand = [...state.stragedyEnemyHand]
var score = state.stragedyEnemyScore
var hasAce = hand.filter(x => /^.*a.*$/gi.test(x)).length > 0
var hasJack = hand.filter(x => /^.*j.*$/gi.test(x)).length > 0
var hasQueen = hand.filter(x => /^.*q.*$/gi.test(x)).length > 0
var hasKing = hand.filter(x => /^.*k.*$/gi.test(x)).length > 0
var hasJoker = hand.filter(x => /^.*\?.*$/gi.test(x)).length > 0
var hasWitch = hand.filter(x => /^.*w.*$/gi.test(x)).length > 0
var hasPriest = hand.filter(x => /^.*p.*$/gi.test(x)).length > 0
var sortedNumberedHandCardsToAddUpTo30 = hand.filter(x => /^\d+$/gi.test(x) && parseInt(x) <= 30 - score).sort((a, b) => parseInt(a) - parseInt(b))
var highestNumberedHandCardAddUpTo30 = sortedNumberedHandCardsToAddUpTo30.length > 0 ? sortedNumberedHandCardsToAddUpTo30[sortedNumberedHandCardsToAddUpTo30.length - 1] : null
if (hand.length > 1) {
if (hasQueen) hand.splice(hand.indexOf("q"))
else if (hasPriest) hand.splice(hand.indexOf("p"))
else if (hasKing) hand.splice(hand.indexOf("k"))
else if (hasWitch) hand.splice(hand.indexOf("w"))
else if (hasJoker) hand.splice(hand.indexOf("?"))
else if (hasJack) hand.splice(hand.indexOf("j"))
else if (hasAce) hand.splice(hand.indexOf("a"))
else if (highestNumberedHandCardAddUpTo30 != null) hand.splice(hand.indexOf(highestNumberedHandCardAddUpTo30))
}
var card = state.stragedyEnemyHand.splice(state.stragedyEnemyHand.indexOf(getRandomInteger(0, hand.length - 1)), 1)
state.stragedyEnemyDiscard.push(card)
var newCards = state.stragedyEnemyDeck.splice(state.stragedyEnemyDeck.length - 2, 2)
state.stragedyEnemyHand.push(...newCards)
return `\nThe opponent has discarded a card and drawn ${newCards.length} cards.\n`
}
function stragedyEnemyRetire() {
state.stragedyEnemyRetired = true
return `\nThe opponent has retired at ${state.stragedyEnemyScore} points.\n`
}
function stragedyEnemyRandom(punish) {
var hand = [...state.stragedyEnemyHand]
if (hand.length == 0) {
if (punish) return "\nThe enemy has no cards to play.\n"
if (state.stragedyEnemyDeck.length > 0) return stragedyEnemyDrawCard()
return stragedyEnemyRetire()
}
do {
var index = getRandomInteger(0, hand.length - 1)
var card = hand.splice(index, 1)[0]
if (/\d+/gi.test(card)) {
return stragedyPlayCard(false, card)
} else if (state.stragedyEnemyBattlefield.length > 0) {
var battlefield = [...new Set(state.stragedyEnemyBattlefield)]
do {
var battlefieldIndex = getRandomInteger(0, battlefield.length - 1)
var battlefieldCard = battlefield.splice(battlefieldIndex, 1)[0]
if (!battlefieldCard.includes(card)) {
return stragedyPlayCard(false, card + battlefieldCard)
}
} while (battlefield.length > 0)
}
} while (hand.length > 0)
if (punish) {
state.stragedyEnemyDiscard.push(...state.stragedyEnemyHand)
state.stragedyEnemyHand = []
return "\nThe enemy could not play any cards and therfore discarded their entire hand.\n"
}
if (state.stragedyEnemyDeck.length > 0) return stragedyEnemyDrawCard()
return stragedyEnemyRetire()
}
function stragedyPlayerRandom(punish) {
var hand = [...state.stragedyPlayerHand]
if (hand.length == 0) {
return "\nThe player has no cards to play.\n"
}
do {
var index = getRandomInteger(0, hand.length - 1)
var card = hand.splice(index, 1)[0]
if (/\d+/gi.test(card)) {
return stragedyPlayCard(true, card)
} else if (state.stragedyPlayerBattlefield.length > 0) {
var battlefield = [...new Set(state.stragedyPlayerBattlefield)]
do {
var battlefieldIndex = getRandomInteger(0, battlefield.length - 1)
var battlefieldCard = battlefield.splice(battlefieldIndex, 1)[0]
if (!battlefieldCard.includes(card)) {
return stragedyPlayCard(true, card + battlefieldCard)
}
} while (battlefield.length > 0)
}
} while (hand.length > 0)
if (punish) {
state.stragedyPlayerDiscard.push(...state.stragedyEnemyHand)
state.stragedyPlayerHand = []
return "\nThe player could not play any cards and therfore discarded their entire hand.\n"
}
if (state.stragedyEnemyDeck.length > 0) return stragedyEnemyDrawCard()
return stragedyEnemyRetire()
}
function stragedyPlayerTurn(text) {
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"
var targetCard = text.substring(1).toLowerCase()
if (targetCard.length == 0) return "\nYou must specify the card you wish to discard\n"
var handIndex = state.stragedyPlayerHand.findIndex(x => x.toLowerCase() == targetCard)
if (handIndex == -1) return "\nYou cannot discard a card that is not in your hand.\n"
state.stragedyPlayerHand.splice(handIndex, 1);
state.stragedyPlayerDiscard.push(targetCard)
var newCards = state.stragedyPlayerDeck.splice(state.stragedyPlayerDeck.length - 2)
state.stragedyPlayerHand.push(...newCards)
text = `You discard the "${targetCard}" card. You draw `
if (newCards.length == 1) text += `a "${newCards[0]}" card.`
else text += `the "${newCards[0]}" and "${newCards[1]}" cards.`
stragedyCalculateScores()
if (state.stragedyEnemyRetired) {
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
} else stragedyEnemyTurn()
return text
} else if (text.startsWith("d") && state.stragedyPlayerHand.length == 0) {
if (state.stragedyPlayerDeck.length == 0) return "\nYou cannot draw if you have 0 cards in your deck.\n"
var drawCard = state.stragedyPlayerDeck.pop()
state.stragedyPlayerHand.push(drawCard)
stragedyCalculateScores()
if (state.stragedyEnemyRetired) {
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
} else stragedyEnemyTurn()
return `You draw a ${drawCard}`
} else if (text == "r") {
var hasJokerOnBattlefield = false
for (var card of state.stragedyPlayerBattlefield) {
if (card.includes("?")) {
hasJokerOnBattlefield = true
break
}
}
if (hasJokerOnBattlefield) {
return "\nYou cannot retire while you have a joker on the battlefield.\n"
}
state.stragedyPlayerRetired = true
stragedyCalculateScores()
var text = `You retire at ${state.stragedyPlayerScore}.`
stragedyEnemyTurn()
stragedyCalculateScores()
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
return text
} else {
var text = stragedyPlayCard(true, text)
if (state.stragedyEnemyRetired) {
stragedyCheckForWin()
state.stragedyTurn = "gameOver"
} else stragedyEnemyTurn()
return text
}
}
function stragedyPlayCard(player, text) {
var character = getCharacter()
if (player) {
var battlefield = state.stragedyPlayerBattlefield
var hand = state.stragedyPlayerHand
var deck = state.stragedyPlayerDeck
var discard = state.stragedyPlayerDiscard
var characterName = toTitleCase(character.name)
var playedWord = character.name == "You" ? "played" : "play"
var enemyName = "The opponent"
var enemyDeck = state.stragedyEnemyDeck
var enemyHand = state.stragedyEnemyHand
var enemyDiscard = state.stragedyEnemyDiscard
var enemyBattlefield = state.stragedyEnemyBattlefield
} else {
var battlefield = state.stragedyEnemyBattlefield
var hand = state.stragedyEnemyHand
var deck = state.stragedyEnemyDeck
var discard = state.stragedyEnemyDiscard
var characterName = "The opponent"
var playedWord = "played"
var enemyName = toTitleCase(character.name)
var enemyDeck = state.stragedyPlayerDeck
var enemyHand = state.stragedyPlayerHand
var enemyDiscard = state.stragedyPlayerDiscard
var enemyBattlefield = state.stragedyPlayerBattlefield
}
var isNumberedCard = /^\d+$/.test(text)
var handCard = isNumberedCard ? text : text.substring(0, 1).toLowerCase()
var targetCard = isNumberedCard ? null : text.substring(1).toLowerCase()
var handIndex = hand.findIndex(x => x.toLowerCase() == handCard)
if (handIndex == -1) {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou can only play cards that are in your hand\n"
}
var targetIndex = targetCard == "" ? -1 : battlefield.findIndex(x => x.toLowerCase() == targetCard)
if (!isNumberedCard && targetCard != "" && targetIndex == -1) {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target that is placed on your side of the battlefield.\n"
}
switch (handCard) {
case "a":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the Ace (ie. a2)\n"
}
hand.splice(handIndex, 1)
for (var i = battlefield.length - 1; i >= 0; i--) {
if (battlefield[i].endsWith(targetCard)) {
discard.push(...battlefield[i])
battlefield.splice(i, 1)
}
}
for (var i = enemyBattlefield.length - 1; i >= 0; i--) {
if (enemyBattlefield[i].endsWith(targetCard)) {
enemyDiscard.push(...enemyBattlefield[i])
enemyBattlefield.splice(i, 1)
}
}
stragedyCalculateScores()
return `\n${characterName} ${playedWord} an ace on ${targetCard}. All ${targetCard}s are removed.\n`
case "j":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the Jack (ie. j2)\n"
}
battlefield.splice(targetIndex, 1)
var discardCards = [...targetCard]
hand.splice(handIndex, 1)
discardCards.push(handCard)
discard.push(...discardCards)
shuffle(discard)
var addCard = discard.pop()
hand.push(addCard)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a jack on the ${targetCard}. The ${targetCard} is removed. ${player ? `${characterName} drew a ${addCard} from the discard pile.` : ""}\n`
case "q":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the Queen (ie. q2)\n"
}
hand.splice(handIndex, 1)
battlefield.splice(targetIndex, 1)
battlefield.push(handCard + targetCard)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a queen on the ${targetCard}. The value is added to the opponent.\n`
case "k":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the King (ie. k2)\n"
}
hand.splice(handIndex, 1)
battlefield.splice(targetIndex, 1)
battlefield.push(handCard + targetCard)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a king on the ${targetCard}. All ${targetCard.match(/\d+/g)} values are doubled.\n`
case "?":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the Joker (ie. ?2)\n"
}
hand.splice(handIndex, 1)
battlefield.splice(targetIndex, 1)
battlefield.push(handCard + targetCard)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a joker on the ${targetCard}. The card's value is increased to make the total score 30.\n`
case "w":
hand.splice(handIndex, 1)
discard.push(handCard)
var enemyMove = !player ? stragedyPlayerRandom(true) : stragedyEnemyRandom(true)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a witch on ${enemyName}. ${enemyMove}\n`
case "p":
if (targetCard == "") {
if (player) state.stragedyEnemySkipTurn = true
return "\nYou must specify a target to use the Priest (ie. p2)\n"
}
hand.splice(handIndex, 1)
battlefield.splice(targetIndex, 1)
battlefield.push(handCard + targetCard)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a priest on the ${targetCard}. This card is prevented from causing ${characterName} to bust.\n`
case "b":
hand.splice(handIndex, 1)
discard.push(handCard)
var i
for (i = 0; i < 5 && enemyDeck.length > 0; i++) {
var card = enemyDeck.pop()
enemyDiscard.push(...card)
}
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a brigand on ${enemyName}. They are forced to discard ${i} cards from their deck\n`
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
case "10":
battlefield.push(handCard)
hand.splice(handIndex, 1)
stragedyCalculateScores()
return `\n${characterName} ${playedWord} a ${handCard}.\n`
default:
if (player) state.stragedyEnemySkipTurn = true
return "\nUnrecognized card specified. Stop playing with counterfit cards!\n"
}
}
function stragedyCheckForWin() {
if (state.stragedyEnemyScore > 30 && state.stragedyPlayerScore > 30) state.stragedyWinner = "tie"
else if (state.stragedyEnemyScore > 30) state.stragedyWinner = "player"
else if (state.stragedyPlayerScore > 30) state.stragedyWinner = "enemy"
else if (state.stragedyPlayerScore > state.stragedyEnemyScore) state.stragedyWinner = "player"
else if (state.stragedyEnemyScore > state.stragedyPlayerScore) state.stragedyWinner = "enemy"
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);
}
String.prototype.plural = function(revert) {
var plural = {
'(quiz)$' : "$1zes",
'^(ox)$' : "$1en",
'([m|l])ouse$' : "$1ice",
'(matr|vert|ind)ix|ex$' : "$1ices",
'(x|ch|ss|sh)$' : "$1es",
'([^aeiouy]|qu)y$' : "$1ies",
'(hive)$' : "$1s",
'(?:([^f])fe|([lr])f)$' : "$1$2ves",
'(shea|lea|loa|thie)f$' : "$1ves",
'sis$' : "ses",
'([ti])um$' : "$1a",
'(tomat|potat|ech|her|vet)o$': "$1oes",
'(bu)s$' : "$1ses",
'(alias)$' : "$1es",
'(octop)us$' : "$1i",
'(ax|test)is$' : "$1es",
'(us)$' : "$1es",
'([^s]+)$' : "$1s"
};
var singular = {
'(quiz)zes$' : "$1",
'(matr)ices$' : "$1ix",
'(vert|ind)ices$' : "$1ex",
'^(ox)en$' : "$1",
'(alias)es$' : "$1",
'(octop|vir)i$' : "$1us",
'(cris|ax|test)es$' : "$1is",
'(shoe)s$' : "$1",
'(o)es$' : "$1",
'(bus)es$' : "$1",
'([m|l])ice$' : "$1ouse",
'(x|ch|ss|sh)es$' : "$1",
'(m)ovies$' : "$1ovie",
'(s)eries$' : "$1eries",
'([^aeiouy]|qu)ies$' : "$1y",
'([lr])ves$' : "$1f",
'(tive)s$' : "$1",
'(hive)s$' : "$1",
'(li|wi|kni)ves$' : "$1fe",
'(shea|loa|lea|thie)ves$': "$1f",
'(^analy)ses$' : "$1sis",
'((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$': "$1$2sis",
'([ti])a$' : "$1um",
'(n)ews$' : "$1ews",
'(h|bl)ouses$' : "$1ouse",
'(corpse)s$' : "$1",
'(us)es$' : "$1",
's$' : ""
};
var irregular = {
'move' : 'moves',
'foot' : 'feet',
'goose' : 'geese',
'sex' : 'sexes',
'child' : 'children',
'man' : 'men',
'tooth' : 'teeth',
'person' : 'people',
'woman' : 'women',
};
var uncountable = [
'sheep',
'fish',
'deer',
'moose',
'series',
'species',
'money',
'rice',
'information',
'equipment',
'gold',
'bass',
'milk',
'food',
'water',
'bread',
'sugar',
'tea',
'cheese',
'coffee',
'currency',
'seafood',
'oil',
'software'
];
// save some time in the case that singular and plural are the same
if(uncountable.indexOf(this.toLowerCase()) >= 0)
return this;
// check for irregular forms
for(word in irregular){
if(revert){
var pattern = new RegExp(irregular[word]+'$', 'i');
var replace = word;
} else{ var pattern = new RegExp(word+'$', 'i');
var replace = irregular[word];
}
if(pattern.test(this))
return this.replace(pattern, replace);
}
if(revert) var array = singular;
else var array = plural;
// check for matches using regular expressions
for(reg in array){
var pattern = new RegExp(reg, 'i');
if(pattern.test(this))
return this.replace(pattern, array[reg]);
}
return this;
}
function clamp(num, min, max) {
return num <= min
? min
: num >= max
? max
: num
}
function toTitleCase(str) {
return str.replace(
/\w\S*/g,
text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
);
}
function stripPunctuation(str) {
return str.replaceAll(/((\.)|(!))\s*$/g, "")
}
var fantasyFemaleNames = ["Luna", "Kayla", "Serenity", "Eira", "Mirah", "Elowen", "Keira", "Calantha", "Natalia", "Eirlys", "Freya", "Ophelia", "Piper", "Alethea", "Melara", "Seraphina", "Delilah", "Lorna", "Echo", "Bree", "Daniella", "Branwen", "Matilda", "Eve", "Brynhild", "Ithilda", "Belinda", "Catarina", "Jora", "Zelda", "Thalia", "Rowan", "Aurora", "Coral", "Vivian", "Briella", "Elvina", "Lylah", "Mirastral", "Nadira", "Marcella", "Kestrel", "Avis", "Laura", "Vesper", "Lucilla", "Sabine", "Evelyn", "Kalinda", "Celeste", "Lilith", "Wren", "Jasmine", "Ondine", "Gabriella", "Astrid", "Elise", "Helena", "Nova", "Lyndal", "Zara", "Niamh", "Vynessa", "Erin", "Lyriel", "Dracaena", "Lila", "Brynna", "Zephyr", "Kira", "Ava", "Elinor", "Carmilla", "Isabella", "Ariana", "Rhianna", "Sylvie", "Kymberley", "Hazel", "Lirien", "Bridget", "Lyra", "Galatea", "Nadine", "Alethia", "Larissa", "Sariel", "Theodora", "Gwynneth", "Eleanor", "Odessa", "Meryll", "Sophia", "Kaia", "Brynhilda", "Haven", "Eluned", "Selene", "Bryony", "Ciara"]
var fantasyMaleNames = ["Alexander", "Thane", "Gabriel", "Orion", "Grayson", "Cedric", "Sebastian", "Arin", "Liam", "Byerson", "Julian", "Zaneth", "Kel", "Eramon", "Ashfur", "Tristam", "Leonidas", "Ryzlen", "Caspian", "Jarron", "Eudicles", "Tarquin", "Terrence", "Ereth", "Thayon", "Braxton", "Twilight", "Argent", "Whisper", "Daemon", "Victor", "Blackthorn", "Dawnrunner", "Gareth", "Caelum", "Zephyr", "Brennan", "Theo", "Draco", "Cian", "Valoric", "Skye", "Sunfor", "Damon", "Maverick", "Bryson", "Lumo", "Drakken", "Ewen", "Waverley", "Lachlan", "Atlas", "Arden", "Ryker", "Asterion", "Bryon", "Judson", "Griffith", "Logan", "Ethan", "Darius", "Brodeth", "Cassius", "Eamon", "Rowan", "Paxton", "Michael", "Dommon", "Aragorn", "Bastier", "Maximus", "Kenrick", "Jasper", "Lucien", "Bryce", "Ryder", "Damian", "Daxton", "Brantley", "Griffin", "Xander", "Galen", "Brody", "Erek", "Drake", "Thayer", "Kieran", "Heath", "Raeleus", "Alistair", "Bastian", "Asher", "Ronan", "Zane", "Jaxon", "Ambrose", "Malcolm", "Axel", "Ehtan", "Avery", "Kael", "Riley"]
var scifiFemaleNames = ["Jala", "Clea", "Jocosa", "Artemis", "Serafina", "Sevyn", "Tesla", "Lux", "Nine", "Kiara", "Valentina", "Morticia", "Rio", "Xyleena", "Libby", "Valkyrie", "Panika", "Lara", "Fenglina", "Makiko", "Katja", "Paige", "Elie", "Lucie", "Samanta", "Hazel", "Helena", "Leia", "Luminara", "Katan", "Vala", "Inara", "Saffron", "Zoe", "Jayne", "Kaylee", "Maeve", "Kara", "Athena", "Cally", "Anastasia", "Pegi", "Alita", "Alkhema", "Arcena", "Ko", "Bodika", "Candi", "Chi", "Rae", "Cylla", "Daria", "Chalma", "Elita", "Eryx", "Eva", "Nova", "Celeste", "Guri", "Hexen", "Indigo", "Juli", "Katana", "Talia", "Lala", "Mika", "Miranda", "Nebula", "Six", "Sasha", "Silica", "Sky", "Strika", "Terra", "Yori", "Andromeda", "Astra", "Bellatrix", "Callista", "Cosima", "Delphine", "Electra", "Phantasy", "Farrah", "Geneva", "Haven", "Jade", "Juno", "Lillix", "Lynx", "Nya", "Oria", "Parris", "Priya", "Rue", "Clarity", "Bloom", "Decca", "Domonique", "Grazi", "Helvetica", "Cadence"]
var scifiMaleNames = ["Ares", "Astro", "Macro", "Cadmus", "Cyno", "Fade", "Hack", "Hax", "Indigo", "Hinge", "Jarno", "Jax", "Knox", "Link", "Maxx", "Merrick", "Miles", "Mirari", "Niko", "Nano", "Oberon", "Onyx", "Orion", "Osso", "Paradox", "Pip", "Phoenix", "Radius", "Rexx", "Razlin", "Reznor", "Rian", "Roscoe", "Ryker", "Rush", "Riden", "Drake", "Frost", "Cassian", "Neyo", "Maverick", "Azriel", "Auryn", "Daggar", "Evyn", "Jace", "Jaron", "Loki", "Oren", "Ridley", "Sagan", "Silas","Solon", "Stellan", "Sorrel", "Seth", "Theron", "Zen", "Klay", "Blaze", "Xander", "Mace", "Dozer", "Eno", "Tip", "Ray", "Genesis", "Galac", "Eclipse", "Zev", "Zaid", "Wilder", "Sol", "Jupiter", "Mars", "Star", "Cosmo", "Aster", "Lazer", "Zeno", "Sirius", "Azra", "Atom", "Teague", "Rigel", "Cato", "Zhane", "Ace", "Rocket", "Kip", "Meter", "Starbuck", "Roman", "Fiat", "Kyron", "Nyx", "Rune", "Nero", "Quantum", "Nym", "Morphius", "Striker", "Bridger"]
var modernFemaleNames = ["Olivia", "Emma", "Amelia", "Ella", "Isabella", "Mia", "Valerie", "Eliana", "Charlotte", "Mila", "Aria", "Luna", "Harper", "Grace", "Zoey", "Jemma", "Priscilla", "Scarlett", "Hazel", "Ellie", "Naya", "Nila", "Tamia", "Cecilia", "Arianna", "Abigail", "Riley", "Autumn", "Maya", "Madelyn", "Maria", "Melody", "Sophia", "Ava", "Luz", "Eleanor", "Ivy", "Freya", "Alice", "Violet", "Clara", "Daphne", "Evelyn", "Nora", "Lucy", "Poppy", "Rose", "Chloe", "Phoebe", "Elsie", "Cordelia", "Willow", "Daisy", "Thea", "Adeline", "Arabella", "Maisie", "Lola", "Olive", "Sienna", "Sierra", "Elena", "Sadie", "Sophie", "Julia", "Alexandra", "Jane", "Mira", "Talia", "Zara", "Vera", "Amara", "Cynthia", "Hannah", "Aurora", "Anya", "Erin", "Felicity", "Juno", "Yelena", "Naomi", "Caroline", "Miriam", "Veronica", "Molly", "June", "Nina", "Piper", "Helena", "Amari", "Everly", "Bonnie", "Alina", "Emilia", "Harriet", "Isabel", "Sofia", "Kayla", "Lena", "Megan", "Diana"]
var modernMaleNames = ["Jackson", "Aiden", "Charles", "Adam", "Christopher", "Daniel", "Liam", "Oliver", "Mateo", "Henry", "Lucas", "William", "Theodore", "Noah", "John", "Arnold", "Norman", "Ralph", "Virgil", "Will", "Sam", "Luca", "David", "Joseph", "Mason", "Luke", "Matthew", "Dylan", "Jacob", "Isaac", "Anthony", "Carter", "Caleb", "Cooper", "Josiah", "Nolan", "Cameron", "Nathan", "Josh", "Angel", "Andrew", "Aaron", "Ian", "Eli", "Ryan", "Everett", "Enzo", "Parker", "Jeremiah", "Landon", "Jordan", "Austin", "Jameson", "Myles", "Dominic", "Nicholas", "Kayden", "Hunter", "Harrison", "Milo", "Arthur", "Ryder", "Archer", "Luis", "George", "Evan", "Carlos", "Juan", "Jason", "Leon", "Calvin", "Ivan", "Cole", "Chase", "Dean", "Jayce", "Olliver", "Alan", "Jesus", "Charlie", "Tyler", "Elliot", "Kevin", "Ayden", "Felix", "Tate", "Jesse", "Brody", "Tucker", "Peter", "Joel", "Edward", "Oscar", "Victor", "Brandon", "Bruce", "Abel", "Richard", "Riley", "Patrick", "Eric", "Elian", "Louis"]
var nordicFemaleNames = ["Freya", "Dagny", "Ingrid", "Froya", "Elin", "Solveig", "Maja", "Sol", "Linnea", "Vilde", "Var", "Aldis", "Alfrida", "Alfsol", "Alva", "Alvdis", "Alvida", "Andora", "Anveig", "Asa", "Astri", "Astrid", "Bolette", "Brynhild", "Disa", "Eir", "Eira", "Eidis", "Elevine", "Elfi", "Embla", "Erna", "Freja", "Frida", "Fredrikke", "Gerda", "Gry", "Gurina", "Gurine", "Gyda", "Haddy", "Halgerd", "Helga", "Helje", "Helle", "Herdis", "Herfrid", "Hilde", "Hulda", "Inga", "Idun", "Isfrid", "Iverna", "Iverine", "Jorgina", "Kari", "Lagertha", "Liv", "Livunn", "Malfrid", "Malmfrid", "Nanna", "Oda", "Odel", "Odine", "Olava", "Runa", "Ragnfrid", "Randi", "Ragnhild", "Saga", "Sif", "Sigrid", "Siv", "Solvei", "Soma", "Svanild", "Thora", "Tora", "Thurid", "Torveig", "Torfrid", "Trude", "Tyra", "Tyri", "Udna", "Unni", "Una", "Unnlaug", "Unnveig", "Valdine", "Vedis", "Valborg", "Vivil", "Ylva", "Yngva", "Thoril", "Thorine", "Sigfrida", "Sigun", "Sigvor", "Signe", "Reidunn"]
var nordicMaleNames = ["Erik", "Bjorn", "Lars", "Asmund", "Harald", "Arne", "Odin", "Ivar", "Leif", "Aesir", "Axel", "Aren", "Aric", "Balder", "Birger", "Bjarke", "Bjarne", "Nils", "Steig", "Erling", "Espen", "Fenrir", "Frey", "Einar", "Garald", "Anders", "Gunnar", "Hagan", "Halfthor", "Halfdan", "Kjell", "Hanne", "Ingvar", "Helge", "Herleif", "Jarl", "Joran", "Magnus", "Norrell", "Njord", "Olaf", "Osman", "Ragnar", "Sigurd", "Steffen", "Sten", "Sven", "Tor", "Torben", "Troels", "Tyr", "Tyrell", "Ulf", "Viggo", "Vali", "Vidar", "Volund", "Wayde", "Waddell", "Alviss", "Anneli", "Stig", "Eino", "Haakon", "Haldor", "Iver", "Sindri", "Trym", "Varg", "Alf", "Amund", "Arn", "Arnstein", "Arvid", "Bjarni", "Bjoern", "Bragi", "Brede", "Dag", "Dagfinn", "Egil", "Endre", "Erlend", "Even", "Finn", "Flosi", "Fredrik", "Frode", "Freyr", "Geir", "Gisli", "Grim", "Gudbrand", "Gustav", "Hakan", "Hakon", "Hans", "Helgi", "Ingolf", "Jomar", "Knut", "Orm"]
function generateName(genre, male) {
if (genre.toLowerCase() == "fantasy") {
if (male) {
if (state.fantasyMaleIndex == null || state.fantasyMaleIndex >= fantasyMaleNames.length) {
state.fantasyMaleIndex = 0
state.fantasyMaleSeed = getRandomInteger(1, 1000)
}
shuffle(fantasyMaleNames, state.fantasyMaleSeed)
return fantasyMaleNames[state.fantasyMaleIndex++]
} else {
if (state.fantasyFemaleIndex == null || state.fantasyFemaleIndex >= fantasyFemaleNames.length) {
state.fantasyFemaleIndex = 0
state.fantasyFemaleSeed = getRandomInteger(1, 1000)
}
shuffle(fantasyFemaleNames, state.fantasyFemaleSeed)
return fantasyFemaleNames[state.fantasyFemaleIndex++]
}
} else if (genre.toLowerCase() == "modern") {
if (male) {
if (state.modernMaleIndex == null || state.modernMaleIndex >= modernMaleNames.length) {
state.modernMaleIndex = 0
state.modernMaleSeed = getRandomInteger(1, 1000)
}
shuffle(modernMaleNames, state.modernMaleSeed)
return modernMaleNames[state.modernMaleIndex++]
} else {
if (state.modernFemaleIndex == null || state.modernFemaleIndex >= modernFemaleNames.length) {
state.modernFemaleIndex = 0
state.modernFemaleSeed = getRandomInteger(1, 1000)
}
shuffle(modernFemaleNames, state.modernFemaleSeed)
return modernFemaleNames[state.modernFemaleIndex++]
}
} else if (genre.toLowerCase() == "scifi") {
if (male) {
if (state.scifiMaleIndex == null || state.scifiMaleIndex >= scifiMaleNames.length) {
state.scifiMaleIndex = 0
state.scifiMaleSeed = getRandomInteger(1, 1000)
}
shuffle(scifiMaleNames, state.scifiMaleSeed)
return scifiMaleNames[state.scifiMaleIndex++]
}
else {
if (state.scifiFemaleIndex == null || state.scifiFemaleIndex >= scifiFemaleNames.length) {
state.scifiFemaleIndex = 0
state.scifiFemaleSeed = getRandomInteger(1, 1000)
}
shuffle(scifiFemaleNames, state.scifiFemaleSeed)
return scifiFemaleNames[state.scifiFemaleIndex++]
}
} else if (genre.toLowerCase() == "nordic") {
if (male) {
if (state.nordicMaleIndex == null || state.nordicMaleIndex >= nordicMaleNames.length) {
state.nordicMaleIndex = 0
state.nordicMaleSeed = getRandomInteger(1, 1000)
}
shuffle(nordicMaleNames, state.nordicMaleSeed)
return nordicMaleNames[state.nordicMaleIndex++]
}
else {
if (state.nordicFemaleIndex == null || state.nordicFemaleIndex >= nordicFemaleNames.length) {
state.nordicFemaleIndex = 0
state.nordicFemaleSeed = getRandomInteger(1, 1000)
}
shuffle(nordicFemaleNames, state.nordicFemaleSeed)
return nordicFemaleNames[state.nordicFemaleIndex++]
}
}
}
const version = "Hashtag DnD v0.7.0"
const rollSynonyms = ["roll"]
const createSynonyms = ["create", "generate", "start", "begin", "setup", "party", "member", "new"]
const renameCharacterSynonyms = ["renamecharacter", "renameperson"]
const cloneCharacterSynonyms = ["clone", "clonecharacter", "cloneperson", "copycharacter", "copyperson", "duplicatecharacter", "duplicateperson", "dupecharacter", "dupeperson"]
const bioSynonyms = ["bio", "biography", "summary", "character", "charactersheet", "statsheet"]
const setClassSynonyms = ["setclass", "class"]
const setSummarySynonyms = ["setsummary", "summary"]
const trySynonyms = ["try", "tryto", "tries", "triesto", "attempt", "attemptto", "attemptsto", "do"]
const setHealthSynonyms = ["sethealth"]
const healSynonyms = ["heal", "mend", "restore"]
const damageSynonyms = ["damage", "hurt", "harm", "injure"]
const restSynonyms = ["rest", "longrest", "shortrest", "sleep", "nap"]
const setExperienceSynonyms = ["setexperience", "setexp", "setxp", "setexperiencepoints"]
const addExperienceSynonyms = ["addexperience", "addexp", "addxp", "addexperiencepoints", "experience", "exp", "gainxp", "gainexperience", "xp", "experiencepoints"]
const levelUpSynonyms = ["levelup", "level"]
const setStatSynonyms = ["setstat", "setstatistic", "setattribute", "setability", "changestat", "changestatistic", "changeattribute", "changeability", "updatestat", "updatestatistic", "updateattribute", "updateability", "stat", "attribute", "ability"]
const showStatsSynonym = ["showstats", "stats", "viewstats", "showabilities", "abilities", "viewabilities", "showstatistics", "statistics", "viewstatistics", "showattributes", "attributes", "viewattributes"]
const removeStatSynonyms = ["removestat", "deletestat", "cancelstat", "removeability", "deleteability", "cancelAbility", "removestatistic", "deletestatistic", "cancelstatistic", "removeattribute", "deleteattribute", "cancelattribute"]
const clearStatsSynonyms = ["clearstats", "clearabilities", "clearstatistics", "clearattributes"]
const setSpellStatSynonyms = ["setspellstat", "setspellstatistic", "setspellability", "setspellcastingability", "changespellstat", "changespellstatistic", "changespellability", "changespellcastingability"]
const setSkillSynonyms = ["setskill", "changeskill", "updateskill", "skill"]
const showSkillsSynonyms = ["showskills", "skills"]
const removeSkillSynonyms = ["removeskill", "deleteskill", "cancelskill"]
const clearSkillsSynonyms = ["clearskills"]
const checkSynonyms = ["check", "checkstat", "checkstatistic", "checkattribute", "checkability", "checkskill", "skillcheck", "abilitycheck"]
const showNotesSynonyms = ["notes", "shownotes", "viewnotes"]
const noteSynonyms = ["note", "takenote", "setnote", "createnote", "remember"]
const clearNotesSynonyms = ["clearnotes"]
const eraseNoteSynonyms = ["erasenote", "removenote", "deletenote", "cancelnote"]
const takeSynonyms = ["take", "steal", "get", "grab", "receive", "loot"]
const 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"]
const giveSynonyms = ["give", "handover", "hand", "gift"]
const renameItemSynonyms = ["renameitem", "renameobject", "renamegear", "renameequipment"]
const inventorySynonyms = ["inv", "inventory", "backpack", "gear", "showinv", "showinventory", "viewinventory", "viewinv"]
const clearInventorySynonyms = ["clearinventory", "clearinv", "emptyinventory", "emptybackpack", "clearbackpack", "emptygear", "cleargear"]
const learnSpellSynonyms = ["learnspell", "learnmagic", "learnincantation", "learnritual", "memorizespell", "memorizemagic", "memorizeincantation", "memorizeritual", "learnsspell", "learnsmagic", "learnsincantation", "learnsritual", "memorizesspell", "memorizesmagic", "memorizesincantation", "memorizesritual", "learn"]
const forgetSpellSynonyms = ["forgetspell", "forgetmagic", "forgetincantation", "forgetritual", "forgetsspell", "forgetsmagic", "forgetsincantation", "forgetsritual", "deletespell", "deletemagic", "deleteincantation", "deleteritual", "deletesspell", "deletesmagic", "deletesincantation", "deletesritual", "cancelspell", "cancelmagic", "cancelincantation", "cancelritual", "cancelsspell", "cancelsmagic", "cancelsincantation", "cancelsritual", "removespell", "removemagic", "removeincantation", "removeritual", "removesspell", "removesmagic", "removesincantation", "removesritual", "forget"]
const castSpellSynonyms = ["cast", "castspell", "castmagic", "castincantation", "castritual", "castsspell", "castsmagic", "castsincantation", "castsritual"]
const clearSpellsSynonyms = ["clearspells", "clearmagic", "clearincantations", "clearrituals", "forgetallspells", "forgetallmagic", "forgetallincantation", "forgetallritual"]
const spellbookSynonyms = ["spellbook", "spells", "listspells", "showspells", "spelllist", "spellcatalog", "spellinventory"]
const resetSynonyms = ["reset", "cleandata", "cleardata", "resetdata", "resetsettings", "clearsettings", "profile"]
const allSynonyms = ["all", "every", "each", "every one", "everyone"]
const attackSynonyms = ["attack", "strike", "ambush", "assault", "fireat", "fireon"]
const setMeleeStatSynonyms = ["setmeleestat", "setmeleestatistic", "setmeleeability", "changemeleestat", "changemeleestatistic", "changemeleeability"]
const setrangedStatSynonyms = ["setrangedstat", "setrangedstatistic", "setrangedability", "changerangedstat", "changerangedstatistic", "changerangedability"]
const showCharactersSynonyms = ["showcharacters", "showparty", "showteam", "characters", "party", "team"]
const removeCharacterSynonyms = ["removecharacter", "deletecharacter", "erasecharacter"]
const setAutoXpSynonyms = ["setautoxp", "autoxp"]
const showAutoXpSynonyms = ["showautoxp"]
const setDefaultDifficultySynonyms = ["setdefaultdifficulty", "defaultdifficulty", "setdefaultdc", "defaultdc", "setdefaultac", "defaultac", "setdifficulty", "difficulty", "dc"]
const showDefaultDifficultySynonyms = ["showdefaultdifficulty", "showdefaultdc", "showdefaultac"]
const generateNameSynonyms = ["generatename", "name", "randomname", "makename", "createname"]
const createLocationSynonyms = ["createlocation", "makelocation", "generatelocation", "addlocation", "setlocation", "createplace", "makeplace", "generateplace", "addplace", "setplace", "createtown", "maketown", "generatetown", "addtown", "settown", "createvillage", "makevillage", "generatevillage", "addvillage", "setvillage", "createcity", "makecity", "generatecity", "addcity", "setcity", "updatelocation", "updateplace", "updatetown", "updatevillage", "updatecity"]
const goToLocationSynonyms = ["gotolocation", "golocation", "movetolocation", "traveltolocation", "travellocation", "gotoplace", "goplace", "movetoplace", "traveltoplace", "travelplace", "gototown", "gotown", "movetotown", "traveltotown", "traveltown", "gotovillage", "govillage", "movetovillage", "traveltovillage", "travelvillage", "gotocity", "gocity", "movetocity", "traveltocity", "travelcity", "goto", "go", "moveto", "move", "travelto", "travel"]
const removeLocationSynonyms = ["removelocation", "deletelocation", "eraselocation", "removeplace", "deleteplace", "eraseplace", "removetown", "deletetown", "erasetown", "removevillage", "deletevillage", "erasevillage", "removecity", "deletecity", "erasecity"]
const showLocationsSynonyms = ["showlocations", "showplaces", "showtowns", "showvillages", "showcities", "locations", "places", "towns", "villages", "cities"]
const getLocationSynonyms = ["getlocation", "location", "getcoordinates", "coordinates", "getcoords", "coords", "showlocation"]
const clearLocationsSynonyms = ["clearlocations", "eraselocations", "deletelocations", "resetlocations"]
const mapSynonyms = ["map", "showmap"]
const goNorthSynonyms = ["gonorth", "north", "goup", "up", "n"]
const goSouthSynonyms = ["gosouth", "south", "godown", "down", "s"]
const goEastSynonyms = ["goeast", "east", "goright", "right", "e"]
const goWestSynonyms = ["gowest", "west", "goleft", "left", "w"]
const showDaySynonyms = ["showday", "showdate", "day", "date"]
const setDaySynonyms = ["setday", "setdate"]
const encounterSynonyms = ["encounter", "startencounter"]
const showEnemiesSynonyms = ["showenemies", "enemies"]
const 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"]
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) => {
init()
const rawText = text
if (state.createStep != null) {
text = handleCreateStep(text)
if (state.createStep != null) return { text }
else text = rawText
}
if (state.setupEnemyStep != null) {
text = handleSetupEnemyStep(text)
if (state.setupEnemyStep != null) return { 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 }
else text = rawText
}
if (state.stragedyTurn != null) {
text = handleStragedyTurn(text)
if (state.stragedyTurn != null) return { 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 }
}
state.characterName = getCharacterName(rawText)
text = sanitizeText(text)
var lineBreakIndex = text.indexOf("\n")
if (lineBreakIndex > -1) {
state.flavorText = text.substring(lineBreakIndex + 1)
if (!state.flavorText.startsWith(" ")) state.flavorText = " " + state.flavorText
text = text.substring(0, lineBreakIndex)
} else {
state.flavorText = null
}
command = text.substring(text.search(/#/) + 1)
var commandName = getCommandName(command).toLowerCase().replaceAll(/[^a-z0-9\s]*/gi, "")
if (state.characterName == null || !hasCharacter(state.characterName)) {
var found = processCommandSynonyms(command, commandName, createSynonyms, function () {return true})
if (state.characterName == null && found) {
state.show = "none"
text = `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n`
return { text }
}
if (!found) found = processCommandSynonyms(command, commandName, helpSynonyms.concat(rollSynonyms, noteSynonyms, eraseNoteSynonyms, showNotesSynonyms, clearNotesSynonyms, showCharactersSynonyms, removeCharacterSynonyms, generateNameSynonyms, setDefaultDifficultySynonyms, showDefaultDifficultySynonyms, renameCharacterSynonyms, cloneCharacterSynonyms, createLocationSynonyms, showLocationsSynonyms, goToLocationSynonyms, removeLocationSynonyms, getLocationSynonyms, clearLocationsSynonyms, goNorthSynonyms, goSouthSynonyms, goEastSynonyms, goWestSynonyms, encounterSynonyms, showEnemiesSynonyms, 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) {
state.show = "none"
text = `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n`
return { text }
} else {
state.show = "none"
text = `\n[Error: Character ${state.characterName} does not exist. Type #setup to create this character]\n`
return { text }
}
}
}
text = processCommandSynonyms(command, commandName, rollSynonyms, doRoll)
if (text == null) text = processCommandSynonyms(command, commandName, createSynonyms, doCreate)
if (text == null) text = processCommandSynonyms(command, commandName, showCharactersSynonyms, doShowCharacters)
if (text == null) text = processCommandSynonyms(command, commandName, removeCharacterSynonyms, doRemoveCharacter)
if (text == null) text = processCommandSynonyms(command, commandName, bioSynonyms, doBio)
if (text == null) text = processCommandSynonyms(command, commandName, setClassSynonyms, doSetClass)
if (text == null) text = processCommandSynonyms(command, commandName, setSummarySynonyms, doSetSummary)
if (text == null) text = processCommandSynonyms(command, commandName, setHealthSynonyms, doSetHealth)
if (text == null) text = processCommandSynonyms(command, commandName, healSynonyms, doHeal)
if (text == null) text = processCommandSynonyms(command, commandName, damageSynonyms, doDamage)
if (text == null) text = processCommandSynonyms(command, commandName, restSynonyms, doRest)
if (text == null) text = processCommandSynonyms(command, commandName, setExperienceSynonyms, doSetExperience)
if (text == null) text = processCommandSynonyms(command, commandName, addExperienceSynonyms, doAddExperience)
if (text == null) text = processCommandSynonyms(command, commandName, levelUpSynonyms, doLevelUp)
if (text == null) text = processCommandSynonyms(command, commandName, showStatsSynonym, doShowStats)
if (text == null) text = processCommandSynonyms(command, commandName, setStatSynonyms, doSetStat)
if (text == null) text = processCommandSynonyms(command, commandName, setSpellStatSynonyms, doSetSpellStat)
if (text == null) text = processCommandSynonyms(command, commandName, showSkillsSynonyms, doShowSkills)
if (text == null) text = processCommandSynonyms(command, commandName, setSkillSynonyms, doSetSkill)
if (text == null) text = processCommandSynonyms(command, commandName, checkSynonyms, doCheck)
if (text == null) text = processCommandSynonyms(command, commandName, trySynonyms, doTry)
if (text == null) text = processCommandSynonyms(command, commandName, showNotesSynonyms, doShowNotes)
if (text == null) text = processCommandSynonyms(command, commandName, noteSynonyms, doNote)
if (text == null) text = processCommandSynonyms(command, commandName, clearNotesSynonyms, doClearNotes)
if (text == null) text = processCommandSynonyms(command, commandName, eraseNoteSynonyms, doEraseNote)
if (text == null) text = processCommandSynonyms(command, commandName, takeSynonyms, doTake)
if (text == null) text = processCommandSynonyms(command, commandName, dropSynonyms, doDrop)
if (text == null) text = processCommandSynonyms(command, commandName, giveSynonyms, doGive)
if (text == null) text = processCommandSynonyms(command, commandName, renameItemSynonyms, doRenameItem)
if (text == null) text = processCommandSynonyms(command, commandName, inventorySynonyms, doInventory)
if (text == null) text = processCommandSynonyms(command, commandName, clearInventorySynonyms, doClearInventory)
if (text == null) text = processCommandSynonyms(command, commandName, learnSpellSynonyms, doLearnSpell)
if (text == null) text = processCommandSynonyms(command, commandName, forgetSpellSynonyms, doForgetSpell)
if (text == null) text = processCommandSynonyms(command, commandName, castSpellSynonyms, doCastSpell)
if (text == null) text = processCommandSynonyms(command, commandName, clearSpellsSynonyms, doClearSpells)
if (text == null) text = processCommandSynonyms(command, commandName, spellbookSynonyms, doSpellbook)
if (text == null) text = processCommandSynonyms(command, commandName, removeStatSynonyms, doRemoveStat)
if (text == null) text = processCommandSynonyms(command, commandName, clearStatsSynonyms, doClearStats)
if (text == null) text = processCommandSynonyms(command, commandName, removeSkillSynonyms, doRemoveSkill)
if (text == null) text = processCommandSynonyms(command, commandName, clearSkillsSynonyms, doClearSkills)
if (text == null) text = processCommandSynonyms(command, commandName, attackSynonyms, doAttack)
if (text == null) text = processCommandSynonyms(command, commandName, setMeleeStatSynonyms, doSetMeleeStat)
if (text == null) text = processCommandSynonyms(command, commandName, setrangedStatSynonyms, doSetRangedStat)
if (text == null) text = processCommandSynonyms(command, commandName, buySynonyms, doBuy)
if (text == null) text = processCommandSynonyms(command, commandName, sellSynonyms, doSell)
if (text == null) text = processCommandSynonyms(command, commandName, resetSynonyms, doReset)
if (text == null) text = processCommandSynonyms(command, commandName, setAutoXpSynonyms, doSetAutoXp)
if (text == null) text = processCommandSynonyms(command, commandName, showAutoXpSynonyms, doShowAutoXp)
if (text == null) text = processCommandSynonyms(command, commandName, setDefaultDifficultySynonyms, doSetDefaultDifficulty)
if (text == null) text = processCommandSynonyms(command, commandName, showDefaultDifficultySynonyms, doShowDefaultDifficulty)
if (text == null) text = processCommandSynonyms(command, commandName, generateNameSynonyms, doGenerateName)
if (text == null) text = processCommandSynonyms(command, commandName, createLocationSynonyms, doCreateLocation)
if (text == null) text = processCommandSynonyms(command, commandName, goToLocationSynonyms, doGoToLocation)
if (text == null) text = processCommandSynonyms(command, commandName, clearLocationsSynonyms, doClearLocations)
if (text == null) text = processCommandSynonyms(command, commandName, removeLocationSynonyms, doRemoveLocation)
if (text == null) text = processCommandSynonyms(command, commandName, showLocationsSynonyms, doShowLocations)
if (text == null) text = processCommandSynonyms(command, commandName, getLocationSynonyms, doGetLocation)
if (text == null) text = processCommandSynonyms(command, commandName, mapSynonyms, doMap)
if (text == null) text = processCommandSynonyms(command, commandName, goNorthSynonyms, doGoNorth)
if (text == null) text = processCommandSynonyms(command, commandName, goSouthSynonyms, doGoSouth)
if (text == null) text = processCommandSynonyms(command, commandName, goEastSynonyms, doGoEast)
if (text == null) text = processCommandSynonyms(command, commandName, goWestSynonyms, doGoWest)
if (text == null) text = processCommandSynonyms(command, commandName, renameCharacterSynonyms, doRenameCharacter)
if (text == null) text = processCommandSynonyms(command, commandName, cloneCharacterSynonyms, doCloneCharacter)
if (text == null) text = processCommandSynonyms(command, commandName, showDaySynonyms, doShowDay)
if (text == null) text = processCommandSynonyms(command, commandName, setDaySynonyms, doSetDay)
if (text == null) text = processCommandSynonyms(command, commandName, versionSynonyms, doVersion)
if (text == null) text = processCommandSynonyms(command, commandName, setAcSynonyms, doSetAc)
if (text == null) text = processCommandSynonyms(command, commandName, encounterSynonyms, doEncounter)
if (text == null) text = processCommandSynonyms(command, commandName, showEnemiesSynonyms, doShowEnemies)
if (text == null) text = processCommandSynonyms(command, commandName, 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)
if (text == null) text = processCommandSynonyms(command, commandName, blockSynonyms, doBlock)
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, 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()
var statNames = []
character.stats.forEach(x => {
statNames.push(x.name.toLowerCase())
})
character.skills.forEach(x => {
statNames.push(x.name.toLowerCase())
})
text = processCommandSynonyms(command, commandName, statNames, doFlipCommandAbility)
}
if (state.flavorText != null) text += state.flavorText
return { text }
}
function handleCreateStep(text) {
state.show = "create"
if (/^\s*>.*says? ".*/.test(text)) {
text = text.replace(/^\s*>.*says? "/, "")
text = text.replace(/"\s*$/, "")
} else if (/^\s*>\s.*/.test(text)) {
text = text.replace(/\s*> /, "")
for (var i = 0; i < info.characters.length; i++) {
var matchString = info.characters[i] == "" ? "You " : `${info.characters[i]} `
if (text.startsWith(matchString)) {
text = text.replace(matchString, "")
break
}
}
text = text.replace(/\.?\s*$/, "")
} else {
text = text.replace(/^\s+/, "")
}
if (text.toLowerCase() == "q") {
state.createStep = null
return text
}
switch (state.createStep) {
case 0:
text = text.toLowerCase();
if (text.startsWith("y")) state.createStep = 100
else if (text.startsWith("n")) state.createStep++
break
case 1:
if (text.length > 0) {
state.tempCharacter.className = text
state.createStep++
state.statDice = []
for (var i = 0; i < 6; i++) {
var dice = []
for (var j = 0; j < 4; j++) {
dice.push(parseInt(calculateRoll("d6")))
}
dice.sort(function(a, b) {
return b - a;
});
dice.splice(3, 1)
state.statDice.push(dice[0] + dice[1] + dice[2])
}
state.statDice.sort(function(a, b) {
return b - a
})
}
return text
break
case 2:
if (text.length > 0) {
var choices = text.split(/\D+/)
choices = [...new Set(choices)];
if (choices.length != 6) break
for (var i = 0; i < 6; i++) {
const stat = {
name: "temp",
value: state.statDice[i]
}
switch (parseInt(choices[i])) {
case 1:
stat.name = "Strength"
break
case 2:
stat.name = "Dexterity"
break
case 3:
stat.name = "Constitution"
break
case 4:
stat.name = "Intelligence"
break
case 5:
stat.name = "Wisdom"
break
case 6:
stat.name = "Charisma"
break
default:
return text
}
state.tempCharacter.stats.push(stat)
}
state.createStep++
}
return text
case 3:
if (text.length == 0) state.createStep++
if (!isNaN(text)) {
switch (parseInt(text)) {
case 1:
state.tempCharacter.spellStat = "Intelligence"
break
case 2:
state.tempCharacter.spellStat = "Wisdom"
break
case 3:
state.tempCharacter.spellStat = "Charisma"
break
case 4:
state.tempCharacter.spellStat = null
}
state.createStep++
}
return text
case 4:
if (text.length > 0) {
state.tempCharacter.summary = text
state.createStep = 500
}
return text
case 100:
if (!isNaN(text)) {
state.createStep = 500
switch (parseInt(text)) {
case 1:
state.tempCharacter.className = "Fighter"
state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 9}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 13}, {name: "Charisma", value: 14}]
state.tempCharacter.inventory.push({name: "Greatsword", quantity: 1}, {name: "Javelin", quantity: 2})
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2;
state.tempCharacter.summary = "A skilled melee warrior specializing in weapons and armor."
break
case 2:
state.tempCharacter.className = "Cleric"
state.tempCharacter.stats = [{name: "Strength", value: 14}, {name: "Dexterity", value: 12}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 18}, {name: "Charisma", value: 14}]
state.tempCharacter.inventory.push({name: "Mace", quantity: 1}, {name: "Light Crossbow", quantity: 1}, {name: "Bolts", quantity: 10})
state.tempCharacter.spells = ["Spiritual Weapon", "Mass Healing Word"]
state.tempCharacter.spellStat = "Wisdom"
state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Medicine").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2;
state.tempCharacter.summary = "A follower of a deity that can call on divine power."
break
case 3:
state.tempCharacter.className = "Rogue"
state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 16}, {name: "Constitution", value: 12}, {name: "Intelligence", value: 13}, {name: "Wisdom", value: 10}, {name: "Charisma", value: 16}]
state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Dagger", quantity: 1}, {name: "Hand Crossbow", quantity: 1}, {name: "Bolts", quantity: 10})
state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Investigation").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2;
state.tempCharacter.summary = "An expert in stealth, subterfuge, and exploitation."
break
case 4:
state.tempCharacter.className = "Ranger"
state.tempCharacter.stats = [{name: "Strength", value: 12}, {name: "Dexterity", value: 17}, {name: "Constitution", value: 13}, {name: "Intelligence", value: 10}, {name: "Wisdom", value: 15}, {name: "Charisma", value: 8}]
state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Longbow", quantity: 1}, {name: "Arrows", quantity: 20})
state.tempCharacter.skills.find((element) => element.name == "Animal Handling").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Nature").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Survival").modifier = 2;
state.tempCharacter.summary = "A talented hunter adept in tracking, survival, and animal handling."
break
case 5:
state.tempCharacter.className = "Barbarian"
state.tempCharacter.stats = [{name: "Strength", value: 17}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 8}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 10}]
state.tempCharacter.inventory.push({name: "Greataxe", quantity: 1}, {name: "Javelin", quantity: 1})
state.tempCharacter.skills.find((element) => element.name == "Animal Handling").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Intimidation").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.summary = "Combat expert focused on brute strength and raw fury."
break
case 6:
state.tempCharacter.className = "Bard"
state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 15}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 13}, {name: "Wisdom", value: 10}, {name: "Charisma", value: 15}]
state.tempCharacter.inventory.push({name: "Rapier", quantity: 1}, {name: "Lute", quantity: 1})
state.tempCharacter.spells = ["Vicious Mockery", "Charm Person", "Healing Word"]
state.tempCharacter.spellStat = "Charisma"
state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2;
state.tempCharacter.summary = "A musician that can transform song and word into magic."
break
case 7:
state.tempCharacter.className = "Druid"
state.tempCharacter.stats = [{name: "Strength", value: 11}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 16}, {name: "Intelligence", value: 14}, {name: "Wisdom", value: 16}, {name: "Charisma", value: 9}]
state.tempCharacter.spells = ["Druidcraft", "Animal Friendship", "Healing Word"]
state.tempCharacter.spellStat = "Wisdom"
state.tempCharacter.inventory.push({name: "Quarterstaff", quantity: 1}, {name: "Small Knife", quantity: 1})
state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Medicine").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Nature").modifier = 2;
state.tempCharacter.summary = "Commands the natural world to cast spells and harness its power."
break
case 8:
state.tempCharacter.className = "Monk"
state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 14}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 8}, {name: "Wisdom", value: 17}, {name: "Charisma", value: 10}]
state.tempCharacter.inventory.push({name: "Dart", quantity: 5}, {name: "Shortsword", quantity: 1})
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Sleight of Hand").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Stealth").modifier = 2;
state.tempCharacter.summary = "A martial artist who has mastered melee and unarmed combat."
break
case 9:
state.tempCharacter.className = "Paladin"
state.tempCharacter.stats = [{name: "Strength", value: 16}, {name: "Dexterity", value: 9}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 13}, {name: "Charisma", value: 14}]
state.tempCharacter.spells = ["Thunderous Smite", "Divine Favor", "Cure Wounds"]
state.tempCharacter.spellStat = "Charisma"
state.tempCharacter.inventory.push({name: "Longsword", quantity: 1}, {name: "Javelin", quantity: 2})
state.tempCharacter.skills.find((element) => element.name == "Athletics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2;
state.tempCharacter.summary = "A virtuous holy warrior with expertise in armor and mysticism."
break
case 10:
state.tempCharacter.className = "Wizard"
state.tempCharacter.stats = [{name: "Strength", value: 10}, {name: "Dexterity", value: 15}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 16}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 8}]
state.tempCharacter.inventory.push({name: "Quarterstaff", quantity: 1}, {name: "Spellbook", quantity: 1})
state.tempCharacter.spells = ["Fire Bolt", "Mage Hand", "Magic Missile"]
state.tempCharacter.spellStat = "Intelligence"
state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Insight").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Investigation").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2;
state.tempCharacter.summary = "An expert in magic ability who found their power through arcane knowledge."
break
case 11:
state.tempCharacter.className = "Sorcerer"
state.tempCharacter.stats = [{name: "Strength", value: 8}, {name: "Dexterity", value: 16}, {name: "Constitution", value: 13}, {name: "Intelligence", value: 11}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 15}]
state.tempCharacter.inventory.push({name: "Dagger", quantity: 1}, {name: "Bag of Holding", quantity: 1})
state.tempCharacter.spells = ["Ray of Frost", "Minor Illusion", "Shield"]
state.tempCharacter.spellStat = "Charisma"
state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Intimidation").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Perception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2;
state.tempCharacter.summary = "A masterful spellcaster deriving their power from an innate source."
break
case 12:
state.tempCharacter.className = "Warlock"
state.tempCharacter.stats = [{name: "Strength", value: 9}, {name: "Dexterity", value: 13}, {name: "Constitution", value: 15}, {name: "Intelligence", value: 14}, {name: "Wisdom", value: 11}, {name: "Charisma", value: 16}]
state.tempCharacter.spells = ["Eldritch Blast", "Witch Bolt", "Thunderwave"]
state.tempCharacter.spellStat = "Charisma"
state.tempCharacter.inventory.push({name: "Dagger", quantity: 1}, {name: "Orb", quantity: 1})
state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Deception").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "History").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Religion").modifier = 2;
state.tempCharacter.summary = "A magic user granted ability by a pact with a powerful patron."
break
case 13:
state.tempCharacter.className = "Artificer"
state.tempCharacter.stats = [{name: "Strength", value: 10}, {name: "Dexterity", value: 14}, {name: "Constitution", value: 14}, {name: "Intelligence", value: 17}, {name: "Wisdom", value: 12}, {name: "Charisma", value: 8}]
state.tempCharacter.inventory.push({name: "Shortsword", quantity: 1}, {name: "Hand Crossbow", quantity: 1}, {name: "Bolts", quantity: 20})
state.tempCharacter.skills.find((element) => element.name == "Acrobatics").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Performance").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Persuasion").modifier = 2;
state.tempCharacter.skills.find((element) => element.name == "Arcana").modifier = 2;
state.tempCharacter.summary = "An inventor and alchemist capable of imbuing objects with magic."
break
}
}
return text
case 500:
state.show = null
state.createStep = null
var character = getCharacter(state.tempCharacter.name)
character.className = state.tempCharacter.className
character.experience = 0
character.stats = [...state.tempCharacter.stats]
character.inventory = [...state.tempCharacter.inventory]
character.skills = [...state.tempCharacter.skills]
character.spells = [...state.tempCharacter.spells]
character.health = getHealthMax()
character.spellStat = state.tempCharacter.spellStat
character.meleeStat = state.tempCharacter.meleeStat
character.rangedStat = state.tempCharacter.rangedStat
character.summary = state.tempCharacter.summary
break
}
return text
}
function handleSetupEnemyStep(text) {
state.show = "setupEnemy"
if (/^\s*>.*says? ".*/.test(text)) {
text = text.replace(/^\s*>.*says? "/, "")
text = text.replace(/"\s*$/, "")
} else if (/^\s*>\s.*/.test(text)) {
text = text.replace(/\s*> /, "")
for (var i = 0; i < info.characters.length; i++) {
var matchString = info.characters[i] == "" ? "You " : `${info.characters[i]} `
if (text.startsWith(matchString)) {
text = text.replace(matchString, "")
break
}
}
text = text.replace(/\.?\s*$/, "")
} else {
text = text.replace(/^\s+/, "")
}
if (text.toLowerCase() == "q") {
state.setupEnemyStep = null
return text
}
switch (state.setupEnemyStep) {
case 0:
text = text.toLowerCase();
if (text.startsWith("y")) state.setupEnemyStep = 100
else if (text.startsWith("n")) state.setupEnemyStep++
break
case 1:
if (text.length > 0) {
state.tempEnemy.name = text
state.setupEnemyStep++
}
return text
case 2:
if (text.length > 0) {
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) {
state.tempEnemy.health = calculateRoll(text)
state.setupEnemyStep++
} else if (!isNaN(text)) {
state.tempEnemy.health = Math.max(0, parseInt(text))
state.setupEnemyStep++
}
}
return text
case 3:
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) {
state.tempEnemy.ac = calculateRoll(text)
state.setupEnemyStep++
} else if (!isNaN(text)) {
state.tempEnemy.ac = Math.max(0, parseInt(text))
state.setupEnemyStep++
}
return text
case 4:
if (!isNaN(text)) {
state.tempEnemy.hitModifier = Math.max(0, parseInt(text))
state.setupEnemyStep++
}
return text
case 5:
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) {
state.tempEnemy.damage = text
state.setupEnemyStep++
} else if (!isNaN(text)) {
state.tempEnemy.damage = Math.max(0, parseInt(text))
state.setupEnemyStep++
}
return text
case 6:
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(text)) {
state.tempEnemy.initiative = calculateRoll(text)
state.setupEnemyStep++
} else if (!isNaN(text)) {
state.tempEnemy.initiative = Math.max(0, parseInt(text))
state.setupEnemyStep++
}
return text
case 7:
if (text.toLowerCase() == "s") {
state.setupEnemyStep = 500
}
else if (text.length > 0) {
state.tempEnemy.spells.push(text)
state.setupEnemyStep++
}
return text
case 8:
if (text.toLowerCase() == "s") {
state.setupEnemyStep = 500
}
else if (text.length > 0) {
state.tempEnemy.spells.push(text)
}
return text
case 100:
if (/^\d+(\s.*)?$/gi.test(text)) {
state.setupEnemyStep = 500
var value = text.match(/^\d+/gi)[0]
var nameMatches = text.match(/(?<=\s).*/gi)
switch (parseInt(value)) {
case 1:
state.tempEnemy = createEnemy("Animated Armor", calculateRoll("6d8+6"), 18, 6, "1d6+2", "d20")
break
case 2:
state.tempEnemy = createEnemy("Awakened Shrub", calculateRoll("2d+6"), 9, -3, "1d4-1", "d20-1")
break
case 3:
state.tempEnemy = createEnemy("Brigand", calculateRoll("5d8+10"), 11, 6, "1d6+2", "d20")
break
case 4:
state.tempEnemy = createEnemy("Black Bear", calculateRoll("3d8+6"), 11, 6, "2d4+2", "d20")
break
case 5:
state.tempEnemy = createEnemy("Boar", calculateRoll("2d8+2"), 11, 4, "1d6+1", "d20")
break
case 6:
state.tempEnemy = createEnemy("Cockatrice", calculateRoll("6d6+6"), 11, 1, "1d4+1", "d20+1", "Petrifying Bite1d4+1")
break
case 7:
state.tempEnemy = createEnemy("Snake", calculateRoll("2d10+2"), 12, 6, "1d8+2", "d20+2", "Poison Bite2d4+1")
break
case 8:
state.tempEnemy = createEnemy("Dire Wolf", calculateRoll("5d10+10"), 14, 8, "2d6+3", "d20+2")
break
case 9:
state.tempEnemy = createEnemy("Ghoul", calculateRoll("5d8"), 12, 3, "2d6+2", "d20+2")
break
case 10:
state.tempEnemy = createEnemy("Giant Centipede", calculateRoll("1d6+1"), 13, 1, "1d4+2", "d20+2")
break
case 11:
state.tempEnemy = createEnemy("Giant Rat", calculateRoll("2d6"), 12, 2, "1d4+2", "d20+2")
break
case 12:
state.tempEnemy = createEnemy("Giant Wolf Spider", calculateRoll("2d8+2"), 13, 4, "1d6+1", "d20+3", "Poison Bite1d6+8")
break
case 13:
state.tempEnemy = createEnemy("Gnoll", calculateRoll("5d8"), 15, 6, "1d8+2", "d20+1")
break
case 14:
state.tempEnemy = createEnemy("Goblin", calculateRoll("2d6"), 15, 3, "1d6+2", "d20+2")
break
case 15:
state.tempEnemy = createEnemy("Harpy", calculateRoll("7d8+7"), 11, 4, "2d4+1", "d20+1", "Luring Song")
break
case 16:
state.tempEnemy = createEnemy("Hobgoblin", calculateRoll("2d8+2"), 18, 4, "1d8+1", "d20+1")
break
case 17:
state.tempEnemy = createEnemy("Kobold", calculateRoll("2d6-2"), 12, 2, "1d4+2", "d20+2")
break
case 18:
state.tempEnemy = createEnemy("Orc", calculateRoll("2d8+6"), 13, 8, "1d12+3", "d20+1")
break
case 19:
state.tempEnemy = createEnemy("Satyr", calculateRoll("5d8"), 15, 4, "1d8+2", "d20+3")
break
case 20:
state.tempEnemy = createEnemy("Skeleton", calculateRoll("2d8+4"), 13, 5, "1d6+2", "d20+2")
break
case 21:
state.tempEnemy = createEnemy("Stirge", calculateRoll("1d4"), 14, 2, "1d4+3", "d20+1", "Blood Drain2d4+6")
break
case 22:
state.tempEnemy = createEnemy("Warhorse", calculateRoll("3d10+3"), 11, 10, "2d6+4", "d20+1", "Charge")
break
case 23:
state.tempEnemy = createEnemy("Wolf", calculateRoll("2d+2"), 13, 6, "2d4+2", "d20+2")
break
case 24:
state.tempEnemy = createEnemy("Worg", calculateRoll("4d10+4"), 13, 8, "2d6+3", "d20+1")
break
case 25:
state.tempEnemy = createEnemy("Zombie", calculateRoll("3d8+9"), 8, 4, "1d6+1", "d20-2")
break
case 26:
state.tempEnemy = createEnemy("Air Elemental", calculateRoll("12d10+24"), 15, 8, "2d8+5", "d20+5", "Whirlwind3d8+2")
break
case 27:
state.tempEnemy = createEnemy("Basilisk", calculateRoll("8d8+16"), 15, 5, "2d6+3", "d20-1")
break
case 28:
state.tempEnemy = createEnemy("Berserker", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1")
break
case 29:
state.tempEnemy = createEnemy("Chuul", calculateRoll("11d10+33"), 16, 6, "2d6+4", "d20", "Tentacles")
break
case 30:
state.tempEnemy = createEnemy("Doppelganger", calculateRoll("8d8+16"), 14, 6, "1d6+4", "d20+4", "Shapechange")
break
case 31:
state.tempEnemy = createEnemy("Druid", calculateRoll("5d8+5"), 11, 4, "1d8", "d20+1", "Produce Flame3d6", "Bark Skin", "Entangle")
break
case 32:
state.tempEnemy = createEnemy("Earth Elemental", calculateRoll("12d10+60"), 17, 8, "2d8+5", "d20-1", "Earth Glide")
break
case 33:
state.tempEnemy = createEnemy("Fire Elemental", calculateRoll("12d10+36"), 13, 6, "2d6+3", "d20+3", "Fire Form")
break
case 34:
state.tempEnemy = createEnemy("Gorgon", calculateRoll("12d8+48"), 19, 8, "2d12+5", "d20", "Petrifying Breath")
break
case 35:
state.tempEnemy = createEnemy("Green Hag", calculateRoll("11d8+33"), 17, 6, "2d8+4", "d20+1", "Minor Illusion", "Invisible Passage")
break
case 36:
state.tempEnemy = createEnemy("Griffon", calculateRoll("7d10+21"), 12, 6, "1d8+4", "d20+2")
break
case 37:
state.tempEnemy = createEnemy("Hell Hound", calculateRoll("7d8+14"), 15, 5, "1d8+3", "d20+1", "Fire Breath6d6")
break
case 38:
state.tempEnemy = createEnemy("Hill Giant", calculateRoll("10d12+40"), 13, 8, "3d8+5", "d20-1", "Throw Rock3d10+5")
break
case 39:
state.tempEnemy = createEnemy("Manticore", calculateRoll("8d10+24"), 14, 5, "1d8+3", "d20+3")
break
case 40:
state.tempEnemy = createEnemy("Minotaur", calculateRoll("9d10+27"), 14, 6, "2d12+4", "d20")
break
case 41:
state.tempEnemy = createEnemy("Mimic", calculateRoll("9d8+18"), 12, 5, "1d8+3", "d20+1", "Grapple")
break
case 42:
state.tempEnemy = createEnemy("Ogre", calculateRoll("7d10+21"), 11, 6, "2d8+4", "d20-1")
break
case 43:
state.tempEnemy = createEnemy("Owlbear", calculateRoll("7d10+21"), 13, 7, "1d10+5", "initiative")
break
case 44:
state.tempEnemy = createEnemy("Red Dragon Wyrmling", calculateRoll("10d8+30"), 17, 6, "1d10+4", "d20")
break
case 45:
state.tempEnemy = createEnemy("Spectator", calculateRoll("6d8+12"), 14, 1, "1d6-1", "d20+2", "Confusion Ray", "Paralyzing Ray", "Fear Ray", "Wounding Ray3d10")
break
case 46:
state.tempEnemy = createEnemy("Troll", calculateRoll("8d10+40"), 15, 7, "1d6+4", "d20+1")
break
case 47:
state.tempEnemy = createEnemy("Wererat", calculateRoll("6d8+6"), 12, 4, "1d4+2", "d20+2")
break
case 48:
state.tempEnemy = createEnemy("Werewolf", calculateRoll("9d8+18"), 12, 4, "18+2", "d20+1")
break
case 49:
state.tempEnemy = createEnemy("Vampire Spawn", calculateRoll("11d8+33"), 15, 6, "2d4+3", "d20+3", "Bite3d6+3")
break
case 50:
state.tempEnemy = createEnemy("Wight", calculateRoll("6d8+18"), 14, 4, "1d8+2", "d20+1", "Life Drain4d6+3")
break
case 51:
state.tempEnemy = createEnemy("Aboleth", calculateRoll("18d10"), 17, 9, "6d6+15", "d20-1", "Enslave", "Psychic Drain3d6")
break
case 52:
state.tempEnemy = createEnemy("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3")
break
case 53:
state.tempEnemy = createEnemy("Chimera", calculateRoll("12d10+48"), 14, 7, "2d6+4", "d20", "Fire Breath7d8")
break
case 54:
state.tempEnemy = createEnemy("Cloud Giant", calculateRoll("16d12+96"), 14, 12, "6d8+16", "d20", "Throw Rock4d10+8", "Control Weather")
break
case 55:
state.tempEnemy = createEnemy("Cyclops", calculateRoll("12d12+60"), 14, 9, "3d8+6", "d20")
break
case 56:
state.tempEnemy = createEnemy("Deva", calculateRoll("16d8+64"), 17, 8, "2d6+8", "d20+4")
break
case 57:
state.tempEnemy = createEnemy("Drider", calculateRoll("13d10+52"), 19, 6, "3d8", "1d10+3", "Poison Bite2d8")
break
case 58:
state.tempEnemy = createEnemy("Frost Giant", calculateRoll("12d12+60"), 15, 9, "6d12+12", "d20-1")
break
case 59:
state.tempEnemy = createEnemy("Hydra", calculateRoll("15d12+75"), 15, 8, "3d10+15", "d20+1")
break
case 60:
state.tempEnemy = createEnemy("Insane Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Cone of Cold8d8", "Greater Invisibility", "Fireball8d6", "Shield")
break
case 61:
state.tempEnemy = createEnemy("Medusa", calculateRoll("17d8+51"), 15, 5, "1d6+2", "d20+2", "Petrifying Gaze", "Snake Hair5d6")
break
case 62:
state.tempEnemy = createEnemy("Shield Guardian", calculateRoll("15d10+60"), 17, 7, "4d6+4", "d20-1", "Shield")
break
case 63:
state.tempEnemy = createEnemy("Spirit Naga", calculateRoll("10d10+20"), 15, 7, "8d8+4", "d20+3", "Dominate Person", "Lightning Bolt9d6")
break
case 64:
state.tempEnemy = createEnemy("Stone Golem", calculateRoll("17d10+85"), 17, 10, "6d8+12", "d20-1")
break
case 65:
state.tempEnemy = createEnemy("Treant", calculateRoll("12d12+60"), 16, 10, "6d6+12", "d20-1")
break
case 66:
state.tempEnemy = createEnemy("Young Black Dragon", calculateRoll("15d10+45"), 18, 7, "4d6+8", "d20+2", "Acid Breath11d8")
break
case 67:
state.tempEnemy = createEnemy("Young Blue Dragon", calculateRoll("16d10+64"), 18, 9, "12d6+10", "d20", "Lightning Breath10d10")
break
case 68:
state.tempEnemy = createEnemy("Young Brass Dragon", calculateRoll("13d10+39"), 17, 7, "2d10+4", "d20", "Fire Breath12d6", "Sleep Breath")
break
case 69:
state.tempEnemy = createEnemy("Young Bronze Dragon", calculateRoll("15d10+60"), 18, 8, "4d6+10", "d20+1", "Lightning Breath10d10", "Repulsion Breath")
break
case 70:
state.tempEnemy = createEnemy("Young Copper Dragon", calculateRoll("14d10+42"), 17, 7, "4d6+8", "d20+1", "Acid Breath9d8", "Slowing Breath")
break
case 71:
state.tempEnemy = createEnemy("Young Gold Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20+2", "Fire Breath10d10", "Weakening Breath")
break
case 72:
state.tempEnemy = createEnemy("Young Green Dragon", calculateRoll("16d10+48"), 18, 7, "4d6+8", "d20+1", "Poison Breath12d6")
break
case 73:
state.tempEnemy = createEnemy("Young Red Dragon", calculateRoll("17d10+85"), 18, 10, "4d6+12", "d20", "Fire Breath16d6")
break
case 74:
state.tempEnemy = createEnemy("Young Silver Dragon", calculateRoll("16d10+8-"), 18, 10, "4d6+12", "d20", "Cold Breath12d8", "Paralyzing Breath")
break
case 75:
state.tempEnemy = createEnemy("Young White Dragon", calculateRoll("14d10+56"), 17, 7, "2d10+4", "d20", "Cold Breath10d8", "Ice Walk")
break
case 76:
state.tempEnemy = createEnemy("Adult Black Dragon", calculateRoll("17d12+85"), 19, 11, "6d6+18", "d20+2", "Acid Breath12d8", "Frightful Presence", "Wing Attack2d6+6")
break
case 77:
state.tempEnemy = createEnemy("Adult Bronze Dragon", calculateRoll("17d12+102"), 19, 12, "6d6+21", "d20", "Repulsion Breath", "Lightning Breath12d10", "Wing Attack2d6+6")
break
case 78:
state.tempEnemy = createEnemy("Adult Copper Dragon", calculateRoll("16d12+80"), 18, 11, "6d6+18", "d20+1", "Acid Breath12d8", "Slowing Breath", "Wing Attack2d6+6")
break
case 79:
state.tempEnemy = createEnemy("Adult Green Dragon", calculateRoll("18d12+90"), 19, 11, "6d6+18", "d20+1", "Poison Breath16d6")
break
case 80:
state.tempEnemy = createEnemy("Animated Statue", calculateRoll("10d12+20"), 17, 7, "2d10+4", "d20-2")
break
case 81:
state.tempEnemy = createEnemy("Arch Mage", calculateRoll("18d8+18"), 12, 4, "1d4+2", "d20+14", "Time Stop", "Globe of Invulnerability", "Lightning Bolt8d6", "Banishment", "Cone of Cold8d8", "Teleport")
break
case 82:
state.tempEnemy = createEnemy("Behir", calculateRoll("16d12+64"), 17, 10, "5d10+12", "d20+3", "Lightning Breath12d10", "Swallow6d6", "Constrict2d10+6")
break
case 83:
state.tempEnemy = createEnemy("Boneclaw", calculateRoll("17d10+34"), 16, 8, "6d10+8", "d20+3", "Shadow Jump5d12+2", "Deadly Reach")
break
case 84:
state.tempEnemy = createEnemy("Deathwolf", calculateRoll("18d8+72"), 15, 10, "6d8+15", "d20+3", "Phantom Deathwolf6d6")
break
case 85:
state.tempEnemy = createEnemy("Djinni", calculateRoll("14d10+84"), 17, 9, "2d6+8", "d20+2")
break
case 86:
state.tempEnemy = createEnemy("Drow Inquisitor", calculateRoll("23d8+46"), 16, 10, "12d8+24", "d20+2", "Spectral Dagger1d8+5")
break
case 87:
state.tempEnemy = createEnemy("Efreeti", calculateRoll("16d10+112"), 17, 10, "4d6+12", "d20+1", "Hurl Flame5d6")
break
case 88:
state.tempEnemy = createEnemy("Elder Brain", calculateRoll("20d10+100"), 10, 7, "5d8+7", "d20", "Mind Blast5d10+5")
break
case 89:
state.tempEnemy = createEnemy("Erinyes", calculateRoll("18d8+72"), 18, 8, "1d10+4", "d20+3")
break
case 90:
state.tempEnemy = createEnemy("Ice Devil", calculateRoll("19d10+76"), 18, 10, "6d4+15", "d20+2", "Wall of Ice")
break
case 91:
state.tempEnemy = createEnemy("Jabberwock", calculateRoll("10d12+50"), 18, 10, "6d10+10", "d20+1", "Regenderation")
break
case 92:
state.tempEnemy = createEnemy("Megapede", calculateRoll("13d20+39"), 15, 10, "6d10+12", "d20", "LifeDrain3d10", "Psychic Bomb5d8")
break
case 93:
state.tempEnemy = createEnemy("Mummy Lord", calculateRoll("13d8+39"), 17, 9, "3d6+4", "d20", "Hold Person", "Silence", "Harm14d6", "Blinding Dust", "Whirlwind of Sand")
break
case 94:
state.tempEnemy = createEnemy("Purple Worm", calculateRoll("15d20+90"), 18, 14, "6d6+18", "d20-2", "Tail Stinger12d6+19")
break
case 95:
state.tempEnemy = createEnemy("Remorhaz", calculateRoll("17d12+85"), 17, 11, "6d10+7", "d20+1", "Swallow6d6")
break
case 96:
state.tempEnemy = createEnemy("Skull Lord", calculateRoll("15d8+45"), 18, 8, "24d6", "d20+3", "Deathly Ray5d8+5")
break
case 97:
state.tempEnemy = createEnemy("Spider Dragon", calculateRoll("15d10+5"), 23, 9, "3d12+12", "d20+8", "Silk Spit", "Spider Breath7d10")
break
case 98:
state.tempEnemy = createEnemy("Storm Giant", calculateRoll("20d12+100"), 16, 14, "12d6+18", "d20+2", "Control Weather", "Lightning Strike12d8")
break
case 99:
state.tempEnemy = createEnemy("Vampire", calculateRoll("17d8+68"), 16, 9, "3d8+8", "d20+4", "Charm", "Shape Change")
break
case 100:
state.tempEnemy = createEnemy("Zikran", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Cone of Cold 8d8", "Lightning Bolt 8d6")
break
case 101:
state.tempEnemy = createEnemy("Ancient Black Dragon", calculateRoll("21d20+147"), 22, 15, "6d10+24", "d20+2", "Acid Breath15d8", "Wing Attack2d6+8")
break
case 102:
state.tempEnemy = createEnemy("Adult Blue Dragon", calculateRoll("18d12+108"), 19, 12, "6d10+21", "d20", "Lightning Breath12d10", "Wing Attack2d6+7")
break
case 103:
state.tempEnemy = createEnemy("Adult Gold Dragon", calculateRoll("19d12+133"), 19, 12, "6d10+21", "d20+2", "Fire Breath 12d10", "Weakening Breath")
break
case 104:
state.tempEnemy = createEnemy("Adult Silver Dragon", calculateRoll("18d12+126"), 19, 13, "6d10+24", "d20", "Cold Breath13d8", "Paralyzing Breath")
break
case 105:
state.tempEnemy = createEnemy("Ancient Gold Dragon", calculateRoll("28d20+252"), 22, 15, "6d10+24", "d20+2", "Fire Breath13d10", "Weakening Breath")
break
case 106:
state.tempEnemy = createEnemy("Ancient Red Dragon", calculateRoll("21d20+147"), 22, 15, "6d8+30", "d20+2", "Fire Breath26d6", "Wing Attack2d6+10")
break
case 107:
state.tempEnemy = createEnemy("Androsphinx", calculateRoll("19d10+95"), 17, 12, "4d10+12", "d20", "Flame Strike8d6", "Roar", "Teleport")
break
case 108:
state.tempEnemy = createEnemy("Bael", calculateRoll("18d10+90"), 18, 13, "4d8+27", "d20+3", "Awaken Greed", "Teleport", "Regenerate", "Inflict Wounds4d8+27", "Invisibility")
break
case 109:
state.tempEnemy = createEnemy("Balor", calculateRoll("21d12+136"), 19, 14, "6d8+16", "d20+2", "Fire Whip 5d6+8", "Teleport")
break
case 110:
state.tempEnemy = createEnemy("Baphomet", calculateRoll("22d12+176"), 22, 17, "3d10+30", "d20+2", "Curse of Brutality", "Desecration Breath20d8", "Gouging Toss2d8", "Raise Labyrinth")
break
case 111:
state.tempEnemy = createEnemy("Cosmic Horror", calculateRoll("16d20+112"), 15, 14, "6d6+16", "d20", "Poison Jet4d6", "Psychic Whispers6d10")
break
case 112:
state.tempEnemy = createEnemy("Death Knight", calculateRoll("19d8+95"), 20, 11, "3d8+15", "d20+2", "Hellfire Orb10d6", "Parry", "Destructive Wave5d6")
break
case 113:
state.tempEnemy = createEnemy("Demogorgon", calculateRoll("32d12+256"), 22, 17, "6d12+18", "d20+2", "Beguiling Gaze", "Hypnotic Gaze")
break
case 114:
state.tempEnemy = createEnemy("Dragon Turtle", calculateRoll("22d20+110"), 20, 13, "6d8+21", "d20", "Steam Breath15d6")
break
case 115:
state.tempEnemy = createEnemy("Drow Matron Mother", calculateRoll("35d8+105"), 17, 10, "2d6+8", "d20+4", "Levitate", "Plane Shift", "Gate", "Geas5d10", "Guardian of Faith", "Tentacle Rod3d6", "Summon Servant")
break
case 116:
state.tempEnemy = createEnemy("Flesh Colossus", calculateRoll("16d20+112"), 14, 13, "6d6+14", "d20-1", "Elemental Breath9d8")
break
case 117:
state.tempEnemy = createEnemy("Kraken", calculateRoll("27d20+189"), 18, 17, "9d6+30", "d20", "Lightning Storm12d10", "Ink Cloud3d10", "Fling1d6")
break
case 118:
state.tempEnemy = createEnemy("Iron Golem", calculateRoll("20d10+100"), 20, 13, "6d8+14", "d20=1", "Poison Breath10d8", "Slam3d8+7")
break
case 119:
state.tempEnemy = createEnemy("Leviathan", calculateRoll("16d20+160"), 17, 16, "4d10+40", "d20+7", "Tidal Wave6d10")
break
case 120:
state.tempEnemy = createEnemy("Lich", calculateRoll("18d8+54"), 17, 12, "3d6", "Acid Arrow4d4", "Fireball8d6", "Dimension Door", "Animate Dead", "Ray of Frost3d8", "Disrupt Life6d6", "Frightening Gaze", "Paralyzing Touch")
break
case 121:
state.tempEnemy = createEnemy("Planetar", calculateRoll("16d10+112"), 19, 12, "4d6+7", "d20+5", "Insect Plague4d10", "Blade Barrier", )
break
case 122:
state.tempEnemy = createEnemy("Raeleus", calculateRoll("19d12+190"), 22, 17, "6d6+10", "d20+5", "Musket Blast6d10+10", "Auto Pistolero10d6", "Canister Grenada4d10", "Stun Grenada", "Magic Chaff Grenada")
break
case 123:
state.tempEnemy = createEnemy("Solar", calculateRoll("18d10+144"), 21, 15, "8d6+16", "d20+6", "Flying Sword", "Searing Burst8d6", "Blinding Gaze")
break
case 124:
state.tempEnemy = createEnemy("Tarrasque", calculateRoll("33d20+330"), 25, 19, "20d8+50", "d20")
break
case 125:
state.tempEnemy = createEnemy("Zariel", calculateRoll("40d10+360"), 21, 16, "4d8+16", "d20+7", "Horrid Touch8d10", "Immolating Gaze4d10", "Teleport")
break
case 126:
state.tempEnemy = createEnemy("Commoner", calculateRoll("1d8"), 10, 2, "1d4", "d20")
break
case 127:
state.tempEnemy = createEnemy("Bandit", calculateRoll("2d8+2"), 12, 3, "1d6+1", "d20+1")
break
case 128:
state.tempEnemy = createEnemy("Guard", calculateRoll("2d8+2"), 16, 3, "1d6+1", "d20+1")
break
case 129:
state.tempEnemy = createEnemy("Cultist", calculateRoll("2d8"), 12, 3, "1d6+1", "d20+1", "Dark Devotion")
break
case 130:
state.tempEnemy = createEnemy("Acolyte", calculateRoll("2d8"), 10, 2, "1d4", "d20", "Sacred Flame1d8", "Cure Wounds")
break
case 131:
state.tempEnemy = createEnemy("Apprentice", calculateRoll("3d8"), 10, 4, "1d10+2", "d20", "Burning Hands3d6")
break
case 132:
state.tempEnemy = createEnemy("Witch", calculateRoll("3d8+3"), 10, 3, "1d6+2", "d20", "Ray of Sickness2d8", "Tashas Hideous Laughter", "Invisibility", "Ray of Frost2d8")//
break
case 133:
state.tempEnemy = createEnemy("Buccaneer", calculateRoll("8d8+24"), 14, 5, "1d6+3", "d20+2", "Invade")
break
case 134:
state.tempEnemy = createEnemy("Spy", calculateRoll("6d8"), 12, 4, "1d6+2", "d20+2", "Sneak Attack2d6+2")
break
case 135:
state.tempEnemy = createEnemy("Captain", calculateRoll("10d8+20"), 15, 5, "3d6+9", "initiative")
break
case 136:
state.tempEnemy = createEnemy("Bard", calculateRoll("8d8+8"), 15, 4, "1d6+2", "d20+2", "Charm Person", "Shatter3d8", "Thunderwave2d8", "Vicious Mockery1d4")
break
case 137:
state.tempEnemy = createEnemy("Berserker", calculateRoll("9d8+27"), 13, 5, "1d12+3", "d20+1")
break
case 138:
state.tempEnemy = createEnemy("Priest", calculateRoll("5d8+5"), 13, 2, "1d6", "d20", "Spirit Guardians3d8", "Spiritual Weapon1d8", "Guiding Bolt4d6", "Cure Wounds")
break
case 139:
state.tempEnemy = createEnemy("Knight", calculateRoll("8d8+16"), 18, 5, "4d6+6", "d20", "Leadership")
break
case 140:
state.tempEnemy = createEnemy("Archer", calculateRoll("10d8+30"), 16, 6, "2d8+8", "d20+4")
break
case 141:
state.tempEnemy = createEnemy("Warrior", calculateRoll("6d8+12"), 16, 6, "1d8+3", "d20+1")
break
case 142:
state.tempEnemy = createEnemy("Conjurer", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Conjure Elemental", "Cloud Kill5d8", "Cloud of Daggers5d8", "Poison Spray1d12")
break
case 143:
state.tempEnemy = createEnemy("Mage", calculateRoll("9d8"), 12, 5, "1d4+2", "d20+2", "Greater Invisibility", "Ice Storm4d6", "Fireball8d6", "Magic Missile3d4+3")
break
case 144:
state.tempEnemy = createEnemy("Assassin", calculateRoll("12d8+24"), 15, 6, "2d6+6", "d20+3", "Sneak Attack6d6+6")
break
case 145:
state.tempEnemy = createEnemy("Evoker", calculateRoll("12d8+12"), 12, 3, "1d6-1", "d20+2", "Chain Lightning10d8", "Wall of Ice", "Counter Spell", "Shatter3d8", "Magic Missile6d4+6")
break
case 146:
state.tempEnemy = createEnemy("Necromancer", calculateRoll("12d8+12"), 12, 7, "2d4", "d20+2", "Circle of Death8d6", "Blight8d8", "Cloudkill5d8", "Animate Dead", "Chill Touch1d8")
break
case 147:
state.tempEnemy = createEnemy("Champion", calculateRoll("22d8+44"), 18, 9, "6d6+15", "d20+2", "Second Wind")
break
case 148:
state.tempEnemy = createEnemy("Warlord", calculateRoll("27d8+108"), 18, 9, "4d6+10", "d20+3", "Command Ally", "Frighten Foe")
break
case 149:
state.tempEnemy = createEnemy("Archmage", calculateRoll("18d8+18"), 12, 6, "1d4+2", "d20+2", "Time Stop", "Mind Blank", "Lightning Bolt8d6", "Cone of Cold8d8", "Shocking Grasp1d8")
break
case 150:
state.tempEnemy = createEnemy("Archdruid", calculateRoll("24d8+24"), 16, 6, "1d6+2", "d20+2", "Fire Storm7d10", "Sunbeam6d8", "Wall of Fire", "Beast Sense", "Conjure Animals")
break
}
if (nameMatches != null) state.tempEnemy.name = nameMatches[0]
}
return text
case 500:
state.show = null
state.setupEnemyStep = null
var enemy = createEnemy(state.tempEnemy.name, state.tempEnemy.health, state.tempEnemy.ac, state.tempEnemy.hitModifier, state.tempEnemy.damage, state.tempEnemy.initiative)
enemy.spells = [...state.tempEnemy.spells]
var enemyMatches = state.enemies.filter(x => x.name.toLowerCase() == enemy.name.toLowerCase() || x.name.toLowerCase() == `${enemy.name.toLowerCase()} a`)
if (enemyMatches.length > 0) {
enemy.name = getUniqueName(enemy.name)
if (enemy.name.endsWith("A")) {
enemyMatches[0].name = enemy.name
enemy.name = enemy.name.substring(0, enemy.name.length - 1) + "B"
}
}
state.enemies.push(enemy)
break
}
return text
}
function 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},
{name: "Animal Handling", stat: "Wisdom", modifier: 0},
{name: "Arcana", stat: "Intelligence", modifier: 0},
{name: "Athletics", stat: "Strength", modifier: 0},
{name: "Deception", stat: "Charisma", modifier: 0},
{name: "History", stat: "Intelligence", modifier: 0},
{name: "Insight", stat: "Wisdom", modifier: 0},
{name: "Intimidation", stat: "Charisma", modifier: 0},
{name: "Investigation", stat: "Intelligence", modifier: 0},
{name: "Medicine", stat: "Wisdom", modifier: 0},
{name: "Nature", stat: "Intelligence", modifier: 0},
{name: "Perception", stat: "Wisdom", modifier: 0},
{name: "Performance", stat: "Charisma", modifier: 0},
{name: "Persuasion", stat: "Charisma", modifier: 0},
{name: "Religion", stat: "Intelligence", modifier: 0},
{name: "Sleight of Hand", stat: "Dexterity", modifier: 0},
{name: "Stealth", stat: "Dexterity", modifier: 0},
{name: "Survival", stat: "Wisdom", modifier: 0},
]
}
function processCommandSynonyms(command, commandName, synonyms, func) {
text = null
synonyms.forEach(x => {
if (commandName == x || commandName == x + "s") {
text = func(command)
}
})
return text
}
function init() {
if (state.tempCharacter == null) {
state.tempCharacter = {
name: "template",
className: "adventurer",
summary: "Template character not meant to be used.",
inventory: [],
spells: [],
stats: [],
spellStat: null,
meleeStat: null,
rangedStat: null,
experience: 0,
health: 10,
ac: 10,
damage: "1d6",
proficiency: 2
}
}
if (state.tempEnemy == null) state.tempEnemy = createEnemy("enemy", 10, 10, "2d6", 10)
if (state.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 = []
if (state.x == null) state.x = 0
if (state.y == null) state.y = 0
if (state.autoXp == null) state.autoXp = 0
if (state.defaultDifficulty == null) state.defaultDifficulty = 10
if (state.day == null) state.day = 0
if (state.enemies == null) state.enemies = []
if (state.allies == null) state.allies = []
if (state.initiativeOrder == null) state.initiativeOrder = []
state.show = null
state.prefix = null
state.critical = null
state.characters.forEach(x => {
if (x.ac == null) x.ac = 10
if (x.damage == null) x.damage = "1d6"
if (x.proficiency == null) x.proficiency = 2
})
}
function doRoll(command) {
var rollType = searchArgument(command, /^(advantage)|(disadvantage)$/gi)
if (rollType == null) rollType = "normal"
var dice = searchArgument(command, /^.*\d.*$/gi)
if (dice == null) dice = "d20"
dice = formatRoll(dice)
var addition = getAddition(dice)
var roll = calculateRoll(dice) - addition
if (rollType == "advantage") roll = Math.max(roll, calculateRoll(dice) - addition)
if (rollType == "disadvantage") roll = Math.min(roll, calculateRoll(dice) - addition)
state.show = "none"
var text = `\n[You roll a ${dice}`
if (rollType != "normal") text += ` with ${rollType}`
text += `. Score: ${roll}`
if (roll == 20) text += " Critical Success!"
else if (roll == 1) text += " Critical Failure!"
else if (addition > 0) text += ` + ${addition} = ${roll + addition}`
else if (addition < 0) text += ` - ${Math.abs(addition)} = ${roll + addition}`
text += "]\n"
return text
}
function doCreate(command) {
if (!hasCharacter(state.characterName)) createCharacter(state.characterName)
var character = getCharacter()
state.createStep = 0
state.tempCharacter.name = character.name
resetTempCharacterSkills()
state.tempCharacter.stats = []
state.tempCharacter.spells = []
state.tempCharacter.inventory = [{name: "Gold", quantity: 50}, {name: "Rope", quantity: 1}, {name: "Ration", quantity: 10}, {name: "Torch", quantity: 1}]
state.tempCharacter.spellStat = null
state.tempCharacter.meleeStat = "Strength"
state.tempCharacter.rangedStat = "Dexterity"
state.tempCharacter.ac = 10
state.tempCharacter.damage = "1d6"
state.tempCharacter.proficiency = 2
state.show = "create"
return " "
}
function doSetupEnemy(command) {
state.setupEnemyStep = 0
state.tempEnemy = createEnemy("enemy", 20, 10, 0, "2d6", 10)
state.show = "setupEnemy"
return " "
}
function 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"
doTake("take Stragedy Ace Card")
doTake("take Stragedy Queen Card")
doTake("take Stragedy 2 Card")
doTake("take Stragedy 2 Card")
doTake("take Stragedy 3 Card")
doTake("take Stragedy 3 Card")
doTake("take Stragedy 4 Card")
doTake("take Stragedy 4 Card")
doTake("take Stragedy 5 Card")
doTake("take Stragedy 5 Card")
doTake("take Stragedy 6 Card")
doTake("take Stragedy 6 Card")
doTake("take Stragedy 7 Card")
doTake("take Stragedy 7 Card")
doTake("take Stragedy 8 Card")
doTake("take Stragedy 8 Card")
doTake("take Stragedy 9 Card")
doTake("take Stragedy 9 Card")
doTake("take Stragedy King Card")
doTake("take Stragedy Jack Card")
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"
return " "
}
function handleStragedyShopStep(text) {
state.show = "stragedyShop"
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.stragedyShopStep = 500
return 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
var item = state.cardDeals[index]
var price = state.cardPrices[index]
var character = getCharacter()
var goldIndex = character.inventory.findIndex(x => x.name.toLowerCase() == "gold")
var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity
if (price > gold) {
state.stragedyShopStep = 2
return text
}
doTake(`take Stragedy ${item} Card`)
character.inventory[goldIndex].quantity -= price
state.cardDeals.splice(index, 1)
state.cardPrices.splice(index, 1)
state.stragedyShopStep = 1
break
case 500:
state.show = null
state.stragedyShopStep = null
break
}
return text
}
function doStragedy(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
arg0 = "easy"
}
var character = getCharacter()
state.stragedyTurn = "intro"
state.show = "stragedy"
state.stragedyPlayerScore = 0
state.stragedyPlayerHand = []
state.stragedyPlayerBattlefield = []
state.stragedyPlayerDeck = []
for (item of character.inventory) {
if (/stragedy ace card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("a")
else if (/stragedy jack card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("j")
else if (/stragedy queen card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("q")
else if (/stragedy king card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("k")
else if (/stragedy joker card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("?")
else if (/stragedy witch card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("w")
else if (/stragedy priest card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("p")
else if (/stragedy brigand card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push("b")
else if (/stragedy \d+ card/gi.test(item.name)) for (var i = 0; i < item.quantity; i++) state.stragedyPlayerDeck.push(item.name.match(/(?<=stragedy )\d+(?= card)/gi)[0])
}
shuffle(state.stragedyPlayerDeck)
state.stragedyPlayerDeck.splice(20)
state.stragedyPlayerDiscard = []
state.stragedyPlayerRetired = false
state.stragedyEnemyScore = 0
state.stragedyEnemyHand = []
state.stragedyEnemyBattlefield = []
switch(arg0) {
case "impossible":
state.stragedyEnemyDeck = ["?", "?", "a", "q", "q", "k", "k", "w", "p", "2", "3", "4", "5", "6", "7", "7", "8", "10", "10", "10"]
case "hard":
state.stragedyEnemyDeck = ["j", "?", "a", "q", "q", "k", "k", "2", "3", "4", "5", "5", "6", "6", "7", "7", "8", "8", "10", "10"]
case "medium":
state.stragedyEnemyDeck = ["j", "j", "a", "q", "q", "k", "k", "2", "3", "4", "5", "5", "6", "6", "7", "7", "8", "8", "9", "10"]
break
case "effortless":
state.stragedyEnemyDeck = ["j", "j", "a", "a", "2", "2", "3", "3", "4", "4", "5", "5", "6", "6", "6", "7", "7", "8", "8", "9"]
case "automatic":
state.stragedyEnemyDeck = ["2", "2", "2", "3", "3", "3", "4", "4", "4", "5", "5", "5", "6", "6", "6", "6", "7", "7", "7", "7"]
case "easy":
default:
state.stragedyEnemyDeck = ["j", "q", "k", "a", "2", "2", "3", "3", "4", "4", "5", "5", "6", "6", "7", "7", "8", "8", "9", "9"]
break
}
shuffle(state.stragedyEnemyDeck)
state.stragedyEnemyDiscard = []
state.stragedyEnemySkipTurn = getRandomBoolean(.5)
state.stragedyEnemyRetired = false
state.stragedyEnemyTurnText = null
return " "
}
function handleStragedyTurn(text) {
state.show = "stragedy"
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 == "f") {
state.stragedyTurn = "gameOver"
state.stragedyWinner = "forfeit"
return "You forfeit the game."
}
switch (state.stragedyTurn) {
case "intro":
if (text == "d") {
if (state.stragedyPlayerDeck.length < 20) return "\nYou cannot play if you don't have at least 20 Stragedy cards.\n"
state.stragedyTurn = "game"
var drawCards = state.stragedyPlayerDeck.splice(state.stragedyPlayerDeck.length - 4)
state.stragedyPlayerHand.push(...drawCards)
drawCards = state.stragedyEnemyDeck.splice(state.stragedyEnemyDeck.length - 4)
state.stragedyEnemyHand.push(...drawCards)
stragedyCalculateScores()
if (!state.stragedyEnemySkipTurn) stragedyEnemyTurn()
}
return `You deal the cards. ${state.stragedyEnemySkipTurn ? "You go first." : "The opponent goes first."}`
case "game":
return stragedyPlayerTurn(text)
case "gameOver":
state.show = null
state.stragedyTurn = null
return text
}
return `\nUnexpected stragedy state. Input text: ${text}`
}
function doAddCard(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
arg0 = ""
}
arg0 = arg0.toLowerCase()
if (arg0.startsWith("w")) arg0 = "w"
if (arg0.startsWith("b")) arg0 = "b"
switch(arg0) {
case "a":
case "ace":
return doTake("take Stragedy Ace Card")
case "j":
case "jack":
return doTake("take Stragedy Jack Card")
case "q":
case "queen":
return doTake("take Stragedy Queen Card")
case "k":
case "king":
return doTake("take Stragedy King Card")
case "?":
case "joker":
return doTake("take Stragedy Joker Card")
case "w":
case "witch":
return doTake("take Stragedy Witch Card")
case "p":
case "priest":
return doTake("take Stragedy Priest Card")
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":
return doTake(`take Stragedy ${getRandomFromList("10", "Ace", "Jack")} Card`)
case "epic":
return doTake(`take Stragedy ${getRandomFromList("Queen", "King", "Joker")} Card`)
case "legendary":
return doTake(`take Stragedy ${getRandomFromList("Witch", "Priest", "Brigand")} Card`)
default:
return doTake(`take Stragedy ${getRandomFromList("2", "3", "4", "5", "6", "7", "8", "9", "10", "Ace", "Jack", "Queen", "King", "Joker", "Witch", "Priest", "Brigand")} Card`)
}
}
function doBio(command) {
state.show = "bio"
return " "
}
function doRenameCharacter(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
state.show = "none"
var text = `\n[${possessiveName} name has been changed to ${arg0}]\n`
character.name = arg0
return text
}
function doCloneCharacter(command) {
var character = getCharacter()
var possessiveName = getPossessiveName(character.name)
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (!hasCharacter(arg0)) createCharacter(arg0)
var newCharacter = getCharacter(arg0)
copyCharacter(character, newCharacter)
state.show = "none"
var text = `\n[${character.name} has been cloned to a new character called ${newCharacter.name}]\n`
return text
}
function doSetStat(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = clamp(parseInt(getArgument(command, 1)), 1, 100)
var possessiveName = getPossessiveName(character.name)
const stat = {
name: arg0,
value: arg1
}
var index = character.stats.findIndex((element) => element.name.toLowerCase() == stat.name.toLowerCase())
if (index == -1) {
character.stats.push(stat)
} else {
var existingStat = character.stats[index]
existingStat.value = parseInt(stat.value)
}
state.show = "none"
return `\n[${possessiveName} ${toTitleCase(arg0)} ability is now ${arg1}]\n`
}
function doSetSpellStat(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
character.spellStat = arg0
state.show = "none"
return `\nSpellcasting Ability is set to ${arg0}\n`
}
function doSetMeleeStat(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
character.meleeStat = arg0
state.show = "none"
return `\nMelee Ability is set to ${arg0}\n`
}
function doSetRangedStat(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
character.rangedStat = arg0
state.show = "none"
return `\nRanged Ability is set to ${arg0}\n`
}
function doSetAutoXp(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
state.autoXp = Math.max(0, arg0)
state.show = "none"
return state.autoXp <= 0 ? `\n[Auto XP is disabled]\n` : `\n[Auto XP is set to ${state.autoXp}]\n`
}
function doShowAutoXp(command) {
state.show = "none"
return state.autoXp <= 0 ? `\n[Auto XP is disabled]\n` : `\n[Auto XP is set to ${state.autoXp}]\n`
}
function doSetDefaultDifficulty(command) {
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var difficulty = getArgument(command, 0)
if (difficulty == null) difficulty = "easy"
var difficultyIndex = difficultyNames.indexOf(difficulty)
if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) {
difficulty = difficultyScores[difficultyIndex]
}
state.defaultDifficulty = Math.max(0, difficulty)
state.show = "none"
return `\n[The default difficulty is set to ${state.defaultDifficulty}]\n`
}
function doShowDefaultDifficulty(command) {
state.show = "none"
return `\n[The default difficulty is set to ${state.defaultDifficulty}]\n`
}
function doSetSkill(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = getArgument(command, 1)
if (arg1 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg2 = getArgument(command, 2)
if (arg2 == null) {
arg2 = (clamp(parseInt(arg1, 1, 100)))
arg1 = null
} else {
arg2 = clamp(parseInt(arg2), 1, 100)
}
var possessiveName = getPossessiveName(character.name)
const skill = {
name: arg0,
stat: arg1,
modifier: arg2
}
var index = character.skills.findIndex((element) => element.name.toLowerCase() == skill.name.toLowerCase())
if (index == -1) {
if (arg1 == null) {
state.show = "none"
return "\n[Error: New skills must have an ability specified. See #help]\n"
}
character.skills.push(skill)
} else {
var existingSkill = character.skills[index]
existingSkill.modifier = parseInt(skill.modifier)
if (arg1 != null) existingSkill.stat = skill.stat
}
state.show = "none"
return `\n[${possessiveName} ${toTitleCase(arg0)} skill is now ${arg2 >= 0 ? "+" + arg2 : "-" + arg2} and based on ${toTitleCase(arg1)}]\n`
}
function doSetAc(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Not a number. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
character.ac = parseInt(arg0)
state.show = "none"
return `\n[${possessiveName} armor class is set to ${character.ac}]\n`
}
function doSetExperience(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Not a number. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
character.experience = parseInt(arg0)
state.show = "none"
return `\n[${possessiveName} experience is set to ${character.experience}]\n`
}
function doAddExperience(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
arg0 = searchArgument(command, /\d+/gi)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
arg0 = parseInt(arg0)
var arg1 = searchArgument(command, /party/gi)
if (arg1 == null && character == null) {
state.show = "none"
return `\n[Error: Character name not specified. Use the "do" or "say" modes. Alternatively, use "story" mode in the following format without quotes: "charactername #hashtag"]\n`
}
if (state.characters.length == 0) {
state.show = "none"
return `\n[Error: There are no characters. Type #setup to create a character]\n`
}
state.prefix = "\n"
characters = arg1 == null ? [character] : state.characters
for (var c of characters) {
var possessiveName = getPossessiveName(c.name)
var level = getLevel(c.experience)
c.experience += arg0
var newLevel = getLevel(c.experience)
if (newLevel > level) state.prefix += `[${possessiveName} experience is increased to ${c.experience}. LEVEL UP! Level: ${newLevel}, Health Max: ${getHealthMax(c)}. Next level at ${getNextLevelXp(c.experience)}]\n`
else state.prefix += `[${possessiveName} experience is increased to ${c.experience}. Next level at ${getNextLevelXp(c.experience)}]\n`
}
state.show = "prefixOnly"
return " "
}
function doLevelUp(command) {
var character = getCharacter()
var level = getLevel(character.experience)
var experience = level >= levelSplits.length ? 0 : levelSplits[level] - character.experience
return doAddExperience(`${command} ${experience}`)
}
function doSetClass(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
character.className = arg0
state.show = "none"
return `\n[${possessiveName} class is set to "${character.className}"]\n`
}
function doSetSummary(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
character.summary = arg0
state.show = "none"
return `\n[${possessiveName} summary is set]\n`
}
function doSetHealth(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
character.health = arg0
character.health = clamp(character.health, 0, getHealthMax())
state.show = "none"
return `\n[${possessiveName} health is set to ${character.health} health]\n`
}
function doHeal(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = getArgumentRemainder(command, 1)
if (arg1 == null) {
if (character == null) {
state.show = "none"
return "\n[Error: Character must be specified. See #help]\n"
}
var healing
var healingMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi)
if (healingMatches != null) healing = calculateRoll(healingMatches[0])
else {
healingMatches = arg0.match(/\d+/g)
if (healingMatches != null) healing = parseInt(healingMatches[healingMatches.length - 1])
}
if (healing == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
var haveWord = character.name == "You" ? "have" : "has"
character.health += healing
character.health = clamp(character.health, 0, getHealthMax())
state.show = "none"
return `\n[${character.name} ${haveWord} been healed for ${healing} hp to a total of ${character.health}]\n`
} else {
var healing
var healingMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi)
if (healingMatches != null) healing = calculateRoll(healingMatches[0])
else {
healingMatches = arg0.match(/\d+/g)
if (healingMatches != null) healing = parseInt(healingMatches[0])
}
if (healing == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
for (var enemy of state.enemies) {
if (enemy.name.toLowerCase() == arg1.toLowerCase()) {
enemy.health = Math.max(0, enemy.health + healing)
state.show = "none"
return `\n[${toTitleCase(enemy.name)} has been healed for ${healing} hp to a total of ${enemy.health}]\n`
}
}
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
character.health = clamp(character.health, 0, getHealthMax(character))
state.show = "none"
return `\n[${toTitleCase(character.name)} has been healed for ${healing} hp to a total of ${character.health}]\n`
}
}
state.show = "none"
return `\n[Error: Could not find an enemy, ally, or character matching the name ${arg1}. Type #enemies, #allies, or #characters to see a list]`
}
}
function doDamage(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = getArgumentRemainder(command, 1)
if (arg1 == null) {
if (character == null) {
state.show = "none"
return "\n[Error: Character must be specified. See #help]\n"
}
var damage
var damageMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi)
if (damageMatches != null) damage = calculateRoll(damageMatches[0])
else {
damageMatches = arg0.match(/\d+/g)
if (damageMatches != null) damage = parseInt(damageMatches[damageMatches.length - 1])
}
if (damage == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
var haveWord = character.name == "You" ? "have" : "has"
character.health -= damage
character.health = clamp(character.health, 0, getHealthMax())
state.show = "none"
return `\n[${character.name} ${haveWord} been damaged for ${damage} hp with ${character.health} remaining] ${character.health == 0 ? " You are unconscious" : ""}\n`
} else {
var damage
var damageMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi)
if (damageMatches != null) damage = calculateRoll(damageMatches[0])
else {
damageMatches = arg0.match(/\d+/g)
if (damageMatches != null) damage = parseInt(damageMatches[0])
}
if (damage == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
for (var enemy of state.enemies) {
if (enemy.name.toLowerCase() == arg1.toLowerCase()) {
enemy.health = Math.max(0, enemy.health - damage)
state.show = "none"
return `\n[${toTitleCase(enemy.name)} has been damaged for ${damage} hp with ${enemy.health} remaining] ${enemy.health == 0 ? " " + toTitleCase(enemy.name) + " has been defeated!" : ""}\n`
}
}
for (var 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)
state.show = "none"
return `\n[${toTitleCase(character.name)} has been damaged for ${damage} hp with ${character.health} remaining] ${character.health == 0 ? " " + toTitleCase(character.name) + " is unconcious!" : ""}\n`
}
}
state.show = "none"
return `\n[Error: Could not find an enemy, ally, or character matching the name ${arg1}. Type #enemies, #allies, or #characters to see a list]`
}
}
function doRest(command) {
var commandName = getCommandName(command)
state.day++
state.enemies = []
state.cardDeals = null
state.cardPrices = null
state.spellShopDeals = null
state.itemShopDeals = null
var healingFactor = 1
var text
if (commandName.toLowerCase() == "shortrest") {
state.day--
healingFactor = .5
text = `\n[All characters have healed 50%]\n`
} else {
text = `\n[All characters have rested and feel rejuvinated. It's now day ${state.day}]\n`
}
state.characters.forEach(function(character) {
var max = getHealthMax(character)
character.health += Math.floor(max * healingFactor)
if (character.health > max) character.health = max
})
state.show = "none"
return text
}
function doHealParty(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var healing
var healingMatches = arg0.match(/\d*d\d+((\+|-)d+)?/gi)
if (healingMatches != null) healing = calculateRoll(healingMatches[0])
else {
healingMatches = arg0.match(/\d+/g)
if (healingMatches != null) healing = parseInt(healingMatches[healingMatches.length - 1])
}
if (healing == null) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
var text = `\n[All characters have been healed by ${healing}.]\n`
state.characters.forEach(function(character) {
var max = getHealthMax(character)
character.health += healing
if (character.health > max) character.health = max
text += `[${toTitleCase(character.name)}: ${character.health} / ${max} health]\n`
})
state.show = "none"
return text
}
function doFlipCommandAbility(command) {
var ability = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) return;
var remainder = getArgumentRemainder(command, 1)
command = `${arg0} "${ability}"${remainder == null ? "" : " " + remainder}`
text = processCommandSynonyms(command, arg0, checkSynonyms, doCheck)
if (text == null) text = processCommandSynonyms(command, arg0, trySynonyms, doTry)
return text
}
function doCheck(command) {
const advantageNames = ["normal", "advantage", "disadvantage"]
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
var character = getCharacter()
var arg0 = null
if (character.stats.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.stats))
if (arg0 == null && character.skills.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.skills))
if (arg0 == null) arg0 = "Ability"
arg0 = toTitleCase(arg0)
var arg1 = searchArgument(command, arrayToOrPattern(advantageNames))
if (arg1 == null) arg1 = "normal"
else arg1 = arg1.toLowerCase()
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var arg2 = searchArgument(command, arrayToOrPattern(difficultyPatternNames))
if (arg2 == null) arg2 = state.defaultDifficulty
else arg2 = arg2.toLowerCase()
var die1 = calculateRoll("1d20")
var die2 = calculateRoll("1d20")
var score = arg1 == "advantage" ? Math.max(die1, die2) : arg1 == "disadvantage" ? Math.min(die1, die2) : die1
var modifier = 0
var skill = character.skills.find(x => x.name.toLowerCase() == arg0.toLowerCase())
if (skill != null) {
var stat = character.stats.find((element) => element.name.toLowerCase() == skill.stat.toLowerCase())
if (stat != null) modifier = skill.modifier + getModifier(stat.value)
} else {
var stat = character.stats.find((element) => element.name.toLowerCase() == arg0.toLowerCase())
if (stat != null) modifier = getModifier(stat.value)
}
var target = 15
if (/^\d+$/.test(arg2)) target = arg2
else {
var targetIndex = difficultyNames.indexOf(arg2)
if (targetIndex >= 0 && targetIndex < difficultyNames.length) target = difficultyScores[targetIndex]
}
state.show = "none"
var dieText = arg1 == "advantage" || arg1 == "disadvantage" ? `${arg1}(${die1},${die2})` : die1
var text
if (score == 20) text = `\n[${arg0} check DC: ${target} roll: ${dieText}. Critical Success!]\n`
else if (score == 1) text = `\n[${arg0} check DC: ${target} roll: ${dieText}. Critical Failure!]\n`
else if (modifier != 0) text = `\n[${arg0} check DC: ${target} roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n`
else text = `\n[${arg0} check DC: ${target} roll: ${dieText}. ${score >= target ? "Success!" : "Failure!"}]\n`
return text
}
function doTry(command) {
if (getArguments(command).length <= 1) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
const advantageNames = ["normal", "advantage", "disadvantage"]
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
var character = getCharacter()
var textIndex = 3
var failword = character.name == "You" ? "fail" : "fails"
var arg0 = null
if (character.stats.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.stats))
if (arg0 == null && character.skills.length > 0) arg0 = searchArgument(command, statsToOrPattern(character.skills))
if (arg0 == null) {
arg0 = "Ability"
textIndex--
}
arg0 = toTitleCase(arg0)
var arg1 = searchArgument(command, arrayToOrPattern(advantageNames))
if (arg1 == null) {
arg1 = "normal"
textIndex--
}
else arg1 = arg1.toLowerCase()
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var arg2 = searchArgument(command, arrayToOrPattern(difficultyPatternNames))
if (arg2 == null) {
arg2 = state.defaultDifficulty
textIndex--
}
else arg2 = arg2.toLowerCase()
var arg3 = getArgumentRemainder(command, textIndex)
var toMatches = arg3.match(/^to\s+/gi)
if (toMatches != null) arg3 = arg3.substring(toMatches[0].length)
if (!/^.*(\.|!|\?)$/gi.test(arg3)) arg3 += "."
var die1 = calculateRoll("1d20")
var die2 = calculateRoll("1d20")
var score = arg1 == "advantage" ? Math.max(die1, die2) : arg1 == "disadvantage" ? Math.min(die1, die2) : die1
var modifier = 0
var skill = character.skills.find(x => x.name.toLowerCase() == arg0.toLowerCase())
if (skill != null) {
var stat = character.stats.find(x => x.name.toLowerCase() == skill.stat.toLowerCase())
if (stat != null) modifier = skill.modifier + getModifier(stat.value)
} else {
var stat = character.stats.find(x => x.name.toLowerCase() == arg0.toLowerCase())
if (stat != null) modifier = getModifier(stat.value)
}
var target = 15
if (/^\d+$/.test(arg2)) target = arg2
else {
var targetIndex = difficultyNames.indexOf(arg2)
if (targetIndex >= 0 && targetIndex < difficultyNames.length) target = difficultyScores[targetIndex]
}
var dieText = arg1 == "advantage" || arg1 == "disadvantage" ? `${arg1}(${die1},${die2})` : die1
state.show = "prefix"
if (score == 20) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}]\n`
else if (score == 1) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}]\n`
else if (modifier != 0) state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${score + modifier}. ${score + modifier >= target ? "Success!" : "Failure!"}]\n`
else state.prefix = `\n[${arg0} check DC: ${target} roll: ${dieText}. ${score >= target ? "Success!" : "Failure!"}]\n`
var text = `\n${character.name} ${score + modifier >= target ? "successfully" : failword + " to"} ${arg3}`
if (score == 20) text += " Critical success! The action was extremely effective."
else if (score == 1) text += " Critical failure! There are dire consequences for this action."
if (score + modifier >= target || score == 20) text += addXpToAll(Math.floor(state.autoXp * clamp(target, 1, 20) / 20)) + "\n"
return text
}
function doAttack(command) {
const advantageNames = ["normal", "advantage", "disadvantage"]
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
var character = getCharacter()
var textIndex = 3
var missWord = character.name == "You" ? "miss" : "misses"
var tryWord = character.name == "You" ? "try" : "tries"
var usingDefaultDifficulty = false
var statText = null
statText = searchArgument(command, /ranged/gi, textIndex - 1)
if (statText == null) {
statText = character.meleeStat
textIndex--
} else if (statText.toLowerCase() == "ranged") statText = character.rangedStat
statText = toTitleCase(statText)
var advantageText = searchArgument(command, arrayToOrPattern(advantageNames), textIndex - 1)
if (advantageText == null) {
advantageText = "normal"
textIndex--
}
else advantageText = advantageText.toLowerCase()
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var difficultyText = searchArgument(command, arrayToOrPattern(difficultyPatternNames), textIndex - 1)
if (difficultyText == null) {
difficultyText = state.defaultDifficulty
usingDefaultDifficulty = true
textIndex--
}
else difficultyText = difficultyText.toLowerCase()
var targetText = getArgumentRemainder(command, textIndex)
if (targetText == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var toMatches = targetText.match(/^to\s+/gi)
if (toMatches != null) targetText = targetText.substring(toMatches[0].length)
targetText = stripPunctuation(targetText)
var die1 = calculateRoll("1d20")
var die2 = calculateRoll("1d20")
var score = advantageText == "advantage" ? Math.max(die1, die2) : advantageText == "disadvantage" ? Math.min(die1, die2) : die1
var modifier = 0
var stat = character.stats.find(x => x.name.toLowerCase() == statText.toLowerCase())
modifier = character.proficiency
if (stat != null) modifier += getModifier(stat.value)
var targetRoll = 15
if (/^\d+$/.test(difficultyText)) targetRoll = difficultyText
else {
var targetIndex = difficultyNames.indexOf(difficultyText)
if (targetIndex >= 0 && targetIndex < difficultyNames.length) targetRoll = difficultyScores[targetIndex]
}
var enemyString = ""
var allyString = ""
if (state.initiativeOrder.length > 0) {
var foundEnemy
for (var enemy of state.enemies) {
if (targetText.toLowerCase().includes(enemy.name.toLowerCase())) {
foundEnemy = enemy
break
}
}
if (foundEnemy == null) {
var indexMatches = targetText.match(/(?<=enemy\s*)\d+/gi)
if (indexMatches != null) {
foundEnemy = state.enemies[parseInt(indexMatches[0]) - 1]
targetText = targetText.replace(/enemy\s*d+/gi, foundEnemy.name)
}
}
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)
var damageMatches = targetText.match(/\d*d\d+((\+|-)d+)?/gi)
if (damageMatches != null) damage = score == 20 ? calculateRoll(damageMatches[0]) + calculateRoll(damageMatches[0]) : calculateRoll(damageMatches[0])
else {
damageMatches = targetText.match(/\d+/g)
if (damageMatches != null) damage = score == 20 ? parseInt(damageMatches[damageMatches.length - 1]) * 2 : parseInt(damageMatches[damageMatches.length - 1])
}
if (foundEnemy != null) {
if (usingDefaultDifficulty) targetRoll = foundEnemy.ac
if (score == 20 || score + modifier >= targetRoll) {
if (score == 20) enemyString += `\nCritical Damage: ${damage}\n`
else enemyString += `\nDamage: ${damage}\n`
state.blockCharacter = foundEnemy
state.blockPreviousHealth = foundEnemy.health
foundEnemy.health = Math.max(0, foundEnemy.health - damage)
if (foundEnemy.health == 0) {
enemyString += ` ${toTitleCase(foundEnemy.name)} has been defeated!`
} else enemyString += ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!`
}
}
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
state.show = "prefix"
if (targetRoll == 0) state.prefix = ""
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}!`
else text = `\n${toTitleCase(character.name)} ${tryWord} to hit ${targetText}. ${toTitleCase(character.name)} ${missWord}!`
if (score == 20) text += " Critical success! The attack is exceptionally damaging!"
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"
}
function doNote(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 != null && arg0.length > 0) {
state.notes.push(arg0)
state.show = "none"
return "\n[Note added successfully]\n"
} else {
state.notes.push(history[history.length - 1].text)
state.show = "none"
return "\n[The last action was successfully added to the notes]\n"
}
}
function doShowDay(command) {
state.show = "none"
return `\n[It is day ${state.day}]\n`
}
function doSetDay(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null || isNaN(arg0)) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
state.day = parseInt(arg0)
state.show = "none"
return `\n[The day has been set to day ${state.day}]\n`
}
function doShowNotes(command) {
state.show = "showNotes"
return " "
}
function doCreateLocation(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var locationArgIndex = 2
var arg0 = getArgument(command, 0)
var arg1 = getArgument(command, 1)
if (arg0.toLowerCase() == "here") {
arg0 = state.x
arg1 = state.y
locationArgIndex = 1
} else if (arg0.toLowerCase() == "far") {
var cx = state.x
var cy = state.y
var coords = rotate(cx, cy, getRandomFloat(50, 100) + cx, cy, Math.random() * 360)
arg0 = coords[0]
arg1 = coords[1]
locationArgIndex = 1
} else if (arg0 == null || isNaN(arg0)) {
var cx = state.x
var cy = state.y
var coords = rotate(cx, cy, getRandomFloat(1, 10) + cx, cy, Math.random() * 360)
arg0 = coords[0]
arg1 = coords[1]
locationArgIndex = 0
} else if (arg1 == null || isNaN(arg1)) {
var cx = state.x
var cy = state.y
var coords = rotate(cx, cy, parseFloat(arg0) + cx, cy, Math.random() * 360)
arg0 = coords[0]
arg1 = coords[1]
locationArgIndex = 1
}
var arg2 = getArgumentRemainder(command, locationArgIndex)
if (arg2 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var location = createLocation(arg0, arg1, arg2)
state.show = "none"
return `\n[Location ${toTitleCase(arg2)} has been created at (${location.x},${location.y})]\n`
}
function doGoNorth(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var commandName = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) arg0 = 1
else {
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
arg0 = parseInt(arg0)
}
return doGoToLocation(`${commandName} ${state.x} ${state.y - arg0}`)
}
function doGoSouth(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var commandName = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) arg0 = 1
else {
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
arg0 = parseInt(arg0)
}
return doGoToLocation(`${commandName} ${state.x} ${state.y + arg0}`)
}
function doGoEast(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var commandName = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) arg0 = 1
else {
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
arg0 = parseInt(arg0)
}
return doGoToLocation(`${commandName} ${state.x + arg0} ${state.y}`)
}
function doGoWest(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var commandName = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) arg0 = 1
else {
if (isNaN(arg0)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
arg0 = parseInt(arg0)
}
return doGoToLocation(`${commandName} ${state.x - arg0} ${state.y}`)
}
function doGoToLocation(command) {
command = command.replaceAll(/\s*,\s*/g, " ")
var character = getCharacter()
var characterName = character == null ? "You" : character.name
var possessiveName = getPossessiveName(characterName)
var travelWord = characterName == "You" ? "travel" : "travels"
var locationArgIndex = 2
var arg0 = getArgument(command, 0)
var arg1 = getArgument(command, 1)
if (arg0 == null || isNaN(arg0)) {
arg0 = state.x
arg1 = state.y
locationArgIndex = 0
}
if (arg0 != null && (arg1 == null || isNaN(arg1))) {
arg1 = null
locationArgIndex = 1
}
var distance = 0
var location
var locationName = getArgumentRemainder(command, locationArgIndex)
if (locationName == null && locationArgIndex == 0) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (!isNaN(arg0) && arg1 == null && locationName == null) {
arg0 = parseInt(arg0) - 1
if (arg0 < 0 || arg0 >= state.locations.length) {
state.show = "none"
return "\n[Error: Incorrect location number. See #help]\n"
}
location = state.locations[arg0]
arg0 = null
arg1 = null
} else if (arg1 == null && locationName != null) {
var index = state.locations.findIndex(x => x.name.toLowerCase() == locationName.toLowerCase())
if (index != -1) {
location = state.locations[index]
var direction = pointDirection(state.x, state.y, location.x, location.y)
var args = rotate(state.x, state.y, state.x + parseInt(arg0), state.y, direction)
arg0 = Math.round(args[0])
arg1 = Math.round(args[1])
location = null
} else {
arg1 = state.y
location = null
}
} else if (locationName == null) {
var index = state.locations.findIndex(x => x.x == arg0 && x.y == arg1)
if (index != -1) location = state.locations[index]
} else {
var index = state.locations.findIndex(x => x.name.toLowerCase() == locationName.toLowerCase())
if (index != -1) location = state.locations[index]
else location = createLocation(arg0, arg1, locationName)
}
if (location == null) {
distance = pointDistance(state.x, state.y, arg0, arg1)
state.x = parseInt(arg0)
state.y = parseInt(arg1)
state.location = null
} else {
distance = pointDistance(state.x, state.y, location.x, location.y)
state.x = location.x
state.y = location.y
state.location = location.name
}
distance = distance.toFixed(1)
state.show = "none"
if (location == null) return `\n${characterName} ${travelWord} ${distance > 0 ? distance + " units " : ""} to (${arg0},${arg1})`
if (state.characters.length > 1) return `\n${possessiveName} party travels ${distance > 0 ? distance + " units " : ""}to ${toTitleCase(location.name)} at (${location.x},${location.y})\n`
return `\n${characterName} ${travelWord} ${distance > 0 ? distance + " units " : ""}to ${toTitleCase(location.name)} at (${location.x},${location.y})\n`
}
function doGetLocation(command) {
state.show = "location"
return `\n[You are at ${state.location == null ? "" : "the location " + toTitleCase(state.location) + " "}(${state.x},${state.y})]`
}
function doClearLocations(command) {
var arg0 = getArgument(command, 0)
if (arg0 != null) {
return doRemoveLocation(command)
}
state.locations = []
state.location = null
state.show = "none"
return "\n[The locations have been cleared]\n"
}
function doRemoveLocation(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (/\d+\D+(\d+\D*)+/gi.test(arg0)) {
var list = arg0.split(/\D+/)
list.sort(function(a, b) {
return b - a;
});
var text = "\n"
list.forEach(x => {
var num = parseInt(x) - 1
if (num >= state.locations.length) {
state.show = "none"
return `\n[Error: Location ${x} does not exist. See #showlocations]\n`
}
var location = state.locations[num]
state.locations.splice(num, 1)
text += `[The location ${toTitleCase(location.name)} has been removed]\n`
})
state.show = "none"
return text
}
var location
if (isNaN(arg0)) arg0 = state.locations.findIndex(x => x.name.toLowerCase() == arg0.toLowerCase())
else arg0--
if (arg0 == -1) {
state.show = "none"
return "\n[Error: Location not found. See #showlocations]\n"
} else if (arg0 >= state.locations.length || arg0 < 0) {
state.show = "none"
return "\n[Error: Location number out of bounds. See #showlocations]\n"
} else {
location = state.locations[arg0]
state.locations.splice(arg0, 1)
}
state.show = "none"
return `\n[The location ${toTitleCase(location.name)} has been removed]\n`
}
function doShowLocations(command) {
var arg0 = searchArgument(command, /^sort$/gi)
state.sortLocations = arg0 != null
state.show = "locations"
return " "
}
function doEncounter(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
arg0 = "easy"
}
var encounter = createEncounter(arg0)
state.enemies = encounter.enemies
var text = `\n${encounter.text}\n`
state.prefix = "\n"
if (encounter.enemies.length > 0) {
state.prefix += "You encounter the following enemies:\n"
for (var enemy of encounter.enemies) {
state.prefix += `${toTitleCase(enemy.name)} (Health: ${enemy.health} AC: ${enemy.ac} Initiative: ${enemy.initiative})\n`
}
}
state.prefix += encounter.enemies.length > 0 ? "[Type #initiative to begin the battle]\n" : ""
state.show = "prefix"
return text
}
function doShowEnemies(command) {
state.show = "showEnemies"
return " "
}
function doShowAllies(command) {
state.show = "showAllies"
return " "
}
function doRemoveEnemy(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (/\d+\D+(\d+\D*)+/gi.test(arg0)) {
var list = arg0.split(/\D+/)
list.sort(function(a, b) {
return b - a;
});
var text = "\n"
list.forEach(x => {
var num = parseInt(x) - 1
if (num >= state.enemies.length) {
state.show = "none"
return `\n[Error: Enemy ${x} does not exist. See #showenemies]\n`
}
var enemy = state.enemies[num]
state.enemies.splice(num, 1)
var index = state.initiativeOrder.indexOf(enemy)
if (index >= 0) state.initiativeOrder.splice(index, 1)
text += `[The enemy ${toTitleCase(enemy.name)} has been removed]\n`
})
state.show = "none"
return text
}
var enemy
if (isNaN(arg0)) arg0 = state.enemies.findIndex(x => x.name.toLowerCase() == arg0.toLowerCase())
else arg0--
if (arg0 == -1) {
state.show = "none"
return "\n[Error: Enemy not found. See #showenemies]\n"
} else if (arg0 >= state.enemies.length || arg0 < 0) {
state.show = "none"
return "\n[Error: Location number out of bounds. See #showenemies]\n"
} else {
enemy = state.enemies[arg0]
state.enemies.splice(arg0, 1)
}
state.show = "none"
return `\n[The enemy ${toTitleCase(enemy.name)} has been removed]\n`
}
function 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) {
return doRemoveEnemy(command)
}
state.enemies = []
state.initiativeOrder = []
state.show = "none"
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) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var health = getArgument(command, 1)
if (health == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
} else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(health)) {
health = calculateRoll(health)
} else if (isNaN(health)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
health = parseInt(health)
var ac = getArgument(command, 2)
if (ac == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
} else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(ac)) {
ac = calculateRoll(ac)
} else if (isNaN(ac)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
ac = parseInt(ac)
var hitModifier = getArgument(command, 3)
if (hitModifier == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
} else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(hitModifier)) {
hitModifier = calculateRoll(hitModifier)
} else if (isNaN(hitModifier)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
var damage = getArgument(command, 4)
if (damage == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
} else if (isNaN(damage) && !/^\d*d\d+((\+|-)\d+)?$/gi.test(damage)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
var initiative = getArgument(command, 5)
if (initiative == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
} else if (/^\d*d\d+((\+|-)\d+)?$/gi.test(initiative)) {
initiative = calculateRoll(initiative)
} else if (isNaN(initiative)) {
state.show = "none"
return "\n[Error: Expected a number. See #help]\n"
}
initiative = parseInt(initiative)
var spells = []
var spell = null
var index = 6
do {
spell = getArgument(command, index++)
if (spell != null) spells.push(spell)
} while (spell != null)
var enemy = createEnemy(name, health, ac, hitModifier, damage, initiative)
enemy.spells = spells
var enemyMatches = state.enemies.filter(x => x.name.toLowerCase() == enemy.name.toLowerCase() || x.name.toLowerCase() == `${enemy.name.toLowerCase()} a`)
if (enemyMatches.length > 0) {
enemy.name = getUniqueName(enemy.name)
if (enemy.name.endsWith("A")) {
enemyMatches[0].name = enemy.name
enemy.name = enemy.name.substring(0, enemy.name.length - 1) + "B"
}
}
state.enemies.push(enemy)
state.show = "none"
return `[Enemy ${toTitleCase(enemy.name)} has been created]`
}
function 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")
if (stat == null) character.calculatedInitiative = calculateRoll("d20")
else character.calculatedInitiative = calculateRoll("d20") + getModifier(stat.value)
}
for (enemy of state.enemies) {
if (isNaN(enemy.initiative)) enemy.calculatedInitiative = calculateRoll(enemy.initiative)
else enemy.calculatedInitiative = enemy.initiative
}
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"
}
createInitiativeOrder()
if (state.initiativeOrder.length == 0) {
state.show = "none"
return "\n[Error: No combatants! Ensure that your characters have health and you have added enemies. See #help]\n"
}
state.show = "initiative"
return "\nBattle has commenced!\n"
}
function doFlee(command) {
if (state.initiativeOrder.length == 0) {
state.show = "none"
return "\n[Error: Not in combat. Type #initiative first]\n"
}
var difficulty = getArgument(command, 0)
if (difficulty != null) {
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var difficultyIndex = difficultyNames.indexOf(difficulty)
if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) {
difficulty = difficultyScores[difficultyIndex]
}
} else {
difficulty = state.defaultDifficulty
}
var roll = calculateRoll("d20")
var text = ""
if (difficulty != 0) text += `\n[DC: ${difficulty} Roll: ${roll}]\n`
if (roll >= difficulty) {
state.initiativeOrder = []
text += `\nThe party successfuly flees from battle!\n`
} else text += `\nThe party tries to flee from battle, but fails!\n`
return text
}
function doTurn(command) {
if (state.initiativeOrder.length > 0) state.initiativeOrder.splice(0, 1)
var defeatedEnemies = 0
for (var enemy of state.enemies) {
if (enemy.health > 0) continue
defeatedEnemies++
var index = state.initiativeOrder.findIndex(x => x.name.toLowerCase() == enemy.name.toLowerCase())
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
defeatedCharacters++
var index = state.initiativeOrder.findIndex(x => x.name.toLowerCase() == character.name.toLowerCase())
if (index >= 0) state.initiativeOrder.splice(index, 1)
}
if (state.initiativeOrder.length == 0) createInitiativeOrder()
if (state.initiativeOrder.length == 0) {
return "\nDraw! All combatants have been incapacitated.\n"
}
if (defeatedEnemies == state.enemies.length) {
state.initiativeOrder = []
return "\nVictory! The party has defeated all opponents.\n"
}
if (defeatedCharacters == state.characters.length) {
state.initiativeOrder = []
return "\nDefeat! The entire party has been incapacitated.\n"
}
return executeTurn(state.initiativeOrder[0])
}
function doRepeatTurn(command) {
return executeTurn(state.initiativeOrder[0])
}
function doBlock(command) {
if (state.blockCharacter == null) {
state.show = "none"
return "\n[Error: No attack to block. See #help]\n"
}
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"
}
character.health = state.blockPreviousHealth
var properName = toTitleCase(character.name)
state.show = "prefix"
state.prefix = `[${properName} has ${character.health} health]`
return `\nHowever, the damage to ${properName} was blocked!\n`
}
function doTake(command) {
var itemIndex = 0
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (arg0 == "the") {
var tempArg = getArgument(command, 1)
if (tempArg != null && !isNaN(tempArg)) {
arg0 = tempArg
itemIndex++
}
}
if (!isNaN(arg0)) itemIndex++
const item = {
quantity: isNaN(arg0) ? 1 : arg0,
name: getArgumentRemainder(command, itemIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true)
}
var character = getCharacter()
var commandName = getCommandName(command)
var commandNamePlural = commandName.plural(character.name == "You")
var haveWord = character.name == "You" ? "have" : "has"
var displayItemName = item.name.plural(item.quantity == 1)
if (item.quantity < 0) item.quantity = 1
var text = "\n"
if (item.quantity == 1) text += `${character.name} ${commandNamePlural} ${displayItemName.toLowerCase().startsWith("the ") ? "" : "the "}${displayItemName}.\n`
else text += `${character.name} ${commandNamePlural} ${item.quantity} ${displayItemName}.\n`
var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase())
if (index == -1) {
character.inventory.push(item)
} else {
var existingItem = character.inventory[index]
existingItem.quantity = parseInt(existingItem.quantity) + parseInt(item.quantity)
displayItemName = existingItem.name.plural(existingItem.quantity == 1)
text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n`
}
return text
}
function 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)
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var characterNameAdjustedCase = character.name == "You" ? "you" : character.name
var dontWord = character.name == "You" ? "don't" : "doesn't"
var haveWord = character.name == "You" ? "have" : "has"
var tryWord = character.name == "You" ? "try" : "tries"
var itemArgIndex = 0
if (isNaN(arg0)) {
if (allSynonyms.indexOf(arg0.toLowerCase()) > -1) {
arg0 = Number.MAX_SAFE_INTEGER
itemArgIndex++
} else {
arg0 = 1
}
} else {
itemArgIndex++
}
const item = {
quantity: arg0,
name: getArgumentRemainder(command, itemArgIndex).replace(/^((the)|(a)|(an))\s/, "").plural(true)
}
var displayItemName = item.name.plural(item.quantity == 1)
if (item.quantity < 0) item.quantity = 1
var text = "\n"
var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase())
if (index == -1) {
if (item.quantity == 1) text += `${character.name} ${tryWord} to ${commandName} the ${displayItemName}, but ${character.name} ${dontWord} have any.`
else text += `${character.name} ${tryWord} to ${commandName} ${item.quantity == Number.MAX_SAFE_INTEGER ? arg0 : item.quantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.`
} else {
var existingItem = character.inventory[index]
if (existingItem.quantity == 1) text = `\n${character.name} ${commandName.plural(character.name == "You")} the ${displayItemName.plural(true)}.\n`
else if (parseInt(item.quantity) >= parseInt(existingItem.quantity)) text = `${character.name} ${commandName.plural(character.name == "You")} all ${existingItem.quantity} of the ${displayItemName}.`
else text = `\n${character.name} ${commandName.plural(character.name == "You")} ${item.quantity} ${displayItemName}. \n`
existingItem.quantity -= item.quantity
if (existingItem.quantity <= 0) {
existingItem.quantity = 0
character.inventory.splice(index, 1)
}
if (existingItem.quantity > 0) {
displayItemName = existingItem.name.plural(existingItem.quantity == 1)
text += `${character.name} now ${haveWord} ${existingItem.quantity} ${displayItemName}.\n`
}
}
return text
}
function doGive(command) {
var character = getCharacter()
var commandName = getCommandName(command)
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = getArgument(command, 1)
if (arg1 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var foundAll = allSynonyms.indexOf(arg1) > -1
const item = {
quantity: !isNaN(arg1) ? arg1 : foundAll ? Number.MAX_SAFE_INTEGER : 1,
name: getArgumentRemainder(command, isNaN(arg1) && !foundAll ? 1 : 2).replace(/^((the)|(a)|(an)|(of the))\s/, "").plural(true)
}
var otherCharacter = getCharacter(arg0)
if (otherCharacter == null || otherCharacter.name == "You" && arg0.toLowerCase() != "you") {
state.show = "none"
return "\n[Error: Target character does not exist. See #characters]\n"
}
var characterNameAdjustedCase = character.name == "You" ? "you" : character.name
var dontWord = character.name == "You" ? "don't" : "doesn't"
var haveWord = character.name == "You" ? "have" : "has"
var tryWord = character.name == "You" ? "try" : "tries"
var otherHaveWord = otherCharacter.name == "You" ? "have" : "has"
var otherNameAdjustedCase = otherCharacter.name == "You" ? "you" : otherCharacter.name
var displayItemName = item.name.plural(item.quantity == 1)
var characterQuantityText = ""
if (item.quantity < 0) item.quantity = 1
var text = "\n\n"
var index = character.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase())
if (index == -1) {
if (item.quantity == 1) text += `${character.name} ${tryWord} to ${commandName.plural(true)} the ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.`
else text += `${character.name} ${tryWord} to ${commandName.plural(true)} ${item.quantity == Number.MAX_SAFE_INTEGER ? arg0 : item.quantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.`
return text + "\n\n"
} else {
var existingItem = character.inventory[index]
if (item.quantity >= existingItem.quantity) {
item.quantity = existingItem.quantity
existingItem.quantity = 0
character.inventory.splice(index, 1)
} else {
existingItem.quantity -= item.quantity
}
if (existingItem.quantity > 0) {
characterQuantityText = ` ${character.name} now ${haveWord} ${existingItem.quantity} ${existingItem.name.plural(existingItem.quantity == 1)}.`
} else if (item.quantity > 1) {
characterQuantityText = ` ${character.name} ${dontWord} have any more.`
}
}
if (item.quantity == 1) text += `${character.name} ${commandName.plural(character.name == "You")} ${otherNameAdjustedCase} the ${displayItemName}.`
else text += `${character.name} ${commandName.plural(character.name == "You")} ${otherNameAdjustedCase} ${item.quantity} ${displayItemName}.`
var otherIndex = otherCharacter.inventory.findIndex((element) => element.name.toLowerCase() == item.name.toLowerCase())
if (otherIndex == -1) {
otherCharacter.inventory.push(item)
} else {
var existingItem = otherCharacter.inventory[otherIndex]
existingItem.quantity = parseInt(existingItem.quantity) + parseInt(item.quantity)
displayItemName = existingItem.name.plural(existingItem.quantity == 1)
text += ` ${otherCharacter.name} now ${otherHaveWord} ${existingItem.quantity} ${displayItemName}.`
}
return text + characterQuantityText + "\n\n"
}
function doBuy(command) {
var character = getCharacter()
command = command.replaceAll(/\s+((for)|(with)|(the)|(a)|(an))\s+/g, " ")
var args = []
args.push(getArgument(command, 0))
if (args[0] == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
args.push(getArgument(command, 1))
if (args[1] == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
args.push(getArgument(command, 2))
args.push(getArgument(command, 3))
var buyQuantity
if (isNaN(args[0])) {
buyQuantity = 1
} else {
buyQuantity = args[0]
args.splice(0, 1)
}
var buyName
buyName = args[0].plural(true)
var sellQuantity
if (isNaN(args[1])) {
sellQuantity = 1
} else {
sellQuantity = args[1]
args.splice(1, 1)
}
var sellName = args[1].plural(true)
var characterNameAdjustedCase = character.name == "You" ? "you" : character.name
var dontWord = character.name == "You" ? "don't" : "doesn't"
var haveWord = character.name == "You" ? "have" : "has"
var tryWord = character.name == "You" ? "try" : "tries"
var tradeWord = character.name == "You" ? "trade" : "trades"
var buyWord = character.name == "You" ? "buy" : "buys"
var displayItemName = sellName.plural(sellQuantity == 1)
var buyItemTotal = 0;
var sellItemTotal = 0;
if (sellQuantity < 0) sellQuantity = 1
var text = "\n\n"
var index = character.inventory.findIndex((element) => element.name.toLowerCase() == sellName.toLowerCase())
if (index == -1) {
if (sellQuantity == 1) text += `${character.name} ${tryWord} to trade the ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.`
else text += `${character.name} ${tryWord} to trade ${sellQuantity} ${displayItemName}, but ${characterNameAdjustedCase} ${dontWord} have any.`
return text + "\n\n"
} else {
var existingItem = character.inventory[index]
if (sellQuantity >= existingItem.quantity) {
sellQuantity = existingItem.quantity
existingItem.quantity = 0
character.inventory.splice(index, 1)
} else {
existingItem.quantity -= sellQuantity
}
sellItemTotal = existingItem.quantity
}
var suffix = `${buyQuantity} ${buyName.plural()}`
if (buyQuantity == 1) suffix = `the ${buyName.plural(true)}`
if (sellQuantity == 1) text += `${character.name} ${tradeWord} the ${displayItemName} for ${suffix}.`
else text += `${character.name} ${tradeWord} ${sellQuantity} ${displayItemName} for ${suffix}.`
index = character.inventory.findIndex((element) => element.name.toLowerCase() == buyName.toLowerCase())
if (index == -1) {
character.inventory.push({name: buyName, quantity: buyQuantity})
buyItemTotal = buyQuantity
} else {
var existingItem = character.inventory[index]
existingItem.quantity = parseInt(existingItem.quantity) + parseInt(buyQuantity)
buyItemTotal = existingItem.quantity
}
text += ` ${character.name} now ${haveWord} ${sellItemTotal} ${sellName.plural(sellItemTotal == 1)} and ${buyItemTotal} ${buyName.plural(buyItemTotal == 1)}.`
return text + "\n\n"
}
function doSell(command) {
command = command.replace(/\s+((for)|(with))\s+/, " ")
var args = []
args.push(getArgument(command, 0))
if (args[0] == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
args.push(getArgument(command, 1))
if (args[1] == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
args.push(getArgument(command, 2))
args.push(getArgument(command, 3))
var sellQuantity
if (isNaN(args[0])) {
sellQuantity = 1
} else {
sellQuantity = args[0]
args.splice(0, 1)
}
var sellName
sellName = args[0]
var buyQuantity
if (isNaN(args[1])) {
buyQuantity = 1
} else {
buyQuantity = args[1]
args.splice(1, 1)
}
var buyName = args[1]
return doBuy(`buy ${buyQuantity} ${buyName} ${sellQuantity} ${sellName}`)
}
function doRenameItem(command) {
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var arg1 = getArgument(command, 1)
if (arg1 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var commandName = getCommandName(command)
var character = getCharacter()
var haveWord = character.name == "You" ? "have" : "has"
var possessiveName = getPossessiveName(character.name)
state.show = "none"
var text = `\n[${possessiveName} ${arg0} has been renamed to ${arg1}]\n`
var index = character.inventory.findIndex((element) => element.name.toLowerCase() == arg0.toLowerCase())
if (index >= 0 ) {
var existingItem = character.inventory[index]
existingItem.name = arg1
}
return text
}
function doInventory(command) {
state.show = "inventory"
return " "
}
function doLearnSpell(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == "") {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var character = getCharacter()
var tryWord = character.name == "You" ? "try" : "tries"
var found = character.spells.find((element) => element == arg0)
if (found != null) return `\n[${character.name} ${tryWord} to learn the spell ${arg0}, but already knows it]\n`
character.spells.push(arg0)
addStoryCard(arg0, "", "spell")
return `\n${character.name} learned the spell ${toTitleCase(arg0)}.\n`
}
function doForgetSpell(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == "") {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var dontWord = character.name == "You" ? "don't" : "doesn't"
var tryWord = character.name == "You" ? "try" : "tries"
var found = character.spells.find(x => x.toLowerCase() == arg0.toLowerCase())
if (found == null) {
state.show = "none"
return `\n[${character.name} ${tryWord} to forget the spell ${arg0}, but ${character.name} ${dontWord} even know it]\n`
}
var index = character.spells.findIndex(x => x.toLowerCase() == arg0.toLowerCase())
character.spells.splice(index, 1)
state.show = "none"
return `\n[${character.name} forgot the spell ${arg0}]\n`
}
function doRemoveStat(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == "") {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var dontWord = character.name == "You" ? "don't" : "doesn't"
var tryWord = character.name == "You" ? "try" : "tries"
var found = character.stats.find((element) => element == arg0)
if (found == null) return `\n[${character.name} ${tryWord} to remove the ability ${arg0}, but ${character.name} ${dontWord} even know it]\n`
var index = character.stats.findIndex((element) => element.toLowerCase() == arg0.toLowerCase())
character.stats.splice(index, 1)
return `\n[${character.name} removed the ability ${arg0}]\n`
}
function doRemoveSkill(command) {
var character = getCharacter()
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == "") {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var dontWord = character.name == "You" ? "don't" : "doesn't"
var tryWord = character.name == "You" ? "try" : "tries"
var found = character.skills.find((element) => element == arg0)
if (found == null) return `\n[${character.name} ${tryWord} to remove the skill ${arg0}, but ${character.name} ${dontWord} even know it]\n`
var index = character.skills.findIndex((element) => element.toLowerCase() == arg0.toLowerCase())
character.skills.splice(index, 1)
return `\n[${character.name} removed the skill ${arg0}]\n`
}
function doCastSpell(command) {
const advantageNames = ["normal", "advantage", "disadvantage"]
const difficultyNames = ["impossible", "extreme", "hard", "medium", "easy", "effortless", "veryeasy", "very easy", "automatic", "auto"]
const difficultyScores = [30, 25, 20, 15, 10, 5, 5, 5, 0, 0]
var character = getCharacter()
const dontWord = character.name == "You" ? "don't" : "doesn't"
const tryWord = character.name == "You" ? "try" : "tries"
var usingDefaultDifficulty = false
var spellIndex = 2;
var advantage = searchArgument(command, arrayToOrPattern(advantageNames), spellIndex - 1)
if (advantage == null) {
advantage = "normal"
spellIndex--
}
const difficultyPatternNames = [...new Set(difficultyNames)]
difficultyPatternNames.push("\\d+")
var difficulty = searchArgument(command, arrayToOrPattern(difficultyPatternNames), spellIndex - 1)
if (difficulty == null) {
difficulty = state.defaultDifficulty
usingDefaultDifficulty = true
spellIndex--
}
var difficultyIndex = difficultyNames.indexOf(difficulty)
if (difficultyIndex >= 0 && difficultyIndex < difficultyNames.length) {
difficulty = difficultyScores[difficultyIndex]
}
var spell = getArgument(command, spellIndex)
if (spell == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
var targetText = null
var atWord = null
var found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase())
if (found != null) {
targetText = getArgumentRemainder(command, spellIndex + 1)
if (targetText != null) {
targetText = targetText.trim()
if (!/^((at)|(on))\s+.*/.test(targetText)) targetText = "at " + targetText
}
} else {
var remainder = getArgumentRemainder(command, spellIndex)
if (/.*\s((at)|(on))\s.*/i.test(remainder)) {
spell = remainder.replace(/\s+((at)|(on)).*/i, "").trim()
targetText = remainder.replace(/^.*\s+(?=(at)|(on))/i, "").trim()
} else {
spell = getArgumentRemainder(command, spellIndex).trim()
}
found = character.spells.find(x => x.toLowerCase() == spell.toLowerCase())
}
if (found == null) {
state.show = "none"
return `\n[${toTitleCase(character.name)} ${tryWord} to cast the spell ${spell}, but ${character.name == "You" ? "you" : toTitleCase(character.name)} ${dontWord} know it]\n`
}
var text = `${character.name} cast the spell ${spell}${advantage != "normal" ? " with " + advantage : ""}${targetText == null ? "" : " " + targetText}.`
var modifier = 0
if (character.spellStat != null) {
var stat = character.stats.find((element) => element.name.toLowerCase() == character.spellStat.toLowerCase())
if (stat != null) modifier = getModifier(stat.value)
}
var roll1 = calculateRoll("d20")
var roll2 = calculateRoll("d20")
var roll = advantage == "advantage" ? Math.max(roll1, roll2) : advantage == "disadvantage" ? Math.min(roll1, roll2) : roll1
var enemyString = ""
var allyString = ""
if (targetText != null && state.initiativeOrder.length > 0) {
var foundEnemy
for (var enemy of state.enemies) {
if (targetText.toLowerCase().includes(enemy.name.toLowerCase())) {
foundEnemy = enemy
break
}
}
if (foundEnemy == null) {
var indexMatches = targetText.match(/(?<=enemy\s*)\d+/gi)
if (indexMatches != null) {
foundEnemy = state.enemies[parseInt(indexMatches[0]) - 1]
targetText = targetText.replace(/enemy\s*d+/gi, foundEnemy.name)
}
}
var 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)
if (damageMatches != null) damage = roll == 20 ? calculateRoll(damageMatches[0]) + calculateRoll(damageMatches[0]) : calculateRoll(damageMatches[0])
else {
damageMatches = targetText.match(/\d+/g)
if (damageMatches != null) damage = roll == 20 ? parseInt(damageMatches[damageMatches.length - 1]) * 2 : parseInt(damageMatches[damageMatches.length - 1])
}
if (foundEnemy != null) {
if (usingDefaultDifficulty) difficulty = foundEnemy.ac
if (roll == 20 || roll + modifier >= difficulty) {
if (roll == 20) enemyString += `\nCritical Damage: ${damage}\n`
else enemyString += `\nDamage: ${damage}\n`
state.blockCharacter = foundEnemy
state.blockPreviousHealth = foundEnemy.health
foundEnemy.health = Math.max(0, foundEnemy.health - damage)
if (foundEnemy.health == 0) enemyString += ` ${toTitleCase(foundEnemy.name)} has been defeated!\n`
else enemyString += ` ${toTitleCase(foundEnemy.name)} has ${foundEnemy.health} health remaining!\n`
}
}
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"
var dieText = advantage == "advantage" || advantage == "disadvantage" ? `${advantage}(${roll1},${roll2})` : roll1
var difficultyWord = targetText == null ? "Difficulty" : "Armor"
if (difficulty == 0) state.prefix = ""
else if (roll == 20) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. Critcal Success!]\n`
else if (roll == 1) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. Critcal Failure!]\n`
else if (modifier != 0) state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}${modifier > 0 ? "+" + modifier : modifier}=${roll + modifier}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n`
else state.prefix = `\n[${difficultyWord} Class: ${difficulty}. Roll: ${dieText}. ${roll + modifier >= difficulty ? "Success!" : "Failure!"}]\n`
if (roll == 20) text += ` Critical success!`
else if (roll == 1) text += ` Critical failure! The spell ${targetText != null ? "misses" : "fails"} in a spectacular way.`
else if (roll + modifier >= difficulty) text += ` The spell ${targetText != null ? "hits the target" : "is successful"}!`
else text += ` The spell ${targetText != null ? "misses" : "fails"}!`
if (enemyString != null) text += enemyString
if (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`
}
function doShowCharacters(command) {
state.show = "characters"
return " "
}
function doSpellbook(command) {
state.show = "spellbook"
return " "
}
function doShowSkills(command) {
state.show = "skills"
return " "
}
function doShowStats(command) {
state.show = "stats"
return " "
}
function doClearNotes(command) {
state.notes = []
state.show = "clearNotes"
return " "
}
function doClearInventory(command) {
var character = getCharacter()
character.inventory = []
state.show = "clearInventory"
return " "
}
function doEraseNote(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) arg0 = 1
var list = arg0.split(/\D+/)
list.sort(function(a, b) {
return b - a;
});
var text = "\n"
list.forEach(x => {
var num = parseInt(x) - 1
if (num >= state.notes.length) {
state.show = "none"
return `\n[Error: Note ${x} does not exist. Type #shownotes]\n`
}
state.notes.splice(num, 1)
text += `[Note #${x} removed]\n`
})
state.show = "none"
return text
}
function doRemoveCharacter(command) {
var arg0 = getArgumentRemainder(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
for (var i = 0; i < state.characters.length; i++) {
var character = state.characters[i]
if (character.name.toLowerCase() == arg0.toLowerCase()) {
state.characters.splice(i, 1)
state.show = "none"
return `[Character ${character.name} removed]`
}
}
return `[Character ${arg0} was not found]`
}
function doGenerateName(command) {
var gender = searchArgument(command, /^(male)|(female)$/gi)
if (gender == null) gender = "male"
var genre = searchArgument(command, /^(fantasy)|(modern)|(scifi)|(nordic)$/gi)
if (genre == null) genre = "fantasy"
state.show = "none"
return `[The character's name is ${generateName(genre, gender.toLowerCase() == "male")}]`
}
function doClearSpells(command) {
var character = getCharacter()
character.spells = []
state.show = "clearSpells"
return " "
}
function doClearStats(command) {
var character = getCharacter()
character.stats = []
state.show = "clearStats"
return " "
}
function doClearSkills(command) {
var character = getCharacter()
character.skills = []
state.show = "clearSkills"
return " "
}
function doVersion(command) {
state.show = "none"
return `[${version}]`
}
function doSetDamage(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(arg0)) {
character.damage = arg0
} else if (!isNaN(arg0)) {
character.damage = parseInt(arg0)
} else {
state.show = "none"
return "\n[Error: Not a number. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
state.show = "none"
return `\n[${possessiveName} attack damage is set to ${character.damage}]\n`
}
function doSetProficiency(command) {
var character = getCharacter()
var arg0 = getArgument(command, 0)
if (arg0 == null) {
state.show = "none"
return "\n[Error: Not enough parameters. See #help]\n"
}
if (/^\d*d\d+((\+|-)\d+)?$/gi.test(arg0)) {
character.hitModifier = calculateRoll(arg0)
} else if (!isNaN(arg0)) {
character.hitModifier = parseInt(arg0)
} else {
state.show = "none"
return "\n[Error: Not a number. See #help]\n"
}
var possessiveName = getPossessiveName(character.name)
state.show = "none"
return `\n[${possessiveName} proficiency is set to ${character.hitModifier}]\n`
}
function doReset(command) {
state.notes = []
state.characters = []
state.locations = []
state.location = null
state.enemies = null
state.allies = null
state.initiativeOrder = []
state.x = null
state.y = null
state.defaultDifficulty = null
state.autoXp = null
state.day = null
state.show = "reset"
return " "
}
function doHelp(command) {
state.show = "help"
return " "
}
modifier(text)
const modifier = (text) => {
if (state.show == null) return { text }
var character = getCharacter()
var possessiveName = character == null ? null : getPossessiveName(character.name)
var type = history[history.length - 1].type
const originalText = text
text = type != "story" ? "" : history[history.length - 1].text.endsWith("\n") ? "" : "\n"
switch (state.show) {
case "create":
switch (state.createStep) {
case 0:
text += `***CHARACTER CREATION***\nCharacter: ${state.tempCharacter.name}\nWould you like to use a prefab character? (y/n/q to quit)\n`
break
case 1:
text += `What class is your character?\n`
break
case 2:
text += `You rolled the following stat dice: ${state.statDice}\nChoose your abilities in order from highest to lowest\n1. Strength: Physical power and endurance\n2. Dexterity: Agility and coordination\n3. Constitution: Toughness and physique \n4. Intelligence: Reasoning and memory\n5. Wisdom: Judgement and insight\n6. Charisma: Force of personality and persuasiveness\n\nEnter the numbers with spaces between or q to quit.\n`
break
case 3:
text += `What ability is your spell casting ability?\n1. Intelligence\n2. Wisdom\n3. Charisma\n4. Not a spell caster\nq to quit\n`
break
case 4:
text += `Enter a short summary about your character or q to quit\n`
break
case 100:
text += `What character will you choose?\n1. Fighter: A skilled melee warrior specializing in weapons and armor.\n2. Cleric: A follower of a deity that can call on divine power.\n3. Rogue: An expert in stealth, subterfuge, and exploitation.\n4. Ranger: A talented hunter adept in tracking, survival, and animal handling.\n5. Barbarian: Combat expert focused on brute strength and raw fury.\n6. Bard: A musician that can transform song and word into magic.\n7. Druid: Commands the natural world to cast spells and harness its power.\n8. Monk: A martial artist who has mastered melee and unarmed combat.\n9. Paladin: A virtuous holy warrior with expertise in armor and mysticism.\n10. Wizard: An expert in magic ability who found their power through arcane knowledge.\n11. Sorcerer: A masterful spellcaster deriving their power from an innate source.\n12. Warlock: A magic user granted ability by a pact with a powerful patron.\n13. Artificer: An inventor and alchemist capable of imbuing objects with magic.\n\nEnter the number or q to quit.\n`
break
case 500:
text += `${state.tempCharacter.name} the ${state.tempCharacter.className} has been created.\nType #bio to see a summary of your character.\n***********\n`
break;
case null:
text += `[Character creation has been aborted!]\n`
break
}
break
case "setupEnemy":
switch (state.setupEnemyStep) {
case 0:
text += `***ENEMY CREATION***\nWould you like to use a preset enemy? (y/n/q to quit)\n`
break
case 1:
text += `What is the enemy's name? This must be a unique name that has no duplicates in the current encounter. Type q to quit.\n`
break
case 2:
text += `What is the enemy's health? This can be any positive integer or a dice roll (ie. 3d6+5). Type q to quit.\n`
break
case 3:
text += `What is the enemy'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\n`
break
case 4:
text += `What is the enemy's hit modifier? This affects how accurate their attacks are. This can be any integer. 0 is normal accuracy. Type q to quit\n`
break
case 5:
text += `What is the enemy'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.\n`
break
case 6:
text += `What is the enemy'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.\n`
break
case 7:
text += "Enter the name of a spell that the enemy knows. If it can target this spell at a player 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 8:
text += "Enter the name of another spell that the enemy knows. If it can target this spell at a player 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 enemy preset will you choose?\nLevel 1 or lesser\n1. Animated Armor\n2. Awakened Shrub\n3. Brigand\n4. Black Bear\n5. Boar\n6. Cockatrice\n7. Constrictor Snake\n8. Dire Wolf\n9. Ghoul\n10. Giant Centipede\n11. Giant Rat\n12. Giant Wolf Spider\n13. Gnoll\n14. Goblin\n15. Harpy\n16. Hobgoblin\n17. Kobold\n18. Orc\n19. Satyr\n20. Skeleton\n21. Strige\n22. Warhorse\n23. Wolf\n24. Worg`
text += `\n\nLevels 2-5\n25. Zombie\n26. Air Elemental\n27. Basilisk\n28. Berserker\n29. Chuul\n30. Doppelganger\n31. Druid\n32. Earth Elemental\n33. Fire Elemental\n34. Gorgon\n35. Green Hag\n36. Griffon\n37. Hell Hound\n38. Hill Giant\n39. Manticore\n40. Minotaur\n41. Mimic\n42. Ogre\n43. Owlbear\n44. Red Dragon Wyrmling\n45. Spectator\n46. Troll\n47. Wererat\n48. Werewolf\n49. Vampire Spawn\n50. Wight`
text += `\n\nLevels 6-10\n51. Aboleth\n52. Assassin\n53. Chimera\n54. Cloud Giant\n55. Cyclops\n56. Deva\n57. Drider\n58. Frost Giant\n59. Hydra\n60. Insane Mage\n61. Medusa\n62. Shield Guardian\n63. Spirit Naga\n64. Stone Golem\n65. Treant\n66. Young Black Dragon\n67. Young Blue Dragon\n68. Young Brass Dragon\n69. Young Bronze Dragon\n70. Young Copper Dragon\n71. Young Gold Dragon\n72. Young Green Dragon\n73. Young Red Dragon\n74. Young Silver Dragon\n75. Young White Dragon`
text += `\n\nLevels 11-15\n76. Adult Black Dragon\n77. Adult Bronze Dragon\n78. Adult Copper Dragon\n79. Adult Green Dragon\n80. Animated Statue\n81. Arch Mage\n82. Behir\n83. Boneclaw\n84. Deathwolf\n85. Djinni\n86. Drow Inquisitor\n87. Efreeti\n88. Elder Brain\n89. Erinyes\n90. Ice Devil\n91. Jabberwock\n92. Megapede\n93. Mummy Lord\n94. Purple Worm\n95. Remorhaz\n96. Skull Lord\n97. Spider Dragon\n98. Storm Giant\n99. Vampire\n100. Zikran`
text += `\n\nLevels 16 and up\n101. Ancient Black Dragon\n102. Adult Blue Dragon\n103. Adult Gold Dragon\n104. Adult Silver Dragon\n105. Ancient Gold Dragon\n106. Ancient Red Dragon\n107. Androsphinx\n108. Bael\n109. Balor\n110. Baphomet\n111. Cosmic Horror\n112. Death Knight\n113. Demogorgon\n114. Dragon\n115. Drow Matron Mother\n116. Flesh Colossus\n117. Kraken\n118. Iron Golem\n119. Leviathan\n120. Lich\n121. Planetar\n122. Raeleus\n123. Solar\n124. Tarrasque\n125. Zariel`
text += `\n\nHumanoid\n126. Commoner\n127. Bandit\n128. Guard\n129. Cultist\n130. Acolyte\n131. Apprentice\n132. Witch\n133. Buccaneer\n134. Spy\n135. Captain\n136. Bard\n137. Berserker\n138. Priest\n139. Knight\n140. Archer\n141. Warrior\n142. Conjurer\n143. Mage\n144. Assassin\n145. Evoker\n146. Necromancer\n147. Champion\n148. Warlord\n149. Archmage\n150. Archdruid`
text += `\n\nEnter the number or q to quit. If you want to rename the enemy, add a space and type the name\n(ie. 25 Thuggish Zombie B)\n`
break
case 500:
var hashtag = `#addenemy "${state.tempEnemy.name}" ${state.tempEnemy.health} ${state.tempEnemy.ac} ${state.tempEnemy.hitModifier} ${state.tempEnemy.damage} ${state.tempEnemy.initiative}`
for (var spell of state.tempEnemy.spells) {
hashtag += ` "${spell}"`
}
text += `${state.tempEnemy.name} has been created.\nType #initiative to start the battle.\nCopy and paste the following hashtag to create another identical enemy like this:\n${hashtag}\n***********\n`
break;
case null:
text += `[Enemy creation has been aborted!]\n`
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`
text += `Health: ${character.health}/${getHealthMax()}\n`
text += `Armor Class: ${character.ac}\n`
text += `Damage: ${character.damage}\n`
text += `Weapon Proficiency: ${character.proficiency}\n`
text += `Experience: ${character.experience}\n`
text += `Level: ${getLevel(character.experience)}\n`
var nextLevel = getNextLevelXp(character.experience)
text += `Next level at: ${nextLevel == - 1 ? "(at maximum)": nextLevel + " xp"}\n\n`
text += `-ABILITIES-\n`
character.stats.forEach(function(x) {
text += `* ${toTitleCase(x.name)} ${x.value}\n`
})
text += `----\n\n`
text += `-SKILLS-\n`
character.skills.forEach(function(x) {
const stat = character.stats.find(y => y.name.toLowerCase() == x.stat.toLowerCase())
var statModifier = stat != null ? getModifier(stat.value): 0
var totalModifier = x.modifier + statModifier
var modifier = x.modifier
if (statModifier >= 0) statModifier = `+${statModifier}`
if (totalModifier >= 0) totalModifier = `+${totalModifier}`
if (modifier >= 0) modifier = `+${modifier}`
text += `* ${toTitleCase(x.name)} ${totalModifier} = ${toTitleCase(x.stat)} ${statModifier} Proficiency ${modifier}\n`
})
text += `----\n\n`
text += `Melee Ability: ${character.meleeStat == null ? "none" : character.meleeStat}\n\n`
text += `Ranged Ability: ${character.rangedStat == null ? "none" : character.rangedStat}\n\n`
text += `Spellcasting Ability: ${character.spellStat == null ? "none" : character.spellStat}\n\n`
if (character.spellStat != null) {
text += `-SPELLS-\n`
character.spells.forEach(function(x) {
text += `* ${toTitleCase(x)}\n`
})
text += `----\n\n`
}
text += `-INVENTORY-\n`
character.inventory.forEach(function(x) {
text += `* ${x.quantity} ${toTitleCase(x.name.plural(x.quantity == 1))}\n`
})
text += `----\n\n`
text += `Summary: ${character.summary}\n\n`
text += `**************\n\n`
break
case "showNotes":
text += "*** NOTES ***"
var counter = 1
state.notes.forEach(function(x) {
text += `\n${counter++}. ${x}`
})
if (state.notes.length == 0) text += "\nThere are no notes!"
text += "\n**************\n\n"
break
case "clearNotes":
text += "[Notes cleared successfully]\n"
break
case "inventory":
text += `*** ${possessiveName.toUpperCase()} INVENTORY ***`
if (character.inventory.length > 0) {
character.inventory.forEach(function(x) {
text += `\n* ${x.quantity} ${toTitleCase(x.name.plural(x.quantity == 1))}`
})
} else {
text += `\n${possessiveName} inventory is empty!`
}
text += "\n******************\n\n"
break
case "characters":
text += `*** CHARACTERS ***`
if (state.characters.length > 0) {
state.characters.forEach(function(x) {
text += `\n* ${toTitleCase(x.name)} the ${toTitleCase(x.className)}: ${x.summary}`
})
} else {
text += `\n${possessiveName} inventory is empty!`
}
text += "\n******************\n\n"
break
case "spellbook":
text += `*** ${possessiveName.toUpperCase()} SPELLBOOK ***`
if (character.spells.length > 0) {
character.spells.forEach(function(x) {
text += "\n* " + toTitleCase(x)
})
} else {
text += `\n${possessiveName} spellbook is empty!`
}
text += "\n******************\n\n"
break
case "stats":
text += `*** ${possessiveName.toUpperCase()} ABILITIES ***\n`
if (character.stats.length > 0) {
character.stats.forEach(function(x) {
text += `* ${toTitleCase(x.name)} ${x.value}\n`
})
} else {
text += `${character.name} has no abilities!\n`
}
text += "******************\n\n"
break
case "skills":
text += `*** ${possessiveName.toUpperCase()} SKILLS ***\n`
if (character.skills.length > 0) {
character.skills.forEach(function(x) {
const stat = character.stats.find(y => y.name.toLowerCase() == x.stat.toLowerCase())
var statModifier = stat != null ? getModifier(stat.value): 0
var totalModifier = x.modifier + statModifier
var modifier = x.modifier
if (statModifier >= 0) statModifier = `+${statModifier}`
if (totalModifier >= 0) totalModifier = `+${totalModifier}`
if (modifier >= 0) modifier = `+${modifier}`
text += `* ${toTitleCase(x.name)} ${totalModifier} = ${toTitleCase(x.stat)} ${statModifier} Proficiency ${modifier}\n`
})
} else {
text += `${character.name} has no skills!\n`
}
text += "******************\n\n"
break
case "location":
var localLocations = []
for (location of state.locations) {
if (location.x == state.x && location.y == state.y) localLocations.push(location)
}
if (localLocations.length > 0) {
text += "Places of interest here:\n"
for (location of localLocations) {
text += `* ${location.name}\n`
}
} else {
text += " "
}
break
case "locations":
text += `Player location: ${state.location == null ? "" : state.location + " "}(${state.x},${state.y})\n`
text += `*** LOCATIONS ***\n`
locations = state.locations
if (state.sortLocations) {
locations = [...new Set(state.locations)]
locations.sort(function(a, b) {
var distanceA = pointDistance(state.x, state.y, a.x, a.y)
var distanceB = pointDistance(state.x, state.y, b.x, b.y)
return distanceA - distanceB;
});
}
if (locations.length > 0) {
var index = 0
locations.forEach(function(location) {
var distance = pointDistance(state.x, state.y, location.x, location.y).toFixed(1)
text += `${state.sortLocations ? "" : ++index + ". "}${toTitleCase(location.name)} (${location.x},${location.y}) Distance: ${distance}\n`
})
} else {
text += `No locations have been discovered!\n`
}
text += "******************\n\n"
break
case "map":
text += `A 11x11 map of the area surrounding (${state.x},${state.y}):\n`
var map = mapGenerate()
state.locations.forEach(location => {
map = mapReplace(map, location.x, location.y, location.name.substring(0, 1).toUpperCase())
})
map = mapReplace(map, state.x, state.y, "@")
text += map
break
case "none":
text += " "
break
case "prefix":
text = state.prefix + originalText
break
case "prefixOnly":
text = state.prefix
break
case "clearInventory":
text += `[${possessiveName} inventory has been emptied]\n`
break
case "clearSpells":
text += `[${possessiveName} spells have been cleared]\n`
break
case "showEnemies":
text += "*** ENEMIES ***\n"
if (state.enemies.length == 0) {
text += "There are no enemies present here. Type #encounter to generate a scripted set or #addenemy to add your own\n"
} else {
var index = 0
for (var enemy of state.enemies) {
text += `${++index}. ${toTitleCase(enemy.name)} (Health: ${enemy.health} AC: ${enemy.ac} Initiative: ${enemy.initiative})\n`
}
}
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":
text += "*** INITIATIVE ORDER ***\n"
if (state.initiativeOrder.length == 0) {
text += "There is no one in the battle. This makes no sense!"
} else {
var index = 0
for (var character of state.initiativeOrder) {
text += `${++index}. ${toTitleCase(character.name)} (Initiative: ${character.calculatedInitiative})\n`
}
}
text += "******************\n\n"
if (state.initiativeOrder.length > 0) {
state.initiativeOrder = []
text += `[Type #turn]\n`
}
break
case "reset":
text += "[All settings have been reset]\n"
break
case "help":
text += "--Basic Hashtags--"
text += "\n#roll (advantage|disadvantage) (dice_value)"
text += "\n Rolls a die/dice and shows the result. dice_value can be in the following formats 5d20+6 or 5d20 or d20 or 20. The parameters can be listed in any order."
text += "\n#generatename (male|female) (fantasy|modern|scifi|nordic)"
text += "\n Retrieves a random name from a list of names in the specified gender and genre. The parameters can be listed in any order."
text += "\n#shownotes"
text += "\n Shows all the notes."
text += "\n#note (message)"
text += "\n Adds the specified message as a note. If the message is not specified, the last action's text will be saved as a note. Keep in mind that player adventures have a limited amount of state storage (an unspecified amount) so avoid saving too many notes. Quotes are not necessary."
text += "\n#clearnotes"
text += "\n Removes all notes."
text += "\n#removenote value"
text += "\n Removes the specified note as indicated by the number listed in #shownotes. To delete multiple notes, type the numbers with spaces or commas between them. This is safer than calling #removenote multiple times because the numbers shift as notes are deleted."
text += "\n\n--Characters--"
text += "\n#setup"
text += "\n Launches the create character setup."
text += "\n#bio"
text += "\n Shows the character's abilities, skills, spells, inventory, and everything else about this character."
text += "\n#renamecharacter new_name"
text += "\n Renames the character to the new name. All abilities, skills, inventory, etc. will remain the same. Quotes are not necessary."
text += "\n#clonecharacter new_name"
text += "\n Copies the abilities, skills, inventory, etc. of the current character to a new character with the name new_name. Quotes are not necessary."
text += "\n#setclass class"
text += "\n Sets the class of the character for player reference. Quotes are not necessary."
text += "\n#setsummary summary"
text += "\n Sets the summary of the character for player reference. Quotes are not necessary."
text += "\n#sethealth value"
text += "\n Sets the character's health to specified value. It's capped at the character's max health."
text += "\n#heal value or dice_roll (target)"
text += "\n Increases the target enemy's or character's health by the specified value or dice_roll. If a target isn't specified, the character calling the command is healed."
text += "\n#healparty value or dice_roll"
text += "\n Increases the health of all party characters' by the specified value or dice_roll."
text += "\n#damage value or dice_roll (target) "
text += "\n Decreases the target enemy's or character's health by the specified value or dice_roll. If a target isn't specified, the character calling the command is damaged. Reaching 0 causes the target to become \"unconscious\"."
text += "\n#setac value"
text += "\n Sets the armor class of the character. The default is 10"
text += "\n#setdamage value or dice_roll"
text += "\n Sets the default damage that the character causes when attacking. If a dice_roll is specified, a randomized damage will be calculated at the time of the attack. The default is 1d6"
text += "\n#setweaponproficiency value or dice_roll"
text += "\n Sets the weapon proficiency of the character which affects the chance to hit. If a dice_roll is specified, a randomized value is calculated. The default is 2"
text += "\n#rest"
text += "\n Sets all of the characters' health to their maximums. Use #shortrest to only restore half health. This command increases the day counter and displays the number of days since your adventure began."
text += "\n#showday"
text += "\n Shows the number of days since your adventure began."
text += "\n#setday"
text += "\n Sets the number of days since your adventure began."
text += "\n#setxp value"
text += "\n Sets the character's experience to the specified value."
text += "\n#addxp (party) value"
text += "\n Increases the character's experience by the specified value. The player is notified if there is a level up. If the parameter party is specified, xp will be added to the entire party instead. Parameters can be listed in any order."
text += "\n#setautoxp value"
text += "\n Automatically increases the experience of all party members when a #try, #attack, or #cast is called. The amount of experience is scaled based on the difficulty class of the check with any check 20 or higher will result in the maximum specified by value. Set to 0 to disable."
text += "\n#showautoxp"
text += "\n Shows the value of the auto xp."
text += "\n#levelup"
text += "\n Increases the character's experience by the exact amount needed to reach the next level."
text += "\n#setdefaultdifficulty (difficulty_class or automatic|effortless|easy|medium|hard|impossible)"
text += "\n Sets the default difficulty for #check, #try, #attack, and #cast when a difficulty is not specified. The normal default is 10 (easy). If you do not pass any parameters, the default will be set to 10 (easy)."
text += "\n#showdefaultdifficulty"
text += "\n Shows the default difficulty for #check, #try, #attack, and #cast when a difficulty is not specified. The normal default is 10 (easy)."
text += "\n#showcharacters"
text += "\n Lists all current characters and their classes/summaries."
text += "\n#removecharacter name"
text += "\n Removes the character that has the indicated name."
text += "\n\n--Character Checks--"
text += "\n#check (ability|skill) (advantage|disadvantage) (difficulty_class or automatic|effortless|easy|medium|hard|impossible)"
text += "\n Rolls a d20 and compares the result (modified by the character's ability/skill) to the specified difficulty. The parameters can be listed in any order."
text += "\n#try (ability|skill) (advantage|disadvantage) (difficulty_class or automatic|effortless|easy|medium|hard|impossible) task"
text += "\n Attempts to do the task based on the character's ability/skill against the specified difficulty. Quotes are not necessary."
text += "\n#attack (ranged) (advantage|disadvantage) (ac or effortless|easy|medium|hard|impossible) target"
text += "\n Attacks the specified target with a melee (the default) or ranged attack. The roll is compared against the specified AC which will determine if the attack succeeds or misses. If the AC is not specified, the default AC or the AC of the opponent in combat will be used. The parameters can be listed in any order, except the target must be listed last. The target can include the name of the enemy or the word \"enemy\" and the number of the enemy as listed in #enemies. The target can also include a damage amount. If the damage is not specified, the character's default damage is used. Quotes are not necessary.\nExample:\nAstri #attack advantage The Evil Knight for 2d12+2 damage"
text += "\n#cast (advantage|disadvantage) (difficulty_class or effortless|easy|medium|hard|impossible) spell(target)"
text += "\n Character will cast the indicated spell if the spell is in their spellbook. It will be a targeted spell if a target is indicated. The roll is modified by the spell casting ability of the character. You may type a phrase without quotes for spell such as \"cast fire bolt at the giant chicken\". If the difficulty is not specified, the default difficulty or the AC of the opponent in combat will be used. The parameters can be listed in any order, except the target must be listed last. The target can include the name of the enemy or the word \"enemy\" and the number of the enemy as listed in #enemies. The target can also include a damage amount. If the damage is not specified, the character's default damage is used. Quotes are not necessary.\nExample:\nAstri #attack advantage The Evil Knight for 2d12+2 damage"
text += "\n\n--Abilities--"
text += "\n#setability ability value"
text += "\n Adds the ability to the character if necessary and sets it to the specified value. Quotes are required for abilities with spaces."
text += "\n#showabilities"
text += "\n Shows the character's list of abilities."
text += "\n#removeability ability"
text += "\n Removes the ability from the character's list of abilities. Quotes are not necessary."
text += "\n#clearabilities"
text += "\n Removes all abilities from the character."
text += "\n#setspellability ability"
text += "\n Sets the ability that affects the modifier for #cast. Quotes are not necessary."
text += "\n#setmeleeability ability"
text += "\n Sets the character's ability modifier that affects melee attacks. Quotes are not necessary."
text += "\n#setrangedability ability"
text += "\n Sets the character's ability modifier that affects ranged attacks. Quotes are not necessary."
text += "\n\n--Skills--"
text += "\n#setskill skill (ability) value"
text += "\n Adds the skill to the character if necessary, and associates it with the specified ability and value. The ability is optional only if this is an existing skill. New skills need an ability specified."
text += "\n#showskills"
text += "\n Shows the character's list of skills"
text += "\n#removeskill"
text += "\n Removes the skill from the character's list of skills."
text += "\n#clearskills"
text += "\n Removes all skills from the character."
text += "\n\n--Inventory--"
text += "\n#take (quantity) item"
text += "\n Adds the specified quantity of item to the character's inventory. If a quantity is omitted, it's assumed to be 1. The words the, a, and an are ignored. 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"
text += "\n Just like #buy, but with the parameters reversed. Adds the specified buy_quantity of the buy_item to the character's inventory and also removes the sell_quantity of sell_item. If quantities are omitted, they are assumed to be 1. The words for, with, the, a, and an are ignored. Quotes are necessary for items with spaces in the name."
text += "\n#drop (quantity or all|every) item"
text += "\n Removes the specified quantity of item from the character's inventory. If a quantity is omitted, it's assumed to be 1. The words the, a, and an are ignored. 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"
text += "\n Shows the items in the inventory of the character."
text += "\n#clearinventory"
text += "\n Removes all items from the character's inventory."
text += "\n\n--Spells--"
text += "\n#learnspell spell"
text += "\n Adds the specified spell to the character's spellbook. Creates a story card if necessary. Quotes are not necessary."
text += "\n#forgetSpell spell"
text += "\n Removes the specified spell from the character's spellbook. Quotes are not necessary."
text += "\n#clearspells"
text += "\n Removes all spells from the character's spellbook."
text += "\n#spellbook"
text += "\n Shows the list of spells that the character has learned."
text += "\n#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"
text += "\nFollow prompts to create an enemy from a template or completely from scratch. It will be added to the existing encounter if there is one already specified."
text += "\n#encounter (funny|easy|medium|hard|boss|god or cr)"
text += "\n Generate an encounter from the specified list. If a list is not specified, it will default to \"easy\" You can instead provide a number as a challenge rating which will scale encounters from the appropriate list and scale their difficulty."
text += "\n#showenemies"
text += "\n Shows the list of current enemies."
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 #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"
text += "\n Updates the turn to the next character in combat. If it is an enemy, the enemy will attack. If it's a player character, the system will allow the player to take their turn. If there are no enemies left or all the player characters are dead, combat ends."
text += "\n#repeatTurn"
text += "\n Repeats the turn. If it is currently an enemy's turn, it will attack or cast another spell again."
text += "\n#block"
text += "\n Reverses the damage that has been inflicted in the last turn. This applies to damage on characters and enemies."
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."
text += "\n#goto (x) (y) or (location_name) or (location_number)"
text += "\n Makes the party travel to the location specified by the coordinates (as integers), location_name, or location_number. You must provide at least one of these. If the location does not exist, it is created at your current coordinates. If you only specify coordinates, you will go to the first location at those coordinates. Quotes are not necessary."
text += "\n#goto distance location_name"
text += "\n An alternative of the above. Travels the specified distance towards the location indicated by location name. Quotes are not necessary"
text += "\n#gonorth (distance)"
text += "\n The party travels north the given distance (an integer). If distance is not specified, it is assumed to be 1."
text += "\n#gosouth (distance)"
text += "\n The party travels south the given distance (an integer). If distance is not specified, it is assumed to be 1."
text += "\n#goeast (distance)"
text += "\n The party travels east the given distance (an integer). If distance is not specified, it is assumed to be 1."
text += "\n#gowest (distance)"
text += "\n The party travels west the given distance (an integer). If distance is not specified, it is assumed to be 1."
text += "\n#getlocation"
text += "\n Returns the coordinates that the party is at. It will also list a location if a location was specified when using #goto."
text += "\n#showlocations (sort)"
text += "\n Shows a list of all discovered locations with their coordinates and their distance from the party's current location. If the parameter \"sort\" is added, the locations will be listed by their distance to the party. Note that the location numbers will only be displayed in the unsorted list."
text += "\n#removelocation location_name or location_number"
text += "\n Removes the specified location by location_name or location_number as listed in #showlocations. To delete multiple locations, type the numbers with spaces or commas between them. This is safer than calling #removenote multiple times because the numbers shift as locations are deleted. Quotes are not necessary."
text += "\n#clearlocations"
text += "\n Deletes all discovered locations."
text += "\n#map"
text += "\n Generates an 11x11 ASCII map of the surrounding locations centered at the party location. The @ symbol is the party location."
text += "\n\n--Stragedy Card Game--"
text += "\n#basicdeck"
text += "\n The basic cards necessary to enjoy the Stragedy card game are added to the character's inventory."
text += "\n#cardshop"
text += "\n This opens the stragedy card shop where characters can spend gold to purchase cards. The inventory is randomized based on the day."
text += "\n#addcard (card_name or card_rarity)"
text += "\n Adds the specified card or randomly generates a card and adds it to the character's inventory. Specifying a rarity (common, rare, epic, legendary) will choose a random card from those lists. Valid card names are a, ace, j, jack, q, queen, k, king, ?, joker, w, witch, p, priest, b, brigand, and any number 2-10"
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!"
text += "\n\n--Other--"
text += "\n#version"
text += "\n Returns the current version of this scenario."
text += "\n#help"
text += "\n This long ass help menu. I am paid by lines of codes."
break
}
state.show = null
return { text }
}
const mapLineBreak = "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n"
const mapLine = "| * * * * * * * * * * * |\n"
function mapGenerate() {
var map = mapLineBreak
for (var i = 0; i < 11; i++) map += mapLine
map += mapLineBreak
return map
}
function mapReplace(map, x, y, character) {
x += 5 - state.x
y += 5 - state.y
if (x < 0 || x > 10 || y < 0 || y > 10) return map
index = mapLineBreak.length + 6 + x * 6 + y * mapLine.length
map = map.replaceAt(index, 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"
var possessiveName = getPossessiveName(character.name)
var text = " "
switch (state.stragedyTurn) {
case "intro":
text = `**Stragedy**
Welcome to Stragedy! A trading card game of wits, strategy, and tragic outcomes!
Please see the game manual on github for rules, tactics, and a complete tutorial:
github.com/raeleus/Hashtag-DnD/
Type d to deal the cards or press f to forfeit.
`
break
case "game":
var hasJokerOnBattlefield = false
for (var card of state.stragedyPlayerBattlefield) {
if (card.includes("?")) {
hasJokerOnBattlefield = true
break
}
}
var kingCards = new Set()
for(var card of state.stragedyPlayerBattlefield) {
if (card.includes("k")) {
kingCards.add(card.match(/(?<=.*)\d+/gi)[0])
}
}
for(var card of state.stragedyEnemyBattlefield) {
if (card.includes("k")) {
kingCards.add(card.match(/(?<=.*)\d+/gi)[0])
}
}
var priestPoints = 0
for (var card of state.stragedyPlayerBattlefield) {
if (card.includes("p")) {
var number = card.replaceAll(/\D/gi, "")
var value = parseInt(number)
if (kingCards.has(number)) value *= 2
priestPoints += value
}
}
var enemyPriestPoints = 0
for (var card of state.stragedyEnemyBattlefield) {
if (card.includes("p")) {
var number = card.replaceAll(/\D/gi, "")
var value = parseInt(number)
if (kingCards.has(number)) value *= 2
enemyPriestPoints += value
}
}
var enemyBattlefield = state.stragedyEnemyBattlefield.length > 0 ? "" : "No cards!"
state.stragedyEnemyBattlefield.sort()
for (card of state.stragedyEnemyBattlefield) {
enemyBattlefield += `${card}, `
}
if (state.stragedyEnemyBattlefield.length > 0) enemyBattlefield = enemyBattlefield.substring(0, enemyBattlefield.length - 2)
var enemyDeckCount = state.stragedyEnemyDeck.length
var enemyDiscardCount = state.stragedyEnemyDiscard.length
var enemyHandCount = state.stragedyEnemyHand.length
var playerBattlefield = state.stragedyPlayerBattlefield.length > 0 ? "" : "No cards!"
state.stragedyPlayerBattlefield.sort()
for (card of state.stragedyPlayerBattlefield) {
playerBattlefield += `${card}, `
}
if (state.stragedyPlayerBattlefield.length > 0) playerBattlefield = playerBattlefield.substring(0, playerBattlefield.length - 2)
var playerHand = state.stragedyPlayerHand.length > 0 ? "" : "No cards!"
state.stragedyPlayerHand.sort()
for (card of state.stragedyPlayerHand) {
playerHand += `${card}, `
}
if (state.stragedyPlayerHand.length > 0) playerHand = playerHand.substring(0, playerHand.length - 2)
var playerDeckCount = state.stragedyPlayerDeck.length
var playerDiscardCount = state.stragedyPlayerDiscard.length
if (!state.stragedyEnemySkipTurn) text = `-----The Opponent's Turn-----${state.stragedyEnemyTurnText}`
else text = "-----The Opponent's Cards-----\n"
text += `The opponent has ${enemyDeckCount} cards in the deck, ${enemyDiscardCount} in the discard pile, and ${enemyHandCount} in their hand.
-----The Battlefield-----
Opponent's cards on the battlefield: ${enemyBattlefield} = ${state.stragedyEnemyScore} points${enemyPriestPoints > 0 ? `. Blessed points: ${enemyPriestPoints}` : ""}
${possessiveName} cards on the battlefield: ${playerBattlefield} = ${state.stragedyPlayerScore} points${priestPoints > 0 ? `. Blessed points: ${priestPoints}` : ""}
-----${possessiveName} Cards-----
${possessiveName} hand: ${playerHand}
${toTitleCase(character.name)} ${haveWord} ${playerDeckCount} cards in the deck and ${playerDiscardCount} in the discard pile.
-----${possessiveName} Turn-----`
if (state.stragedyPlayerHand.length > 0) text += `
Play a number card by typing its number. Play a letter card by typing the letter and target card.
Type d and a number/letter to discard. ${!hasJokerOnBattlefield ? "Type r to retire. " : ""}Type f to forfeit.
`
else text += `
Type d to draw a card. ${!hasJokerOnBattlefield ? "Type r to retire. " : ""}Type f to forfeit.
`
if (state.stragedyPlayerScore > 30) text += `WARNING: You must lower your score below 30 or you will bust!\n`
break
case "gameOver":
text = ""
if (state.stragedyWinner != "forfeit" && state.stragedyEnemyTurnText != null) text += state.stragedyEnemyTurnText
text += `The battle has concluded.${state.stragedyWinner != "forfeit" ? `\nFinal scores:\n${character.name}: ${state.stragedyPlayerScore}\nOpponent: ${state.stragedyEnemyScore}`: ""}
`
if (state.stragedyWinner == "player") text += `${toTitleCase(character.name)} ${haveWord} won! Congratulations.`
else if (state.stragedyWinner == "enemy" || state.stragedyWinner == "forfeit") text += `${toTitleCase(character.name)} ${haveWord} lost! Better luck next time.`
else text += `${toTitleCase(character.name)} and the opponent have tied! Try again.`
break
}
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")
var gold = goldIndex == -1 ? 0 : character.inventory[goldIndex].quantity
var text = " "
var seed = state.day
if (state.cardDeals == null) {
state.cardDeals = ["2", "3", "4", "5", "6", "7", "8", "9"]
state.cardPrices = [400, 400, 400, 400, 400, 400, 400, 400, 2000]
var items = ["10", "Ace", "Jack"]
state.cardDeals.push(items[Math.floor(getRandom(seed) * 3)])
seed += 100
if (getRandom(seed) > .6) {
items = ["Queen", "King", "Joker"]
state.cardDeals.push(getRandomFromList(items[Math.floor(getRandom(seed) * 3)]))
seed += 100
state.cardPrices.push(5000)
}
if (getRandom(seed) > .9) {
items = ["Witch", "Priest", "Brigand"]
state.cardDeals.push(getRandomFromList(items[Math.floor(getRandom(seed) * 3)]))
seed += 100
state.cardPrices.push(12000)
}
}
switch (state.stragedyShopStep) {
case 0:
text = `**Welcome to the Stragedy Shop**
Deals change every day!`
break
case 1:
text = "Card purchased!"
break
case 2:
text = "You do not have enough gold!"
}
switch (state.stragedyShopStep) {
case 0:
case 1:
case 2:
text += `
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 ${numberWithCommas(state.cardPrices[i])} gold\n`
}
text += `
You have ${numberWithCommas(gold)} gold
Enter the number or q to quit:
`
break
case 500:
text = "Thank you for shopping at the Stragedy Shop!"
break
}
return text
}
modifier(text)
// Library Script
// Story Arc Engine Script by Yi1i1i
/* Credits:
LewdLeah - Idea for AI calling, debugging, testing, feedback
Purplejump - Testing, feedback
*/
onLibrary_SAE();
function onLibrary_SAE(){
// Update settingsSC at start of every hook
createIfNoSettingsSC();
retrieveSettingsFromSC();
storeSettingsToSC();
// Update ArcSC at the start of every hook
createIfNoArcSC();
retrieveArcFromSC();
storeArcToSC();
}
function onInput_SAE(text){
text = helpCommandInput(text);
text = detectRedoStoryArc(text);
text = detectStopGenerating(text);
return text;
}
function onContext_SAE(text){
text = removeAngleText(text);
text = feedAIPrompt(text);
text = feedStoryArc(text);
text = logContextToSettingsSC(text);
//log(text);
return text;
}
function onOutput_SAE(text) {
text = helpCommandOutput(text);
text = saveStoryArc(text);
//log("state.storyArc", state.storyArc);
text = callAIForArc(text);
//
log(text);
turnCounter();
return text;
}
function helpCommandInput(text){
if(text.includes("/help sae")){
text = " ";
state.commandCenter =
`
<<
- Story Arc Engine calls the AI to create a story arc in the Author's notes to better guide future storytelling.
- Type "Story Arc" into story cards to access and modify settings. Logs are logged in the notes.
- Input "/redo arc" to call the AI to regen the story arc.
- Text encased in << >> are auto cleared from context.
- Repeated attempts for generating story arcs may be due to AI failing to fulfill instructions or low response length (< 125). troubleshoot by stopping and retrying in a few turns.
>>
`
}
return text;
}
function helpCommandOutput(text){
if(state.commandCenter){
text = state.commandCenter;
}
delete state.commandCenter
return text;
}
// Prompt to be fed to AI context
state.arcPrompt = state.arcPrompt || [`
<<</SYSTEM>
- Stop the story.
- Only write a structured story arc outline for the future based on everything so far by following these strict instructions:
- Write a numbered list of 11 major events within the story arc.
- Each event must be under 7 words.
- Events must be in chronological order.
- Each event must build on the last and be further in the future.
- Dont write clichés, dialogue, description, and prose.
- Dont write the protagonist, main character, and player.
- Use only brief, high-level story developments.
- Events contain turning points, twists, discoveries, conflicts, motives, and lore.
- Maintain immersion and consistent narrative tone. >>`
];
// Initialize variables
if(state.unlockFeedAIPrompt == undefined){
state.unlockFeedAIPrompt = false;
}
if(state.saveOutput == undefined){
state.saveOutput = false;
}
if(state.storyArc == undefined){
state.storyArc = "";
}
if(state.attemptCounter == undefined){
state.attemptCounter = 0;
}
state.turnsPerAICall = state.turnsPerAICall || 25;
log("state.turnsPerAICall: " + state.turnsPerAICall);
// Increment turn counter at end of onOutput
function turnCounter(){
if (state.turnCount == undefined) {
state.turnCount = 0;
}
state.turnCount += 1;
log("state.turnCount: " + state.turnCount);
}
// Remove script texts to clean AI context
function removeAngleText(text) {
return text.replace(/<<[\s\S]*?>>/g, '');
}
function createIfNoArcSC(){
if (!storyCards.find(sc => sc.title === "Current Story Arc")) {
// If sc doesn't exist, create it
addStoryCard("Current Story Arc", "", "Current Story Arc");
// Fetch the sc
const arcSC = storyCards.find(sc => sc.title === "Current Story Arc");
arcSC.keys = "/Current Story Arc"
arcSC.description = "SPOILERS! This story card stores the story arc being fed to the AI to improve storytelling. Feel free to modify the contents.";
}
}
function storeArcToSC(){
// Fetch the sc
const arcSC = storyCards.find(sc => sc.title === "Current Story Arc");
arcSC.entry = state.storyArc;
}
function retrieveArcFromSC(){
// Fetch the sc
const arcSC = storyCards.find(sc => sc.title === "Current Story Arc");
state.storyArc = arcSC.entry;
}
function createIfNoSettingsSC(){
if (!storyCards.find(sc => sc.title === "Story Arc Settings")) {
// If sc doesn't exist, create it
addStoryCard("Story Arc Settings", "", "Story Arc Settings");
// Fetch the sc
const settingsSC = storyCards.find(sc => sc.title === "Story Arc Settings");
settingsSC.description = `
turnsPerAICall: Number of turns before calling AI to update the story arc. Takes in an integer.
arcPrompt: Prompt that is fed to the AI to generate a story arc. Must be encased in << >>.
`;
}
}
function storeSettingsToSC(){
// Fetch the sc
const settingsSC = storyCards.find(sc => sc.title === "Story Arc Settings");
settingsSC.entry = `turnsPerAICall = ${state.turnsPerAICall}\narcPrompt = ${state.arcPrompt}`
}
function retrieveSettingsFromSC(){
// Fetch the sc
const settingsSC = storyCards.find(sc => sc.title === "Story Arc Settings");
// Extract turnsPerAICall
const turnsMatch = settingsSC.entry.match(/turnsPerAICall\s*=\s*(\d+)/);
if (turnsMatch) {
state.turnsPerAICall = Number(turnsMatch[1]) ?? state.turnsPerAICall;
}
// Extract arcPrompt block
const promptMatch = settingsSC.entry.match(/arcPrompt\s*=\s*(<<[\s\S]*?>>)/);
if (promptMatch) {
state.arcPrompt = promptMatch[1];
}
}
// On output, waits for the correct turn to call AI for generating story arc
function callAIForArc(text){
if (state.turnCount == 1 || state.turnCount % state.turnsPerAICall === 0) {
// Warn player of AI call next turn
text = text + "\n\n<< ⚠️ Updating Story Arc Next Turn! Click 'Continue' or type '/stop'. >>";
// Unlock feed prompt to AI for onContext
state.unlockFeedAIPrompt = true;
log("state.unlockFeedAIPrompt: " + state.unlockFeedAIPrompt);
// Unlock save resulting output to save story arc for next onOutput
state.saveOutput = true;
log("state.saveOutput: " + state.saveOutput);
}
return text;
}
// After AI is called, this function will feed the prompt onContext for AI to create a story arc
function feedAIPrompt(text){
if(state.unlockFeedAIPrompt){
text = text + " " + state.arcPrompt;
// Turn off after done feeding
state.unlockFeedAIPrompt = false;
}
return text;
}
// After AI call and prompt is fed to context, this function saves the generated story arc during the following output hook
function saveStoryArc(text){
if(state.saveOutput){
// Copy the generated story arc from the output text
state.storyArc = text;
// Clean story arc text to ensure no incomplete numbered lines
log("Before: ", state.storyArc);
state.storyArc = state.storyArc.replace(/\n?\d+\.\s*$/, '');
state.storyArc = state.storyArc
.split('\n')
.filter(line => /^\d+\.\s/.test(line.trim()))
.join('\n');
log("After: ", state.storyArc);
// Incorrect story arc formatting recalls AI
if(!/[89]/.test(state.storyArc)){
state.unlockFeedAIPrompt = true;
state.saveOutput = true;
state.attemptCounter += 1;
text = `\n<< ⏳ Generating Story Arc (Attempt ${state.attemptCounter})... Click 'Continue' or type '/stop'. >>`;
}
// Correct story arc formatting gets saved
else {
state.attemptCounter = 0;
state.storyArc = "Write the story in the following direction:\n" + state.storyArc;
text = "\n<< ✅ Story Arc generated and saved! Click 'Continue'. >>\n\n";
// Fetch the sc and log the previous arc in sc notes
const arcSC = storyCards.find(sc => sc.title === "Current Story Arc");
arcSC.description = `Log ${state.turnCount} | Previous Story Arc:\n${arcSC.entry}\n` + arcSC.description;
// Save the new story arc to the sc
storeArcToSC();
// Turn off save output when done saving story arc
state.saveOutput = false;
}
}
return text;
}
// Feeds the Story Arc into the Author's Note in the AI context every turn
function feedStoryArc(text){
// Ensure story arc is fed only when a new story arc is not being generated
if(state.saveOutput == false){
text = text.replace(
/(\[Author's note: [\s\S]*?)(])/,
(_, noteStart, noteEnd) => noteStart + "\n" + state.storyArc + noteEnd
);
}
return text;
}
function detectRedoStoryArc(text){
if(text.includes("/redo arc")){
state.unlockFeedAIPrompt = true;
state.saveOutput = true;
text = "<< ➰ Regenerating Story Arc... >>"
}
return text;
}
// Function to allow player to stop story arc generating
function detectStopGenerating(text){
if(text.includes("/stop") && state.unlockFeedAIPrompt == true){
state.unlockFeedAIPrompt = false;
state.saveOutput = false;
state.attemptCounter = 0;
text = "<< ⛔ Story Arc Generation Stopped. >>";
}
return text;
}
function logContextToSettingsSC(text){
// Fetch the sc
const settingsSC = storyCards.find(sc => sc.title === "Story Arc Settings");
// Trim notes on char limit to prevent memory overfill
if(settingsSC.description.length > 5000){
halfIndex = Math.floor(settingsSC.description.length / 2);
settingsSC.description = settingsSC.description.slice(0, halfIndex);
console.log("Trimming description to prevent memory overload.");
}
// Log to setting sc notes
settingsSC.description = `Context Log ${state.turnCount} | ${text}\n` + settingsSC.description;
return text;
}
// Word lists used in output modifier.
function getGender() {
return gender[Math.floor(Math.random() * gender.length)];
}
const gender = [
"male",
"female"
]
function getRace() {
return race[Math.floor(Math.random() * race.length)];
}
const race = [
"human",
"elf",
"dwarf",
"gnome",
"halfling",
"hobbit",
"drow",
"angel",
"demon",
"troll",
"ogre",
"vampire",
"werewolf",
"slimegirl",
"Cthulhu",
"fairy",
"pixie",
"sasquatch",
"centaur",
"merperson",
"zombie",
"harpy",
"dragonborn",
"dragon",
"kobold",
"ent",
"aarakocra",
"kitsune",
"catfolk",
"wood elf",
"high elf",
"golem",
"ghost",
"gargoyle",
"sylph",
"djinn",
"android",
"cyborg",
"hill giant",
"yeti",
"orc",
"half-orc",
"goblin",
"half-elf",
"leprechaun",
"star-child",
"titan",
"mind flayer",
"gorgon",
"alien",
"nymph",
"dryad",
"naiad",
"cyclops",
"minotaur",
"banshee",
"sidhe",
"naga",
"satyr",
"faun"
];
function getClass() {
return classes[Math.floor(Math.random() * classes.length)];
}
const classes = [
"warrior",
"mage",
"thief",
"bard",
"wizard",
"sorcerer",
"warlock",
"witch",
"barbarian",
"knight",
"gladiator",
"assassin",
"priest",
"cleric",
"jester",
"noble",
"merchant",
"sage",
"acrobat",
"peasant",
"tourist",
"dancer",
"artist",
"gangster",
"paladin",
"soldier",
"poet",
"ranger",
"hunter",
"tailor",
"blacksmith",
"innkeeper",
"healer",
"alchemist",
"pirate",
"smuggler",
"cavalier",
"prisoner",
"maid",
"shaman",
"monk",
"ninja",
"druid",
"tax collector",
"lumberjack",
"fortuneteller",
"spellsword",
"sailor",
"farmer",
"runemaster",
"scholar",
"scribe",
"scout",
"Valkyrie",
"samurai",
"plague doctor",
"necromancer",
"miner",
"monster tamer",
"battlemage",
"artificer",
"aristocrat",
"librarian",
"janitor",
"psion",
"hermit",
"pilgrim",
"butler"
];
function getMonAdj() {
return monadj[Math.floor(Math.random() * monadj.length)];
}
const monadj = [
"sabertooth",
"fire-breathing",
"venomous",
"gelatinous",
"winged",
"flame",
"dancing",
"radioactive",
"mechanical",
"vorpal",
"tentacled",
"eldritch",
"ethereal",
"drooling",
"nightmare",
"magical",
"floating",
"rabid",
"were-",
"screaming",
"demonic",
"angelic",
"rotten",
"twilight",
"festering",
"alien",
"fungal",
"giant",
"writhing",
"pustulent",
"rainbow",
"possessed",
"feral",
"fiendish",
"prismatic",
"furry",
"verbose",
"psionic",
"carnivorous",
"mummified",
"glowing",
"snow",
"drunken",
"primordial",
"majestic",
"storm",
"acid",
"frost",
"shadow",
"doom",
"celestial",
"hollow",
"mountain",
"eight-legged",
"one-eyed",
"shambling",
"weeping",
"demented",
"phantom",
"ancient",
"skeletal",
"lightning",
"thunder",
"ravenous",
"steam",
"river",
"ocean",
"cloud",
"star",
"armored",
"ferocious",
"voodoo",
"sea",
"fluffy",
"sand",
"lava",
"chaos",
"solar",
"lunar",
"void",
"mythical",
"magma",
"crystalline",
"stone",
"golden",
"vampiric",
"undead",
"mutant",
"polar",
"swamp",
"gnarled",
"twisted",
"toxic",
"cave",
"chocolate",
"flatulent",
"bloated",
"insane",
"metal",
"deformed"
];
function getMonType() {
return montype[Math.floor(Math.random() * montype.length)];
}
const montype = [
"lizard",
"spider",
"slime",
"gremlin",
"tree",
"vine",
"dragon",
"grandma",
"vortex",
"ghost",
"moth",
"monkey",
"bear",
"beholder",
"puppet",
"wolf",
"lemming",
"clock",
"baby",
"gorgon",
"wyvern",
"wraith",
"manticore",
"sphinx",
"centipede",
"behemoth",
"velociraptor",
"cockroach",
"lion",
"wombat",
"sheep",
"banana",
"pudding",
"leech",
"tarantula",
"golem",
"mimic",
"orc",
"goblin",
"guardian",
"goose",
"eyeball",
"hawk",
"raven",
"mole",
"squid",
"hand",
"ghoul",
"unicorn",
"balrog",
"pumpkin",
"warthog",
"shrimp",
"mushroom",
"jackal",
"carrot",
"tongue",
"minotaur",
"doll",
"crustacean",
"genie",
"horror",
"slug",
"snail",
"worm",
"wyrm",
"cactus",
"kudzu",
"flower",
"fog",
"wisp",
"wasp",
"bee",
"assassin",
"viper",
"cobra",
"rat",
"beetle",
"tiger",
"armadillo",
"statue",
"blob",
"turtle",
"fishman",
"abomination",
"suit of armor",
"skull",
"titan",
"cadaver",
"frog",
"phoenix",
"turd",
"brain",
"heart",
"platypus",
"panda",
"mirror",
"octopus",
"banshee",
"shark"
];
function getTrap() {
return trap[Math.floor(Math.random() * trap.length)];
}
const trap = [
"poison dart",
"swinging axe blade",
"falling boulder",
"poison gas",
"rolling log",
"moving wall",
"spiked wall",
"spiked pit",
"electric shock",
"incineration",
"fireball",
"ice spikes",
"earthquake",
"paralysis",
"hallucination",
"summon monster",
"life drain",
"sleeping gas",
"mental daze",
"spirit lock",
"screaming baby",
"blindness",
"searing flame",
"crumbling floor",
"alarm"
];
function getMetal() {
return metal[Math.floor(Math.random() * metal.length)];
}
const metal = [
"copper",
"bronze",
"lead",
"iron",
"steel",
"black steel",
"silvered steel",
"silver",
"gold",
"platinum",
"mythril",
"adamantine",
"orichalchum",
"dragon metal",
"demonite",
"angelite",
"jewel-encrusted",
"bone",
"ivory",
"ceramic",
"crystal",
"glass",
"coral"
];
function getItemAdj() {
return itemadj[Math.floor(Math.random() * itemadj.length)];
}
const itemadj = [
"glowing",
"magical",
"finely crafted",
"cheap",
"deadly",
"rusty",
"broken",
"superior",
"legendary",
"epic",
"cursed",
"blessed",
"reinforced",
"royal",
"ominous",
"holy",
"evil",
"mighty",
"glimmering",
"radiant",
"glorious",
"slimy",
"impervious",
"corrosive",
"cruel",
"menacing",
"valiant",
"vicious",
"sturdy",
"shining",
"talking",
"vorpal",
"dainty",
"razor-sharp",
"heavy",
"light",
"engraved",
"stunning",
"ancient",
"mythical",
"mystical",
"enchanted",
"runic",
"jeweled",
"prismatic"
];
function getMetalItem() {
return metalitem[Math.floor(Math.random() * metalitem.length)];
}
const metalitem = [
"dagger",
"knife",
"throwing knife",
"shuriken",
"chakhram",
"short sword",
"longsword",
"broadsword",
"claymore",
"zweihander",
"bastard sword",
"falchion",
"rapier",
"epee",
"kukri",
"trident",
"katana",
"wakizashi",
"cutlass",
"scimitar",
"main gauche",
"nodachi",
"tanto",
"naginata",
"spear",
"pike",
"javelin",
"hand axe",
"battle axe",
"halberd",
"mace",
"flail",
"morningstar",
"hammer",
"pickaxe",
"stiletto",
"hatchet",
"breastplate",
"helmet",
"horned helmet",
"helm",
"hauberk",
"cuirass",
"pair of gauntlets",
"pair of bracers",
"pair of greaves",
"pair of vambraces",
"pair of boots",
"chainmail",
"platemail",
"scale mail",
"shield",
"spiked shield",
"kite shield",
"tower shield",
"chalice",
"goblet",
"amulet",
"necklace",
"ring",
"bracelet",
"circlet",
"crown",
"sword-breaker"
];
function getLeather() {
return leather[Math.floor(Math.random() * leather.length)];
}
const leather = [
"leather",
"studded leather",
"brigandine",
"snakeskin",
"sharkskin"
];
function getLeatherItem() {
return leatheritem[Math.floor(Math.random() * leatheritem.length)];
}
const leatheritem = [
"breastplate",
"cap",
"hat",
"jerkin",
"jacket",
"hauberk",
"cuirass",
"pair of gauntlets",
"pair of gloves",
"pair of bracers",
"pair of greaves",
"pair of vambraces",
"pair of boots",
"pair of shoes",
"quiver",
"whip",
"belt",
"buckler",
"satchel",
"backpack",
"bag",
"purse"
];
function getItemMod() {
return itemmod[Math.floor(Math.random() * itemmod.length)];
}
const itemmod = [
"doom",
"flame",
"ice",
"frost",
"lightning",
"thunder",
"storms",
"poison",
"venom",
"acid",
"the waves",
"the wind",
"earthquakes",
"the sun",
"the moon",
"the stars",
"the night",
"the raven",
"the wolf",
"the falcon",
"the bear",
"the fox",
"the llama",
"the tiger",
"the lion",
"the eagle",
"the owl",
"the spider",
"the platypus",
"the penguin",
"the dragon",
"Heaven",
"Hell",
"the apocalypse",
"darkness",
"shadows",
"light",
"mystery",
"Zeus",
"Ares",
"Hades",
"Artemis",
"Apollo",
"Poseidon",
"Hermes",
"Hephaestus",
"Aphrodite",
"Kali",
"Vishnu",
"Shiva",
"Ra",
"Osiris",
"Isis",
"Bastet",
"Horus",
"Thoth",
"Anubis",
"Hathor",
"Thor",
"Odin",
"Freyr",
"Freya",
"Loki",
"Valhalla",
"Nyx",
"Cthulhu",
"invisibility",
"healing",
"the deep",
"slime",
"flatulence",
"thorns",
"life stealing",
"silence",
"disruption",
"seeking",
"wailing",
"karma",
"fate",
"time",
"chaos",
"magic draining",
"vomiting",
"dreams",
"dragon slaying",
"banish undead",
"troll slaying",
"goblin slaying",
"orc slaying",
"ogre slaying",
"demon slaying",
"the void",
"immortality",
"invincibility",
"anger",
"love",
"war",
"grief",
"fear",
"mercy",
"nightmares",
"gravity",
"twilight",
"plague",
"hallucination"
];
function getWood() {
return wood[Math.floor(Math.random() * wood.length)];
}
const wood = [
"pine",
"birch",
"cherrywood",
"chestnut",
"oak",
"spruce",
"poplar",
"beech",
"applewood",
"elm",
"hickory",
"larch",
"mulberry",
"cedar",
"yew",
"redwood",
"ivory",
"ebony",
"bone",
"teak",
"mahogany"
];
function getWoodItem() {
return wooditem[Math.floor(Math.random() * wooditem.length)];
}
const wooditem = [
"shortbow",
"longbow",
"crossbow",
"staff",
"quarterstaff",
"bo-staff",
"round shield",
"wand",
"rod",
"cudgel",
"club",
"spear",
"flute",
"box"
];
function getMagicItem() {
return magicitem[Math.floor(Math.random() * magicitem.length)];
}
const magicitem = [
"scroll",
"spellbook",
"staff",
"wand",
"potion",
"ring"
];
function getSpell() {
return spell[Math.floor(Math.random() * spell.length)];
}
const spell = [
"fireball",
"magic missile",
"ice spikes",
"lightning",
"earthquake",
"rockslide",
"wind gust",
"tornado",
"lava",
"glacier",
"acid rain",
"holy flame",
"sunburst",
"plasma flare",
"featherfall",
"light",
"reveal traps",
"purify",
"healing",
"polymorph self",
"polymorph other",
"cure poison",
"cure disease",
"resurrect",
"teleport",
"locate stairs",
"locate monsters",
"invisibility",
"summon monster",
"water walking",
"water breathing",
"charm monster",
"stone skin",
"petrify",
"open door",
"astral projection",
"protection from fire",
"protection from ice",
"protection from lightning",
"protection from earth",
"curse",
"bless",
"banish undead",
"growth",
"shrink",
"animal friendship",
"giant strength",
"celerity",
"transmutation",
"language translation",
"stun",
"animate object",
"plant control",
"animal control"
];
function getElement() {
return element[Math.floor(Math.random() * element.length)];
}
const element = [
"fire",
"ice",
"frost",
"water",
"lightning",
"wind",
"earth",
"acid",
"poison",
"holy",
"evil",
"chaos",
"void",
"steam",
"fate",
"time",
"shadow",
"doom",
"light",
"nature"
];
function getGem() {
return gem[Math.floor(Math.random() * gem.length)];
}
const gem = [
"diamond",
"ruby",
"emerald",
"sapphire",
"amethyst",
"topaz",
"opal",
"onyx",
"garnet",
"peridot",
"alexandrite",
"amber",
"jade",
"aquamarine",
"turquoise",
"pearl",
"beryl",
"black pearl",
"tourmaline",
"bloodstone",
"moonstone",
"spinel",
"zircon",
"carnelian",
"quartz",
"chrysoberyl",
"citrine",
"agate",
"kyanite",
"jasper",
"lapis lazuli",
"malachite",
"obsidian",
"sunstone",
"tanzanite"
];
function getJewelry() {
return jewelry[Math.floor(Math.random() * jewelry.length)];
}
const jewelry = [
"necklace",
"bracelet",
"ring",
"pendant",
"pair of earrings",
"anklet",
"circlet",
"crown"
];
function getClothingColor() {
return clcolor[Math.floor(Math.random() * clcolor.length)];
}
const clcolor = [
"red",
"orange",
"yellow",
"green",
"blue",
"purple",
"white",
"black",
"brown",
"pink",
"magenta",
"maroon",
"violet",
"tangerine",
"turquoise",
"teal",
"cyan",
"cerulean",
"chartreuse",
"mauve",
"fuchsia",
"beige",
"tan",
"gray",
"olive green",
"goldenrod",
"silver",
"indigo",
"forest green",
"rainbow colored",
"polka-dotted",
"plaid",
"paisley",
"checkered",
"striped",
"khaki",
"camouflage"
];
function getFabric() {
return fabric[Math.floor(Math.random() * fabric.length)];
}
const fabric = [
"cotton",
"wool",
"linen",
"silk",
"satin",
"lace",
"burlap",
"cashmere",
"spidersilk",
"muslin",
"taffeta",
"brocade",
"terrycloth",
"felt",
"velvet",
"fur-lined",
"hemp"
];
function getGarment() {
return garment[Math.floor(Math.random() * garment.length)];
}
const garment = [
"tunic",
"pair of breeches",
"loincloth",
"doublet",
"cloak",
"wizard's robe",
"bathrobe",
"surcoat",
"tabard",
"pair of trousers",
"skirt",
"dress",
"gown",
"pair of socks",
"pair of gloves",
"top hat",
"waistcoat",
"kilt",
"cummerbund",
"bowtie",
"necktie",
"tuxedo",
"kimono",
"karate gi",
"pair of toe socks",
"sarong",
"scarf",
"pair of legwarmers",
"trenchcoat",
"pair of shorts",
"pair of leggings",
"blouse",
"sweater",
"cardigan",
"wizard's hat",
"feathered hat",
"tutu",
"cape"
];
function getMiscItem() {
return misc[Math.floor(Math.random() * misc.length)];
}
const misc = [
"a quiver of arrows",
"some gold coins",
"a pile of rations",
"a cheese wheel",
"a few apples",
"a loaf of stale bread",
"a magic lamp",
"some torches",
"a set of lockpicks",
"a magical harp",
"a half-eaten ham sandwich",
"a roast turkey",
"several carrots",
"a fruitcake",
"a crystal skull",
"a stone tablet",
"some runestones",
"an empty flask",
"a broom",
"a pair of scissors",
"a mop",
"a clown nose",
"a raven totem",
"a pig totem",
"a wolf totem",
"a fox totem",
"an eagle totem",
"a roc feather",
"a bat wing",
"an old painting",
"a tinderbox",
"a lantern",
"a treasure map",
"a rope",
"a violin",
"a lute",
"a shrunken head",
"an accordion",
"some magic beans"
];
function getLootPlace() {
return lootplace[Math.floor(Math.random() * lootplace.length)];
}
const lootplace = [
"You open an old wooden treasure chest and find",
"In an old wooden chest, you find",
"Peering into a dark corner, you see a dusty shelf containing",
"You see something glinting in a dark alcove. Looking closer, you find",
"You trip over",
"In a pile of junk, you find",
"You find a barrel containing",
"You notice a large canvas bag lying around. Inside it, you find",
"A stone seems to be missing from the wall. Inside the hole, you find",
"You notice an ancient altar containing",
"Lying atop an intricately carved pedestal, you see",
"Finding an old clay pot lying around, you reach inside and pull out"
];
function getRandomItem() {
return item[Math.floor(Math.random() * item.length)];
}
const item = [
`${getItemAdj()} ${getMetal()} ${getMetalItem()}.`,
`${getMetal()} ${getMetalItem()} of ${getItemMod()}.`,
`${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getItemMod()}.`,
`${getMetal()} ${getMetalItem()} of ${getElement()}.`,
`${getMetal()} ${getMetalItem()} of protection from ${getElement()}.`,
`${getMetal()} ${getMetalItem()} of ${getSpell()}`,
`${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getElement()}.`,
`${getItemAdj()} ${getMetal()} ${getMetalItem()} of protection from ${getElement()}.`,
`${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getSpell()}`,
`${getLeather()} ${getLeatherItem()}.`,
`${getItemAdj()} ${getLeather()} ${getLeatherItem()}.`,
`${getLeather()} ${getLeatherItem()} of ${getItemMod()}.`,
`${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getItemMod()}.`,
`${getLeather()} ${getLeatherItem()} of ${getElement()}.`,
`${getLeather()} ${getLeatherItem()} of protection from ${getElement()}.`,
`${getLeather()} ${getLeatherItem()} of ${getSpell()}.`,
`${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getElement()}.`,
`${getItemAdj()} ${getLeather()} ${getLeatherItem()} of protection from ${getElement()}.`,
`${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getSpell()}.`,
`${getWood()} ${getWoodItem()}.`,
`${getItemAdj()} ${getWood()} ${getWoodItem()}.`,
`${getWood()} ${getWoodItem()} of ${getItemMod()}.`,
`${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getItemMod()}.`,
`${getWood()} ${getWoodItem()} of ${getElement()}.`,
`${getWood()} ${getWoodItem()} of protection from ${getElement()}.`,
`${getWood()} ${getWoodItem()} of ${getSpell()}.`,
`${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getElement()}.`,
`${getItemAdj()} ${getWood()} ${getWoodItem()} of protection from ${getElement()}.`,
`${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getSpell()}.`,
`${getMagicItem()} of ${getSpell()}.`,
`${getClothingColor()} orb of ${getElement()}.`,
`${getClothingColor()} orb of ${getSpell()}.`,
`${getMetal()} and ${getGem()} ${getJewelry()}.`,
`${getLeather()} quiver full of ${getWood()} arrows.`,
`${getClothingColor()} ${getFabric()} ${getGarment()}.`]
// Checkout the repo examples to get an idea of other ways you can use scripting
// https://github.com/AIDungeon/Scripting/blob/master/examples
//Allows user to enter [RACE] and/or [CLASS] to have script choose for them.
const modifier = (text) => {
var Prace = getRace();
const regex1 = /\[RACE\]/gi;
var modifiedText = text.replace(regex1, Prace);
var Pclass = getClass();
const regex2 = /\[CLASS\]/gi;
var modifiedText = modifiedText.replace(regex2, Pclass);
// You must return an object with the text property defined.
return { text: modifiedText }
}
// Don't modify this part
modifier(text)
//Credits:
//Onyx
//dragranis
//Devon (the duck)
//Draco
//Also thanks to: Aether
//The percentage of turns an event will fire.
let percentChance = 10
//List of possible events
state.options = {
npc:[
`Suddenly you see the corpse of a ${getGender()} ${getRace()} ${getClass()} lying on the ground. Should you loot it for equipment?`,
`You see a ${getGender()} ${getRace()} ${getClass()} walking down the hall towards you.`,
`You stumble across a ${getGender()} ${getRace()} ${getClass()} who seems very lost.`,
`Suddenly a secret door opens and a ${getGender()} ${getRace()} ${getClass()} steps out of it.`,
`You spot a strange altar at the end of the hall. As you approach, you notice a ${getGender()} ${getRace()} ${getClass()} standing there holding ${getMiscItem()}.`,
`"Greetings, adventurer," says a ${getGender()} ${getRace()} ${getClass()}, stepping out from the shadows. "I seek a ${getRandomItem()}. There is one nearby, but it is hidden in a dangerous ${getMonAdj()} ${getMonType()} lair. If you retrieve it for me, you will be richly rewarded."`,
`Entering the next room, you startle a ${getGender()} ${getRace()} ${getClass()}, who drops ${getMiscItem()} and runs away.`,
`Entering the next room, you find a ${getGender()} ${getRace()} ${getClass()} chained to the wall.`,
`Suddenly a ${getGender()} ${getRace()} ${getClass()} comes running down the hallway towards you, chased by a ${getMonAdj()} ${getMonType()}.`,
`The sounds of battle reach your ears, and you find a ${getGender()} ${getRace()} ${getClass()} fighting a ${getMonAdj()} ${getMonType()} up ahead.`,
`A noise catches your attention, and you turn to see a ${getGender()} ${getRace()} ${getClass()} opening a nearby treasure chest.`,
`You spot a ${getGender()} ${getRace()} ${getClass()} resting in a secluded alcove. "Oh good, a hero," the person says with a sigh of relief. "A ${getMonAdj()} ${getMonType()} just beat me up and stole my ${getRandomItem()}. Can you help me get it back?"`,
`You jump as the ghost of a ${getGender()} ${getRace()} ${getClass()} floats through a nearby wall. This must be one of the unlucky adventurers whose life was claimed by the dungeon.`,
`You notice something moving off in the distance and decide to investigate. As you get closer, you realize it's a ${getGender()} ${getRace()} ${getClass()} dressed in a ${getClothingColor()} ${getFabric()} ${getGarment()}.`,
`Off in the distance, you see a ${getGender()} ${getRace()} ${getClass()} struggling to open a locked door.`,
`You spot a male ${getRace()} ${getClass()} across the room. He appears to be wearing a ${getMetal()} and ${getGem()} ${getJewelry()}.`,
`You spot a female ${getRace()} ${getClass()} across the room. She appears to be wearing a ${getMetal()} and ${getGem()} ${getJewelry()}.`,
`Hearing footsteps nearby, you ready your weapon, but relax a moment later when the source of the noise walks into view. Whew, it's only a ${getGender()} ${getRace()} ${getClass()}!`
],
monster:[
`You see a ${getMonAdj()} ${getMonType()} step out from around a corner in front of you.`,
`You hear a noise behind you and turn around. Oh no, it's a ${getMonAdj()} ${getMonType()}!`,
`You see a ${getMonAdj()} ${getMonType()} lying on the floor. It appears to be dead.`,
`You hear the cries of a ${getMonAdj()} ${getMonType()} in the distance.`,
`You find a ${getMonAdj()} ${getMonType()} dressed in an ill-fitting ${getClothingColor()} ${getFabric()} ${getGarment()} rooting through some trash.`,
`You see a treasure chest in the next room. Unfortunately, it seems to be guarded by a ${getMonAdj()} ${getMonType()}.`,
`Hearing a forlorn cry, you decide to investigate. Entering the next room, you find a ${getMonAdj()} ${getMonType()} locked in a cage.`,
`As you're wandering through the dungeon, you stumble upon a ${getMonAdj()} ${getMonType()}. It immediately shrieks and runs away.`,
`You spot a sign hanging on the wall. It says "WARNING: Beware of the ${getMonAdj()} ${getMonType()}!"`,
`Peering through the darkness, you spot a ${getMonAdj()} ${getMonType()} skulking around.`,
`You spot a ${getMonAdj()} ${getMonType()} off in the distance and ready your weapon. However, as soon as it sees you, it drops a ${getMetal()} and ${getGem()} ${getJewelry()} on the floor and runs away.`,
`You spot a ${getMonAdj()} ${getMonType()} off in the distance and ready your weapon. However, as soon as it sees you, it drops ${getMiscItem()} on the floor and runs away.`,
`Suddenly, you stop short and stare ahead of you in dismay. You seem to have wandered into a ${getMonAdj()} ${getMonType()} den.`,
`You wrinkle your nose as you get a whiff of a strange smell. You'd bet anything there's a ${getMonAdj()} ${getMonType()} around here somewhere.`,
`Suddenly you see a ${getMonAdj()} ${getMonType()} up ahead. It doesn't seem to have noticed you yet.`,
`You hear a shriek behind you and whirl around to discover a ${getMonAdj()} ${getMonType()} has triggered a ${getTrap()} trap!`,
`You see a treasure chest in front of you. You're just bending down to open it when it opens its lid and tries to bite you!`
],
loot:[
`${getLootPlace()} a small ${getWood()} figurine of a ${getGender()} ${getRace()}`,
`${getLootPlace()} ${getMiscItem()}.`,
`${getLootPlace()} a ${getMetal()} ${getMetalItem()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getMetal()} ${getMetalItem()}.`,
`${getLootPlace()} a ${getMetal()} ${getMetalItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getMetal()} ${getMetalItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getMetal()} ${getMetalItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getMetal()} ${getMetalItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getMetal()} ${getMetalItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getMetal()} ${getMetalItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getLeather()} ${getLeatherItem()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getLeather()} ${getLeatherItem()}.`,
`${getLootPlace()} a ${getLeather()} ${getLeatherItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getLeather()} ${getLeatherItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getLeather()} ${getLeatherItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getLeather()} ${getLeatherItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getLeather()} ${getLeatherItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getLeather()} ${getLeatherItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getWood()} ${getWoodItem()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getWood()} ${getWoodItem()}.`,
`${getLootPlace()} a ${getWood()} ${getWoodItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getItemMod()}.`,
`${getLootPlace()} a ${getWood()} ${getWoodItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getWood()} ${getWoodItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getWood()} ${getWoodItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getWood()} ${getWoodItem()} of protection from ${getElement()}.`,
`${getLootPlace()} a ${getItemAdj()} ${getWood()} ${getWoodItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getMagicItem()} of ${getSpell()}.`,
`${getLootPlace()} a ${getClothingColor()} orb. It seems to have power over the element of ${getElement()}.`,
`${getLootPlace()} a ${getClothingColor()} orb of ${getSpell()}.`,
`${getLootPlace()} a ${getMetal()} and ${getGem()} ${getJewelry()}.`,
`${getLootPlace()} a ${getLeather()} quiver full of ${getWood()} arrows.`,
`${getLootPlace()} a ${getClothingColor()} ${getFabric()} ${getGarment()}.`,
`${getLootPlace()} a ${getMetal()} key. You wonder what it opens.`
],
trap:[
`You feel something shift beneath your feet and your stomach clenches with dread. It's a ${getTrap()} trap!`,
`Oops! You trip and fall, triggering a ${getTrap()} trap.`,
`You stop abruptly as you realize there's a tripwire in front of you. It seems to be attached to a ${getTrap()} trap.`,
`You pause to lean against the wall, only to regret it a moment later when you hear a ${getTrap()} trap go off.`,
`You hear an ominous click. Oh no, you've triggered a ${getTrap()} trap!`,
`As you brush your hand along the wall, you hear an ominous click and realize with horror that you've triggered a ${getTrap()} trap.`
],
desc:[
`A strange painting depicting ${getMiscItem()} hangs on the wall before you.`,
`You see a treasure chest in the next room.`,
`You notice a piece of paper on the floor. It seems to be a note.`,
`You hear an eerie noise in the distance.`,
`You hear a scream echoing eerily through the shadowy corridors.`,
`The torchlight flickers eerily on the stone walls.`,
`You hear footsteps echoing eerily along the ancient corridors. Someone -- or something -- must be nearby.`,
`Your movements kick up a cloud of ancient dust, and you sneeze.`,
`The stones of the floor are cracked and worn, as if many feet have passed here.`,
`You hear water dripping somewhere in the distance.`,
`Something golden gleams at the edge of the torchlight.`,
`You notice a strange sigil on the wall in front of you. Could there be a secret door here?`,
`There's a lever on the wall in front of you. You wonder if it opens a secret door...or triggers a trap.`,
`You hear a rumbling noise and the wall in front of you slides open. It's a secret door!`,
`Suddenly the floor ahead of you opens up and you nearly plummet into the shadows below.`,
`You see a mysterious ${getClothingColor()} button on the wall in front of you.`,
`A stone in the wall seems to be loose.`,
`You spot a hole in the wall just big enough to put your hand in.`,
`You see an alcove with a ladder going up.`,
`The torchlight flickers, revealing a ladder going down.`,
`You notice a trapdoor set into the floor.`,
`You notice a trapdoor set into the ceiling.`,
`In the corner of the room, you spot some stairs going up.`,
`Behind a glowing ${getClothingColor()} magical barrier, you catch a glimpse of some stairs going down.`,
`Behind a glowing ${getClothingColor()} magical barrier, you catch a glimpse of some stairs going up.`,
`In the distance, you spot some stairs going down. Unfortunately, they seem to be guarded by a ${getMonAdj()} ${getMonType()}.`,
`In the distance, you spot some stairs going up. Unfortunately, they seem to be guarded by a ${getMonAdj()} ${getMonType()}.`,
`You spot a wooden door in the wall. It seems to be locked.`,
`You spot an iron door in the wall. It seems to be locked.`,
`In an out-of-the-way corner of the room, you spot a gem-encrusted golden door. It must have something really good behind it!`,
`You notice an old wooden door with rusted chains across it.`,
`Off in the distance, you notice a wooden door reinforced by metal bands. You wonder what could be in there.`,
`A tapestry depicting a ${getGender()} ${getRace()} ${getClass()} fighting a ${getMonAdj()} ${getMonType()} hangs on the wall before you.`,
`Farther ahead, you spot a statue of a ${getGender()} ${getRace()} ${getClass()}.`,
`In the center of the room, you spot a gigantic glowing ${getGem()}.`,
`A ${getGem()}-encrusted ${getWood()} treasure chest sits in an out-of-the-way corner of the room.`
]
}
//Chooses a random event from state.option
function pickEventText() {
let min = 1;
let max = 100;
let random = Math.floor(Math.random()*(max-min+1))+min;
//Weights for different event types.
if(random<=20){
return state.options.npc[Math.floor(Math.random()*state.options.npc.length)]
}
else if(random>=21 && random<=45){
return state.options.monster[Math.floor(Math.random()*state.options.monster.length)]
}
else if(random>=46 && random<=65){
return state.options.loot[Math.floor(Math.random()*state.options.loot.length)]
}
else if(random>=66 && random<=80){
return state.options.trap[Math.floor(Math.random()*state.options.trap.length)]
}
else{
return state.options.desc[Math.floor(Math.random()*state.options.desc.length)]
}
}
const modifier = (text) => {
let modifiedText = text;
//Pick a number between 0 - 99
let num = Math.floor(Math.random() * 100);
//DEBUG. Uncomment to display the number that was chosen each turn.
//state.message = num
//If the number is below the percentage (but not equal to, to account for 0), fire the event.
if(num < percentChance) {
let tText = pickEventText();
modifiedText = text + '\n\n' + tText + '\n\n';
}
return {text: modifiedText}
}
modifier(text)
/* === STORY ARC CONFIGURATION === */
state.originalAuthorsNote = "Put your original authors note here!" //Add your story's authors note here. This will allow the system to modify the authors note whiile preserving your current one. If you have a dynamically changing authors note, you can instead put a "state.variableName" here.
state.initialHeatValue = 0 //Increasing this will increase the chance of the temperature increasing in the first few moments of the story.
state.initialTemperatureValue = 1 //Increasing this will increase the amount of conflict and tension in the initial sections of the story.
state.temperatureIncreaseChance = 15 //Increasing this value makes the conflict and tension in the story advance quicker.
state.heatIncreaseValue = 1 //Increasing this value makes the temperature increase more often, creating a faster paced story.
state.temperatureIncreaseValue = 1 //Increasing this value makes the conflit and tension in the story advance by larger segments, so the story will feel less like a gradual slope of tension and more like big steps.
state.playerIncreaseHeatImpact = 2 //The impact that the player has on increasing the conflict, so if the player attacks an NPC or does something drama inducing, the conflict and tension will increase by this amount.
state.playerDecreaseHeatImpact = 2 //The impact that the player has on decreasing the conflict, so if the player helps others or is doing something relaxing, the conflict and tension will decrease by this amount.
state.playerIncreaseTemperatureImpact = 1 //The impact that the player has on increasing the conflict, so if the player attacks an NPC or does something drama inducing, the conflict and tension will increase by this amount.
state.playerDecreaseTemperatureImpact = 1 //The impact that the player has on decreasing the conflict, so if the player helps others or is doing something relaxing, the conflict and tension will decrease by this amount.
state.threshholdPlayerIncreaseTemperature = 2 //This is the number of conflict words that have to be said by the player in their input in order to increase the temperature.
state.threshholdPlayerDecreaseTemperature = 2 //This is the number of calming words that have to be said by the player in their input in order to decrease the temperature.
state.modelIncreaseHeatImpact = 1 //The impact that the AI model has on increasing the conflict.
state.modelDecreaseHeatImpact = 2 //The impact that the AI model has on decreasing the conflict.
state.modelIncreaseTemperatureImpact = 1 //The impact that the AI model has on increasing the conflict.
state.modelDecreaseTemperatureImpact = 1 //The impact that the AI model has on decreasing the conflict.
state.threshholdModelIncreaseTemperature = 3 //This is the number of conflict words that have to be said by the AI Model in in order to increase the temperature.
state.threshholdModelDecreaseTemperature = 3 //This is the number of conflict words that have to be said by the AI Model in in order to decrease the temperature.
state.maximumTemperature = 12 //This is the maximum level of conflict the story can get to. Lower values make for a more calm experience, while higher values can make the story go overboard with the AI trying to kill you at every step. Be careful with this value, as it can get out of hand quite quickly.
state.trueMaximumTemperature = 15 //This determines the actual maximum temperature, as random explosions can cause the normal maxmium temperature to increase beyond its normal state. Players cannot cause the temperature to increase beyond the normal maximum. !WARNING! TRUE MAXIMUM TEMPERATURE VALUES ABOVE 15 CAN CAUSE CHAOTIC AND HIGHLY DESTRUCTIVE EVENTS TO RUIN YOUR STORY. ONLY ENABLE VALUES ABOVE 15 IF YOU WANT A REALLY PUNISHING EXPERIENCE.
state.minimumTemperature = 1 //This determines the lowest value that the player can get the temperature to. Systems like AI influence can reduce it to whatever the true minimum temperature value is.
state.trueMinimumTemperature = 1 //This determines the true lowest value that the temperature can get to. No system can set the value of the temperature to anything lower.
state.smartOverheatTimer = "This feature is currently being worked on, do not set it to true." //If you set this setting to true, it will make the overheat timer entirely dependent on what is being said and done in the story. For instance if people are in combat and fighting, or a heavily dramatic climax is going on, the smart timer will detect this and keep the action going for until the action is resolved. !WARNING! THIS FEATURE CAN BE INCREADIBLY BUGGY AND CAUSE CONFLICTS TO GO ON FOREVER DEPENDING ON YOUR MODEL AND SETTINGS. BE CAREFUL WHEN USING THIS.
state.overheatTimer = 4 //After the maximum temperature is reached, the script will go into overheat mode, meaning that after this many actions, the temperature will start to decrease. This is good if you want your maximum tension point to last multiple actions before calming down.
state.overheatReductionForHeat = 5 //After the overheat timer is over, the temperature will decrease by this amount. A higher number will make the story much calmer after the maxmium temperature point, a lower number will make the action decrease more gradually.
state.overheatReductionForTemperature = 1 //After the overheat timer is over, the temperature will decrease by this amount. A higher number will make the story much calmer after the maxmium temperature point, a lower number will make the action decrease more gradually.
state.cooldownTimer = 5 //After the overheat timer is over, this cooldown timer determines the number of actions the story will take before being able to increase the temperature and conflict again. A higher value will allow the player to have more downtime, a lower value will push the player to jump into the next conflict faster.
state.cooldownRate = 2 //For each action that the cooldown phase goes for, the temperature will reduce by this amount. A higher value will make the temperature decrease more rapidly, a lower value will make the cooldown more of a gradual slope.
state.randomExplosionChance = 3 //This determines the percent chance that the story will suddenly have the temperature increased by a large value.
state.randomExplosionHeatIncreaseValue = 5 //This determines the impact of the random temperature increase. A higher value will make the story suddenly have something crazy happen, a lower value will make more of a mild surprise.
state.randomExplosionTemperatureIncreaseValue = 2 //This determines the impact of the random temperature increase. A higher value will make the story suddenly have something crazy happen, a lower value will make more of a mild surprise.
/* DONT MODIFY ANYTHING BEYOND THIS POINT */
function randomint(min, max) {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min + 1)) + min
}
const conflictWords = ["attack", "stab", "destroy", "break", "steal", "ruin", "burn", "smash", "sabotage", "disrupt", "vandalize", "overthrow", "assassinate", "plunder", "rob", "ransack", "raid", "hijack", "detonate", "explode", "ignite", "collapse", "demolish", "shatter", "strike", "slap", "obliterate", "annihilate", "corrupt", "infect", "poison", "curse", "hex", "summon", "conjure", "mutate", "provoke", "riot", "revolt", "mutiny", "rebel", "resist", "intimidate", "blackmail", "manipulate", "brainwash", "lie", "cheat", "swindle", "disarm", "fire", "hack", "overload", "flood", "drown", "rot", "dissolve", "slaughter", "terminate", "execute", "drama", "conflict", "evil", "kill", "slay", "defeat", "fight", "doom", "slice", "pain", "dying", "die", "perish", "blood"]
const calmingWords = ["calm", "rest", "relax", "meditate", "sleep", "comfort", "hug", "smile", "forgive", "mend", "repair", "plant", "sing", "dance", "celebrate", "collaborate", "share", "give", "donate", "protect", "shelter", "trust", "hope", "dream", "revive", "eat", "drink", "balance", "cheer", "laugh", "play", "build", "bake", "craft", "cook", "empathize", "apologize", "befriend", "admire", "sympathize", "thank", "appreciate", "cherish", "love", "pet", "respect", "restore", "guide", "teach", "learn", "daydream", "wander", "explore", "discover", "reflect", "happy", "joy", "kind", "heal", "help", "assist"]
const modifier = (text) => {
if (state.heat == undefined){
state.heat = state.initialHeatValue
state.cooldownMode = false
state.overheatMode = false
}
if (state.storyTemperature == undefined){
state.storyTemperature = state.initialTemperatureValue
}
const lowerText = text.toLowerCase()
const words = lowerText.split(/\s+/)
let conflictCount = 0
let calmingCount = 0
words.forEach(word => {
const fixedWord = word.replace(/^[^\w]+|[^\w]+$/g, '')
if (conflictWords.includes(fixedWord)) {
conflictCount++
}
if (calmingWords.includes(fixedWord)) {
calmingCount++
}
})
if (state.cooldownMode == false){
if (conflictCount > 0) {
state.heat += conflictCount * state.playerIncreaseHeatImpact
if (conflictCount >= state.threshholdPlayerIncreaseTemperature){
state.storyTemperature += conflictCount * state.playerIncreaseTemperatureImpact
log(`Detected ${conflictCount} conflict words (Player). Increasing heat & temperature.`)
}
else{
log(`Detected ${conflictCount} conflict words (Player). Increasing heat.`)
}
}
if (calmingCount > 0) {
state.heat -= conflictCount * state.playerDecreaseHeatImpact
if (calmingCount >= state.threshholdPlayerDecreaseTemperature){
state.storyTemperature -= calmingCount * state.playerDecreaseTemperatureImpact
log(`Detected ${calmingCount} calming words (Player). Decreasing heat & temperature.`)
}
else{
log(`Detected ${calmingCount} calming words (Player). Decreasing heat.`)
}
}
}
state.chance = randomint(1, 100)
if (state.chance <= state.randomExplosionChance){
state.heat = state.heat + state.randomExplosionHeatIncreaseValue
state.storyTemperature = state.storyTemperature + state.randomExplosionTemperatureIncreaseValue
log("!WARNING! Explosion Occured! (+" + state.randomExplosionHeatIncreaseValue + " heat) (+" + state.randomExplosionTemperatureIncreaseValue + " temperature)")
}
if(state.cooldownMode == false && state.overheatMode == false){
state.heat = state.heat + state.heatIncreaseValue
log("Heat: " + state.heat)
}
state.chance = randomint(1, state.temperatureIncreaseChance)
if (state.chance <= state.heat){
state.heat = 0
state.storyTemperature = state.storyTemperature + state.temperatureIncreaseValue
log("Temperature Increased. Temperature is now " + state.storyTemperature)
}
if (state.storyTemperature >= state.maximumTemperature){
if (state.cooldownMode == false && state.overheatMode == false){
state.overheatMode = true
state.overheatTurnsLeft = state.overheatTimer
log("Overheat Mode Activated")
}
}
if (state.cooldownMode == true){
state.cooldownTurnsLeft --
log("Cooldown Timer: " + state.cooldownTurnsLeft)
state.storyTemperature = state.storyTemperature - state.cooldownRate
if(state.cooldownTurnsLeft <= 0){
state.cooldownMode = false
log("Cooldown Mode Disabled")
}
}
else{
if(state.overheatMode == true){
state.overheatTurnsLeft --
log("Overheat Timer: " + state.overheatTurnsLeft)
if (state.overheatTurnsLeft <= 0){
state.storyTemperature = state.storyTemperature - state.overheatReductionForTemperature
state.heat = state.heat - state.overheatReductionForHeat
state.overheatMode = false
state.cooldownMode = true
state.cooldownTurnsLeft = state.cooldownTimer
log("Cooldown Mode Activated")
}
}
}
if (state.storyTemperature > state.trueMaximumTemperature){
state.storyTemperature = state.trueMaximumTemperature
log("Temperature over maximum, recalibrating...")
}
if (state.storyTemperature <= 0){
state.storyTemperature = 1
log("Temperature under minimum, recalibrating...")
}
if (state.cooldownMode == false){
log("cooldownMode false, deploying prompt")
//Non-Optimized Story Prompts
if (state.storyTemperature == 1) {
state.memory.authorsNote = "Story Phase: Introduction. Introduce characters and locations. There should be no conflict or tension in the story. " + state.originalAuthorsNote
}
if (state.storyTemperature == 2) {
state.memory.authorsNote = "Story Phase: Introduction. Introduce characters, locations, and plot hooks. There should be only a little conflict and tension in the story unless the player is seeking it out. " + state.originalAuthorsNote
}
if (state.storyTemperature == 3) {
state.memory.authorsNote = "Story Phase: Introduction. Introduce characters, locations, and plot hooks. There should be only minor conflicts. Introduce the possibility of a moderate conflict that could appear far in the future. " + state.originalAuthorsNote
}
if (state.storyTemperature == 4) {
state.memory.authorsNote = "Story Phase: Introduction. Introduce characters, locations, and plot hooks. There should be only minor conflicts. Introduce the possibility of a moderate conflict that could appear far in the future. " + state.originalAuthorsNote
}
if (state.storyTemperature == 5) {
state.memory.authorsNote = "Story Phase: Rising Action. Introduce more minor conflicts. Give minor hints as to what a greater conflict in the far future could be. " + state.originalAuthorsNote
}
if (state.storyTemperature == 6) {
state.memory.authorsNote = "Story Phase: Rising Action. Introduce the occasional moderate conflict. Give minor hints as to what a greater conflict in the far future could be. " + state.originalAuthorsNote
}
if (state.storyTemperature == 7) {
state.memory.authorsNote = "Story Phase: Rising Action. Introduce the occasional moderate conflict. Give minor hints as to what a greater conflict in the far future could be. Introduce conntections to discovered plot hooks. " + state.originalAuthorsNote
}
if (state.storyTemperature == 8) {
state.memory.authorsNote = "Story Phase: Rising Action. Introduce the occasional moderate conflict. Give moderate hints as to what a greater conflict in the far future could be. Introduce conntections to discovered plot hooks. " + state.originalAuthorsNote
}
if (state.storyTemperature == 9) {
state.memory.authorsNote = "Story Phase: Rising Action. Introduce the occasional moderate conflict. Give moderate hints as to what a greater conflict in the far future could be. Introduce conntections to discovered plot hooks. Begin moving the story towards the greater conflict ahead. " + state.originalAuthorsNote
}
if (state.storyTemperature == 10) {
state.memory.authorsNote = "Story Phase: Climax. Introduce the climax of the story. All previous hints about this greater conflict should intersect with this climactic moment. Plot hooks should be connected to this climax. Emphisise major conflict. " + state.originalAuthorsNote
}
if (state.storyTemperature == 11) {
state.memory.authorsNote = "Story Phase: Climax. Plot hooks should be connected to this climax. Emphisise major conflict. Push the characters near their limits while staying fair. " + state.originalAuthorsNote
}
if (state.storyTemperature == 12) {
state.memory.authorsNote = "Story Phase: Climax. Advance the climax of the story, introduce a challenge to go with it. Emphisise major conflict. Push the characters near their limits while staying fair. " + state.originalAuthorsNote
}
if (state.storyTemperature == 13) {
state.memory.authorsNote = "Story Phase: Climax. Advance the climax of the story, introduce challenges to go with it. Emphisise major conflict. Push the characters to their limits. Punish terrible decisions with an appropreate story response. " + state.originalAuthorsNote
}
if (state.storyTemperature == 14) {
state.memory.authorsNote = "Story Phase: Climax. Advance the climax of the story. Emphisise major conflict. Push the characters to their limits. Punish bad decisions while not being unfair. " + state.originalAuthorsNote
}
if (state.storyTemperature == 15) {
state.memory.authorsNote = "Story Phase: Climax. Advance the climax of the story. Emphisise major conflict. Push the characters to their limits. Punish bad decisions that the characters make. Be unfair at times, but make unfairness in the story make sense with the current plot. " + state.originalAuthorsNote
}
if (state.storyTemperature == 16) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Ultimate Climax. Emphisise increadibly difficult conflict. Push the characters to their limits. Punish bad decisions that the characters make. Be unfair at times. " + state.originalAuthorsNote
}
if (state.storyTemperature == 17) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Ultimate Climax. Emphisise insanely difficult conflict. Push the characters to their absolute limits. Punish bad decisions that the characters make. Make the challenges unfair for characters. " + state.originalAuthorsNote
}
if (state.storyTemperature == 18) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Ultimate Climax. Emphisise insanely difficult conflict. Push the characters to their absolute limits. Heavily punish bad decisions that the characters make. Make the challenges increadibly unfair. " + state.originalAuthorsNote
}
if (state.storyTemperature == 19) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Ultimate Climax. Emphisise impossibly difficult conflict. Push the characters to their absolute limits. Very heavily punish bad decisions that the characters make. Make the challenges increadibly unfair. " + state.originalAuthorsNote
}
if (state.storyTemperature == 20) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Omega Insane Ultimate Climax of Doom. Emphisise insanely difficult conflict. Push the characters to their absolute limits. Very heavily punish bad decisions that the characters make. Make the challenges increadibly unfair. There is no success. " + state.originalAuthorsNote
}
if (state.storyTemperature > 20) {
//!WARNING! IT IS NOT RECOMMENDED FOR YOUR STORY TO GET TO THIS STATE. ONLY ENABLE YOUR TRUE MAXIMUM TEMPERATURE TO THIS VALUE IF YOU REALLY REALLY WANT IT TO BE PUNISHING.
state.memory.authorsNote = "Story Phase: Apocalypse. Emphisise impossible conflict. There is no success. Make challenges blatently unfair. Punish every decision. Actively attempt to push the characters away from their goal in any way possible. " + state.originalAuthorsNote
}
}
else{
log("cooldownMode true, deploying alternate prompt")
//Cooldown Prompts
if (state.storyTemperature <= 1) {
state.cooldownMode = false
}
if (state.storyTemperature == 2) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only small bits of tension, with most of the current story being filled with peace and quiet. " + state.originalAuthorsNote
}
if (state.storyTemperature == 3) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension, with most of the current story being filled with peace and quiet. " + state.originalAuthorsNote
}
if (state.storyTemperature == 4) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension, with most of the current story being filled with peaceful encounters. " + state.originalAuthorsNote
}
if (state.storyTemperature == 5) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension, with most of the current story being filled with peaceful encounters, unless characters actively try to cause chaos. " + state.originalAuthorsNote
}
if (state.storyTemperature == 6) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension and conflict, with most of the current story being filled with peaceful encounters, unless characters actively try to cause chaos." + state.originalAuthorsNote
}
if (state.storyTemperature == 7) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension and conflict, with most of the current story being filled with neutral encounters, unless characters actively try to cause chaos. " + state.originalAuthorsNote
}
if (state.storyTemperature == 8) {
state.memory.authorsNote = "Story Phase: Downtime. There should be only minor tension and conflict, with most of the current story containing neutral encounters and minor surprises. This section of story should have a satisfying conclusion for its characters. " + state.originalAuthorsNote
}
if (state.storyTemperature == 9) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be quickly ending, and this section of story should have a satisfying conclusion for its characters. There is still some minor tension and conflict. " + state.originalAuthorsNote
}
if (state.storyTemperature == 10) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be slowly ending, and this section of story should have a satisfying conclusion for its characters. There is still some moderate tension and conflict. " + state.originalAuthorsNote
}
if (state.storyTemperature == 11) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be slowly ending, and this section of story should have a satisfying conclusion for its characters. There is still moderate tension and conflict, but not as much as before. " + state.originalAuthorsNote
}
if (state.storyTemperature == 12) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be slowly ending, and this section of story should have a satisfying conclusion for its characters. There is still moderatly high tension and conflict, but not as much as before. " + state.originalAuthorsNote
}
if (state.storyTemperature == 13) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be slowly ending. There is still moderatly high tension and conflict, but not as much as before. " + state.originalAuthorsNote
}
if (state.storyTemperature == 14) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be beginning to come to a close. There is still moderatly high tension and conflict, but not as much as before. " + state.originalAuthorsNote
}
if (state.storyTemperature == 15) {
state.memory.authorsNote = "Story Phase: Falling Action. The conflicts should be beginning to come to a close. Tension and conflict is still high. " + state.originalAuthorsNote
}
if (state.storyTemperature == 16) {
state.memory.authorsNote = "Story Phase: Extreme Falling Action. The conflicts should start to show signs of ending. Tension and conflict is still high. " + state.originalAuthorsNote
}
if (state.storyTemperature == 17) {
state.memory.authorsNote = "Story Phase: Extreme Falling Action. The conflicts should start to show signs of slightly ending. Tension and conflict is still high. " + state.originalAuthorsNote
}
if (state.storyTemperature == 18) {
state.memory.authorsNote = "Story Phase: Extreme Falling Action. The conflicts should start to show signs of slightly ending. Tension and conflict is still very high. " + state.originalAuthorsNote
}
if (state.storyTemperature == 19) {
state.memory.authorsNote = "Story Phase: Extreme Falling Action. Tension and conflict is still very high. " + state.originalAuthorsNote
}
if (state.storyTemperature >= 20) {
state.memory.authorsNote = "Story Phase: Omega Extreme Falling Action. Tension and conflict is still extremely high. " + state.originalAuthorsNote
}
}
state.authorsNoteStorage = state.memory.authorsNote
return { text }
}
// Don't modify this part
modifier(text)
const modifier = (text) => {
let Success = 0.5 // If a roll is higher than Success, it's a success, i.e. 0.6 would leave a 40% success chance
text.match(/disadvantage/i) ? Success += 0.2 : text.match(/advantage/i) ? Success -= 0.2 : ''
const PartialSuccess = Success + 0.2 // If a roll is a Success, but rolls below PartialSuccess, it's a partial success, i.e. 0.2 would turn 20% of successes into partial successes
const CritSuccess = 0.9 // If a roll is a Success, and rolls above CritSuccess, it's a critical success, i.e. 0.9 would turn 10% of succeses into critical successes
const CritFail = 0.1 // If a roll is not a Success, and rolls below CritFail, it's a critical failure, i.e. 0.1 would turn 10% of failures into critical failures
const outcome = (v, w, s) => (w = text.match(/> (.*) (try|trie|attempt)(s?)/i)) && !((w[1].match(/"/g)??[]).length % 2) ? ((s = v > Success) ? 'And ' : 'But ') + w[1].replace(/^You\b/, 'you').replace(/,$/, ' and') + ((s && v < PartialSuccess) ? ' partially' : '') + (s ? ' succeed' : ' fail') + (w[3] ? 's' : '') + ((v > CritSuccess || v < CritFail) ? (s ? ' perfectly' : ' horribly') : '') + (s ? '.' : '!') : ''
function roll(){
state.roll.frontMemory = outcome(Math.random())
state.memory.frontMemory = state.roll.frontMemory
state.message = state.roll.frontMemory
state.roll.action = info.actionCount
}
state.roll ? '' : state.roll = { frontMemory: '', action: 0 }
state.roll.action == info.actionCount ? state.memory.frontMemory = state.roll.frontMemory : roll()
return { text }
}
modifier(text)