///
///
/**
* - Scripting API: https://help.aidungeon.com/scripting
* - Scripting Guidebook: https://github.com/magicoflolis/aidungeon.js/blob/main/Scripting%20Guidebook.md
*/
// ==Scenario==
// @name š“ Magic Cards
// @description Automatically generate setting-appropriate Story Cards, similar to AutoCards
// @version v1.1.0
// @author Magic
// @homepageURL https://github.com/magicoflolis/MagicCards
// @changelog https://github.com/magicoflolis/MagicCards/blob/main/CHANGELOG.md
// @license MIT
// ==/Scenario==
//#region MagicCards
/**
* @type { Partial }
*/
const OPTIONS = {
/*
Uncomment if you use large AI models, "premium account"
settings: {
autoHistory: true,
autoRetrieve: true,
useSmallModel: false
}
*/
};
//#region Requirements
//#region Polyfill
{
if (typeof globalThis.stop !== 'boolean') {
globalThis.stop = false;
}
if (typeof globalThis.text !== 'string') {
globalThis.text = ' ';
}
globalThis.history ??= [];
globalThis.storyCards ??= [];
globalThis.info ??= {
actionCount: 0,
characters: []
};
globalThis.state ??= {
memory: {},
message: ''
};
state.messageHistory ??= [];
}
{
/** https://jsr.io/@li/regexp-escape-polyfill */
const SYNTAX_CHARACTERS = /[\^$\\.*+?()[\]{}|]/;
const CONTROL_ESCAPES = new Map([
['\t', 't'],
['\n', 'n'],
['\v', 'v'],
['\f', 'f'],
['\r', 'r']
]);
const OTHER_PUNCTUATORS = /^[,\-=<>#&!%:;@~'`"]$/;
const WHITE_SPACE = /^[\t\v\f\uFEFF\p{Zs}]$/u;
const LINE_TERMINATOR = /^[\n\r\u2028\u2029]$/;
const SURROGATE = /^[\uD800-\uDFFF]$/;
const DECIMAL_DIGIT = /^[0-9]$/;
const ASCII_LETTER = /^[a-zA-Z]$/;
/**
* @param {string} str
* @returns {string}
*/
const regExpEscape = (str) => {
if (typeof str !== 'string') {
throw new TypeError('Expected a string');
}
let escaped = '';
for (const c of str) {
if (escaped === '' && (DECIMAL_DIGIT.test(c) || ASCII_LETTER.test(c))) {
escaped += `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`;
} else {
escaped += encodeForRegExpEscape(c);
}
}
return escaped;
};
Object.defineProperty(regExpEscape, 'name', { value: 'escape' });
/**
* @param {string} c - A single code-point char.
* @returns {string} the encoded representation of `c`.
*/
function encodeForRegExpEscape(c) {
if (SYNTAX_CHARACTERS.test(c) || c === '/') {
return '\\' + c;
}
if (CONTROL_ESCAPES.has(c)) {
return '\\' + CONTROL_ESCAPES.get(c);
}
if (
OTHER_PUNCTUATORS.test(c) ||
WHITE_SPACE.test(c) ||
LINE_TERMINATOR.test(c) ||
SURROGATE.test(c)
) {
// eslint-disable-next-line no-control-regex
if (/[\x00-\xFF]/.test(c)) {
return `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`;
}
return c
.split('')
.map((c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`)
.join('');
}
return c;
}
Object.defineProperty(RegExp, 'escape', {
value: regExpEscape,
writable: true,
enumerable: false,
configurable: true
});
}
//#endregion
//#region Utilities
/**
* @param {?} obj
* @returns {string}
*/
const objToStr = (obj) => {
try {
return Object.prototype.toString.call(obj).match(/\[object (.*)\]/)?.[1] || '';
} catch {
return '';
}
};
/**
* @template T
* @template {Record} A
* @param {T | null | undefined} target - The target to normalize into an array
* @param {A} [args={}]
* @returns {T extends null | undefined ? [] : T extends readonly unknown[] ? T : T extends string ? A extends { split: true; } ? string[] : [T] : A extends { entries: true; } ? T extends Record ? Array<[K extends string ? K : string, V]> : Array<[string, unknown]> : A extends { keys: true; } ? T extends Record ? Array : T extends Set | Map ? K[] : string[] : A extends { values: true; } ? T extends Record ? V[] : T extends Set | Map ? V[] : unknown[] : T extends Iterable ? U[] : unknown[]}
*/
const toArray = (target, args = {}) => {
if (target == null) return [];
if (Array.isArray(target)) return target;
/** @type {keyof typeof args | undefined} */
const method = ['split', 'entries', 'keys', 'values'].find((key) => args[key]);
if (typeof target === 'string') return method === 'split' ? [...target] : [target];
if (method != null) {
const s = objToStr(target);
const m = method === 'split' ? 'keys' : method;
if (/Object/.test(s)) {
if (Object[m]) return Array.from(Object[m](target));
} else if (/Set|Map/.test(s)) {
/** @type {Set | Map} */
const prim = target;
if (prim[m]) return Array.from(prim[m]());
}
}
return Array.from(target);
};
/**
* @param {?} obj
* @returns {obj is Record}
*/
const isObj = (obj) => /Object/.test(objToStr(obj));
/**
* @param {?} obj
* @returns {obj is (null | undefined)}
*/
const isNull = (obj) => Object.is(obj, null) || Object.is(obj, undefined);
/**
* Object is Blank
* @template O
* @param {O} obj
* @returns {boolean}
*/
const isBlank = (obj) => {
if (typeof obj === 'string') return Object.is(obj.replaceAll('\0', '').trim(), '');
return Object.is(toArray(obj, { split: true }).length, 0);
};
/**
* Object is Empty
* @template O
* @param {O} obj
* @returns {boolean}
*/
const isEmpty = (obj) => isNull(obj) || isBlank(obj);
/**
* @param {?} elem
* @returns {elem is string}
*/
const isValid = (elem) => typeof elem === 'string' && !isBlank(elem);
/**
* @param {?} a
* @param {?} b
* @returns {boolean}
*/
const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);
/**
* @param {string} num
* @returns {boolean}
*/
const isNum = (num) => !Number.isNaN(Number.parseInt(num, 10));
/**
* @template T
* @param {...T} arr
*/
const rmDup = (...arr) => [...new Set(arr.flat(1))];
/**
* @param {...string} data
* @returns {string}
*/
const prose = (...data) => (data.every((i) => typeof i === 'string') ? data.join('\n').trim() : '');
/**
* @param {?} a
* @param {?} b
* @returns {boolean}
*/
const equalArr = (a, b) => isEqual([...new Set(a)].sort(), [...new Set(b)].sort());
/**
* @param {?} obj
*/
const toEntry = (obj) =>
Object.entries(obj)
.map(([, val]) => (isObj(val) ? Object.entries(val) : val))
.flat(Infinity)
.sort();
//#endregion
//#region Words
class Words extends null {
/**
* Converts the first character of a string to its Unicode code point in hexadecimal.
* @param {string} str
* @returns {string}
*/
static toCodePoint(str) {
const cp = str.codePointAt(0);
if (cp === undefined) throw new TypeError('Input string is empty.');
return cp.toString(16);
}
/**
* @template {string | number} N
* @param {N} num
* @returns {string}
*/
static getUniHex(num) {
return String.fromCodePoint(typeof num === 'number' ? num : parseInt(num, 16));
}
/**
* @template {string} S
* @template {number} L
* @param {S} str
* @param {L} lengthLimit
*/
static limit(str, lengthLimit) {
if (typeof str === 'string' && typeof lengthLimit === 'number' && lengthLimit < str.length) {
return str.slice(0, lengthLimit).trim();
}
return str;
}
/**
* @param {string} str
* @returns {string[]}
*/
static split(str) {
return isValid(str) ? str.split(/\f|\t|\n|\r|\v|\0/) : [];
}
/**
* @template S
* @param {S} str
*/
static toLowerCase(str) {
return isValid(str) ? str.toLowerCase().trim() : '';
}
}
//#endregion
//#region Options
class Options extends null {
static createDefault() {
const $db = Options.createDB();
const p = prose;
/** @type {dataQueue} */
const data = {};
/**
* @type { defaultOptions }
*/
const _default = {
settings: {
autoHistory: false,
autoRetrieve: false,
cooldown: 22,
enabled: true,
hiddenCards: ['debug'],
useSmallModel: true
},
data,
database: [
{
category: [
'Age',
'Gender',
'Personality',
'Appearance',
'Relationships',
'Quirks',
'Flaws',
'Likes',
'Dislikes',
'Occupation',
'Backstory',
'Hobbies',
'Other'
],
instruction: {
user: p('', '$2', 'Output Entry:', '$1 = $3'),
example: 'name1 = [CAT1:TRAIT1(DESC1)[,...];...]'
},
type: /** @type {const} */ ('Characters')
},
{
category: [
'Location',
'Unique Features',
'Setting',
'Factions',
'Threats',
'Society',
'Government',
'Military',
'Cultural Traits',
'Economy',
'Religion',
'Other'
],
instruction: {
user: p('', '$2', 'Output Entry:', '$1 = $3'),
example: 'name1 = [CAT1:TRAIT1(DESC1)[,...];...]'
},
type: /** @type {const} */ ('Locations')
},
{
category: ['Name'],
instruction: {
user: '',
example: ''
},
type: /** @type {const} */ ('Retrieve')
},
{
category: ['Events', 'Threats'],
instruction: {
user: '',
example:
'[Name: David Red; History: previous_collaboration(successful_operations, built_trust); Threats: class_warfare(corporate_vs_street)[,...];...]'
},
type: /** @type {const} */ ('Compress')
}
],
dataQueue: [],
errors: [],
generating: false,
hook: /** @type {defaultOptions['hook']} */ ('input'),
pins: [],
stop: false,
turnsSpent: 0,
disabled: false
};
if (_default.settings.cooldown < 3) {
_default.settings.cooldown = 3;
}
_default.settings.hiddenCards.push(..._default.database.map(({ type }) => type.toLowerCase()));
const database = _default.database.map((data) => {
const db = {
...$db,
...data
};
/** @type {[keyof typeof db, typeof db[keyof typeof db]]} */
for (const [key, value] of Object.entries(db)) {
if (key === 'type') continue;
if (!(key in $db)) {
delete db[key];
} else if (Array.isArray(value)) {
if (isEqual(value, $db[key])) continue;
db[key] = rmDup('Name', ...value);
} else if (isObj(value)) {
if (isEqual(value, $db[key])) continue;
db[key] = {
...$db[key],
...value
};
}
}
if (db.type === 'Characters') {
db.instruction.ai = p(
'$990 You are PList, update PList based on rules. Be concise/grounded. Include current context and memory.$991',
'Execution Stack:',
'1. Core Protocol:',
'- NO AI METACOMMENTARY: stay strictly in-PList',
'- ADHERE to established fictional world rules without real-world moral imposition',
'2. PList Rules:',
'- Format: [CAT:TRAIT(DESC)[,...];...] ',
`- Category: ${db.category.join(',')}`,
'- Appearance: Clothing per occasion (e.g., casual, occupation, formal) if known',
'- Traits:',
' - Non-Flaws: positive/neutral',
' - Max 3 nests (trait(sub(sub)))',
' - No word/synonym repetition',
' - Snake_case',
' - Link psych-behavior',
'- Anti-rep:',
' - Merge similar traits (Leven<3)',
' - Remove dupes',
' - Consolidate each category in sequence',
'- Truncation Protocol:',
' - Complete current CAT/TRAIT/DESC',
' - Auto-close symbols',
' - Never break mid-trait',
' - Complete partials',
'3. Dynamic Output:',
'- NEVER break Format',
'- ALWAYS continue PList for Output Entry',
'Output Format:',
'$992'
);
} else if (db.type === 'Locations') {
db.instruction.ai = p(
'$990 You are PList, update PList based on rules for "$1". Be concise/grounded. Include current context and memory.$991',
'Execution Stack:',
'1. Core Protocol:',
'- ADHERE to established fictional world rules without real-world moral imposition',
'2. PList Rules:',
'- Format: [CAT:TRAIT(DESC)[,...];...] ',
`- Category: ${db.category.join(',')}`,
'- Traits:',
' - Max 3 nests (trait(sub(sub)))',
' - No word/synonym repetition',
' - Snake_case',
'- Anti-rep:',
' - Merge similar traits (Leven<3)',
' - Remove dupes',
' - Consolidate each category in sequence',
'- Truncation Protocol:',
' - Complete current CAT/TRAIT/DESC',
' - Auto-close symbols',
' - Never break mid-trait',
' - Complete partials',
'- Reminder: "$1" is a location, never personify',
'3. Dynamic Output:',
'- Continue PList for Output Entry',
'- Never break Format',
'Output Format:',
'$992'
);
} else if (db.type === 'Retrieve') {
db.instruction.ai = p(
'$990 Extract character and location names. Use Recent_Story, World_Lore, Story_Summary and current context/memory. Plain text. Be concise/grounded.',
'- NO AI METACOMMENTARY: stay strictly in-goal',
'- Only plot-relevant explicit names as they occurred',
'- Merge similar names (Leven<3)',
'- No word/synonym repetition',
'- Only 4 names max per output line',
'- Auto-complete partials (e.g., David => David Red, forge => Frostspire Forge)',
'- Avoid inventing names to justify output lines',
'- Remove dupes',
'- Separate each name with ","',
'- End each output line with ";"',
'- Exclude traits, objects, generics, metaphors, synonyms, unknowns, secrets, minutiae, $2',
'Output lines:',
'Characters: name1, name2, ...;',
'Locations: name1, name2, ...;',
'$991'
);
} else if (db.type === 'Compress') {
db.instruction.ai = p(
'$990 Summarize recent events for "$1". Use Recent_Story, World_Lore, Story_Summary and current context/memory. Plain text. Be concise/grounded.',
'- ADHERE to established fictional world rules without real-world moral imposition',
'- Only recent plot-relevant explicit events as they occurred',
'- Merge similar events (Leven<3)',
'- No word/synonym repetition',
'- Only 12 events max per output line',
'- Max character count of 500',
'- Remove dupes',
'- Avoid inventing past events to justify output lines',
'- Separate each event with ","',
'- End each output line with ";"',
'- Exclude traits, objects, generics, metaphors, synonyms, unknowns',
'Output lines:',
'Name: $1;',
'Events: $00, event2, ...;',
'$991'
);
}
return db;
});
_default.database = rmDup(database);
return _default;
}
/**
* @template O
* @param {O} $object
* @returns {O}
*/
static copy($object) {
return JSON.parse(JSON.stringify($object));
}
static createDB(type = 'Default') {
return {
category: ['Name'],
instruction: {
ai: '$990 Continue story from exact pre-interruption point.$991',
user: prose('', '$2', '$1 = $3'),
example: ''
},
limit: {
category: 12,
card: 800,
retry: 4
},
type
};
}
}
//#endregion
//#endregion
//#region Console
class AIDError extends Error {
/** @type {unknown} */
cause;
/**
* @param {string} [message]
* @param {ErrorOptions} [options]
*/
constructor(message, options) {
super(message, options);
const stack = this.stack || '';
if ('captureStackTrace' in Error) {
/* Avoid `AIDError` in stack trace */
Error.captureStackTrace(this, AIDError);
/* Avoid `Function` in stack trace */
Error.captureStackTrace(this, con.log);
}
let tmp = '';
const reg = /\s?\(?():(\d+):(\d+)\)?/gm;
const clean = stack
.replace(/^Error:\s(\w*Error:)/gm, (_m, p1) => p1)
.replace(reg, (_m, _p1, line, column) => `:${line}:${column}`);
tmp += clean;
this.stack = clean;
if (!this.cause) {
const [, c] = /\s(\w+):\d+:\d+/.exec(stack) ?? [];
if (c) {
this.cause = c;
} else {
this.cause = 'Unknown';
}
}
this.message = `[${this.cause}] ${tmp}`;
}
}
class con extends null {
/**
* @param {...?} messages
*/
static log(...messages) {
messages.forEach((m) => {
if (m instanceof Error) {
const e = m instanceof AIDError ? m : new AIDError(m.message, m.cause || undefined);
con.msg(e.message);
} else {
console.log(m);
}
});
}
/**
* @param {Error['message']} error
* @param {?} [cause]
*/
static err(error, cause) {
/** @type {ErrorOptions} */
let errorOptions = cause ? { cause } : { cause: 'Unknown' };
let message = error;
if (error instanceof Error) {
message = error.message;
if (error.cause) errorOptions = { cause: error.cause };
}
console.log(new AIDError(message, errorOptions).message);
}
/**
* @param {...string} messages
*/
static msg(...messages) {
const MESSAGES = messages.filter((m) => typeof m === 'string' && !Object.is(state.message, m));
if (isBlank(MESSAGES)) return;
if (state.messageHistory) state.messageHistory.push(...MESSAGES);
}
}
//#endregion
class MagicCards {
//#region MC Utilities
static constant = {
type: `${Words.getUniHex('1fa84')}š“`,
location: `${Words.getUniHex('1fa84')}šļø`,
internal: `${Words.getUniHex('1fa84')}š§`,
storyCard: 2000
};
/** Filter output lines */
static regExp = /=>|š“|ā ļø/g;
/**
* Find and create Story Cards
* @param { Partial } $StoryCard
* @param { ((value: StoryCard, index: number, array: StoryCard[]) => boolean) | undefined } [callback]
* @param { number } [remaining=2] - Number of remaining retries before function times out.
* @returns { { id: string; index: number; card: StoryCard | null; error?: AIDError } }
*/
StoryCard($StoryCard = {}, callback, remaining = 2) {
if (remaining > 0) {
remaining -= 1;
const {
keys = '',
entry = '',
type = MagicCards.constant.type,
title = keys,
description,
id,
pin = false
} = $StoryCard;
if (typeof callback !== 'function') {
callback = ({ keys: k, entry: e, title: t, type: ty, description: d, id: i }) => {
return (
(id != null && id === i) ||
(description != null && description === d) ||
(entry === e && type === ty && (keys === k || title === t))
);
};
}
const card = storyCards.find(callback);
if (card) {
if (id) card.id = id;
if (pin) {
if (!this.cache.pins.includes(id)) this.cache.pins.push(id);
storyCards.splice(Math.max(0, this.cache.pins.indexOf(id)), 0, card);
}
return { id: card.id, index: storyCards.indexOf(card), card };
}
storyCards.push({ id, keys, entry, type, title, description });
return this.StoryCard($StoryCard, callback, remaining);
}
return {
id: '-2',
index: -2,
card: null,
error: new AIDError(`Failed, "${$StoryCard.title ?? 'StoryCard'}" has timed out.`, {
cause: 'MagicCards.StoryCard()'
})
};
}
installed = false;
/**
* @type { ?{ id: string; index: number; card: StoryCard } }
*/
debugCard = null;
/**
* @type { defaultOptions }
*/
cache = {};
/** @type {ModifierFN[]} */
modifiers = [];
get text() {
return text == null ? text : `${text}`;
}
set text(str) {
if ((typeof str === 'string' && !Object.is(str, '')) || str === null) text = str;
}
get stop() {
const c = isEmpty(this.cache) ? { hook: 'input', stop: false } : this.cache;
if (!Object.is(c.stop, globalThis.stop)) globalThis.stop = c.stop;
return c.stop && /context/.test(c.hook);
}
set stop(bol) {
const c = isEmpty(this.cache) ? { stop: false } : this.cache;
if (typeof bol === 'boolean' && !Object.is(c.stop, bol)) {
c.stop = bol;
if (!Object.is(c.stop, globalThis.stop)) globalThis.stop = c.stop;
}
}
get turn() {
if (typeof info.actionCount === 'number') {
return Math.abs(info.actionCount);
}
return 0;
}
get actionCount() {
return this.turn - this.cache.turnsSpent;
}
get history() {
const s = this.turn - this.cache.settings.cooldown;
return (history[s] ? history.slice(s) : history)
.filter(({ text }) => !isEmpty(text) && !MagicCards.regExp.test(text))
.map((h) => {
return {
index: history.indexOf(h),
text: Words.split(h.text).join(' ').trim(),
rawText: h.text,
type: h.type
};
});
}
//#endregion
//#region Constructor
/**
* @param { Partial } options
*/
constructor(options = {}) {
//#region Binders
this.input = this.input.bind(this);
this.context = this.context.bind(this);
this.output = this.output.bind(this);
this.message = this.message.bind(this);
this.StoryCard = this.StoryCard.bind(this);
//#endregion
this.#modifier();
if (isObj(options)) {
for (const s of ['generating', 'stop', 'turnsSpent', 'hook'])
if (s in options) delete options[s];
} else {
options = {};
con.err(
new TypeError('"options" must be a type of JSON Object', {
cause: 'MagicCards.constructor()'
})
);
}
const _default = Options.createDefault();
/**
* @param { ...defaultOptions } params
* @returns { defaultOptions }
*/
const initConfig = (...params) => {
const obj = {};
for (const p of params.filter(isObj)) {
for (const [k, v] of toArray(p, { entries: true })) {
if (k === 'settings' && !isEqual(p[k], _default[k])) {
for (const key of toArray(_default[k], { keys: true })) {
if (!(key in v)) v[key] = _default[k][key];
}
}
Object.assign(obj, { [k]: v });
}
}
return obj;
};
this.cache = initConfig(_default, options, state.MagicCards);
if (!('settings' in this.cache)) {
this.cache = _default;
this.cache.errors.push('Invalid config: restoring...');
}
const hook = this.#hook();
const cache = this.cache;
//#region Init Card
/**
* @template {keyof defaultOptions | dataEntry['type']} T
* @param {T} title
* @returns {{ card: StoryCard; index: number; id: string }}
*/
const getInitCard = (title) => {
if (typeof title !== 'string') throw new TypeError('Expected a string');
const main = /settings/i.test(title);
/**
* @param {StoryCard} sc
* @returns {boolean}
*/
const cb = (sc) => {
try {
const { card } = this.storyCards.edit(sc, 1);
return Words.toLowerCase(card.id) === title;
} catch {
return false;
}
};
/**
* @type {OptionId}
*/
const obj = {
id: title,
pin: true
};
const SC = this.StoryCard(
{
id: JSON.stringify(obj),
title: title.toUpperCase(),
type: MagicCards.constant.internal,
pin: obj.pin
},
cb
);
if (isNull(SC.card)) throw SC.error;
if (/debug/i.test(title)) {
return {
card: SC.card,
id: SC.id,
index: SC.index
};
}
/**
* Changed values of this Story Card
* @type {defaultOptions['settings'] | dataEntry}
*/
const $config = {};
let equ = true;
/** @type { any } */
let opt;
if (main) {
opt = Options.copy(cache.settings);
} else {
const Title = Words.toLowerCase(title);
opt =
cache.database.find(({ type }) => Words.toLowerCase(type) === Title) ??
Options.createDB(title);
}
opt = Object.fromEntries(Object.entries(opt).sort());
Object.assign($config, opt);
if (!isEmpty(SC.card.entry) && isBlank(cache.errors)) {
const scEntry = SC.card.entry.matchAll(/^([\w\s]+):\x20?(.*)/gm) || [];
if (main) {
for (const [, key, value] of scEntry) {
if (!(key in opt)) continue;
if (/(true|false)$/im.test(value)) {
$config[key] = Words.toLowerCase(value) === 'true';
} else if (isNum(value)) {
$config[key] = Number(value);
} else if (key === 'hiddenCards') {
$config[key] = value
.split(/,|\[|\]/)
.filter((i) => !isBlank(i))
.map((i) => i.trim());
}
}
equ = equalArr(Object.entries($config), Object.entries(opt));
if (!equ) {
Object.assign(cache.settings, $config);
this.message(`Updated ${title}.`);
}
} else {
$config.instruction.ai = SC.card.description;
for (const [, key, value] of scEntry) {
if (isBlank(value) || !(key in $config)) continue;
if (/limit|category/.test(key)) {
if (key === 'limit') {
$config[key] = JSON.parse(value.replaceAll(/([\w\d]+)/g, '"$1"'));
for (const k of toArray($config[key], { keys: true })) {
$config[key][k] = Number($config[key][k]);
}
} else {
$config[key] = value
.split(/,|\[|\]/)
.filter((i) => !isBlank(i))
.map((i) => i.trim());
$config[key].splice(0, 0, 'Name');
}
} else if (key === 'user') {
const [, user] = SC.card.entry.match(/user:([\s\S]+)(?=example:)/gi) || [];
if (user) $config.instruction[key] = user;
} else if (key === 'example') {
const [, user] = SC.card.entry.match(/example:([\s\S]+)(?=limit:)/gi) || [];
if (user) $config.instruction[key] = user;
} else {
$config[key] = value;
}
}
equ = equalArr(toEntry($config), toEntry(opt));
if (!equ) {
if (
!equalArr($config.category, opt.category) ||
$config.limit.category !== opt.limit.category
)
$config.limit.category = opt.limit.category = $config.category.length;
const i = cache.database.indexOf(opt) ?? 0;
cache.database.splice(i, 1, $config);
this.message(`Updated ${title}.`);
}
}
} else {
equ = false;
}
if (!equ) {
const scLimit = MagicCards.constant.storyCard;
const resp = [];
const add = (entry) => resp.push(entry.trim());
for (const [k, v] of toArray($config, { entries: true })) {
if (isObj(v)) {
const val = toArray(v, { entries: true })
.filter(([key]) => key !== 'ai')
.map(([key, value]) => `${key}: ${value}`);
if (isBlank(val)) continue;
if (k === 'instruction') {
for (const key of val) {
add(key);
}
} else {
add(`${k}: {${val.join(', ')}}`);
}
} else if (Array.isArray(v) && !isBlank(v)) {
add(`${k}: [${(k === 'category' ? v.slice(1) : v).join(', ')}]`);
} else {
add(`${k}: ${v}`);
}
}
if (!main) {
SC.card.description = Words.limit($config.instruction.ai, scLimit);
}
SC.card.entry = Words.limit(resp.join('\n').trim(), scLimit);
}
return {
card: SC.card,
id: SC.id,
index: SC.index
};
};
if (hook === 'context' && this.turn > 2) {
getInitCard('settings');
const addDB = (cache.settings.useSmallModel ? [] : cache.database)
.map(({ type }) => type.toLowerCase())
.filter((type) => !/default|settings/.test(type));
const hiddenCards = Array.isArray(cache.settings.hiddenCards)
? cache.settings.hiddenCards
: [];
if (!hiddenCards.includes('debug')) this.debugCard = getInitCard('debug');
for (const type of addDB.filter((type) => !hiddenCards.includes(type))) {
getInitCard(type);
}
for (const type of addDB.filter((type) => hiddenCards.includes(type))) {
const { index, card } = this.storyCards.get(type);
if (isNull(card)) continue;
storyCards.splice(index, 1);
}
// Only works on turn 3
if (this.turn === 3) {
const memoryBank = storyCards.some(
({ id }) => typeof id === 'string' && id.startsWith('{') && id.endsWith('}')
);
if (!memoryBank) {
cache.errors.push(
'Please enable "MEMORY BANK"\nPath: GAMEPLAY > MEMORY SYSTEM > MEMORY BANK'
);
cache.settings.enabled = false;
}
}
}
//#endregion
if (cache.settings.cooldown < 3) {
cache.settings.cooldown = 3;
cache.errors.push('Invalid cooldown: must be <= 3, restoring...');
}
if (cache.settings.useSmallModel === true && cache.settings.autoRetrieve === true) {
cache.errors.push('Invalid: disable "useSmallModel" first');
cache.settings.autoRetrieve = false;
}
this.refresh(false);
try {
if (hook === 'output') {
if (isEmpty(cache.errors)) {
if (this.turn === 3) {
this.message('Install complete!');
} else if (!this.turn) {
this.message('Installing...');
}
}
while (cache.errors.length > 0) {
const text = cache.errors.shift();
if (!text) break;
this.message({ emoji: 'ā ļø', text });
}
}
if (!cache.settings.enabled) {
cache.data = {};
cache.dataQueue = [];
cache.generating = false;
}
const installed =
this.turn >= 3 &&
typeof this[hook] === 'function' &&
(cache.settings.enabled || hook === 'input');
this.installed = installed;
if (installed) this[hook]();
} catch (e) {
con.err(e, hook);
}
}
//#endregion
/**
* Ensures `modifier` function exists & prevent it from being `undefined`
*
* _This will ALWAYS be the last function executed!_
*
* - Add your modifier functions into `hook()`
* - **Modifier functions are not called when `mc.cache.generating === true`**
*/
#modifier() {
const modifier = () => {
const c = isEmpty(this.cache) ? { hook: 'input', generating: false } : this.cache;
if (!c.generating) {
/**
* @param {Text | ModifierFN | [typeof text, typeof stop] | ReturnType} val
*/
const extract = (val) => {
const str = objToStr(val);
if (/(Async|Generator)Function|Promise/.test(str)) {
throw new TypeError(`Unsupported, "val" is a type of ${str}.`);
} else if (typeof val === 'function') {
if (/(auto|smart|magic)cards?/i.test(val.name)) {
throw new TypeError(`Please remove "${val.name}" incompatible with MagicCards.`);
}
return extract(val(this.text, this.stop, c.hook));
} else if (Array.isArray(val)) {
const [TEXT = ' ', STOP = false] = val;
if (Object.is(STOP, true)) this.stop = STOP;
return TEXT;
} else if (isObj(val)) {
const { text, stop = false } = val;
if (Object.is(stop, true)) this.stop = stop;
return text;
}
return val;
};
for (const fn of rmDup(this.modifiers)) {
try {
const txt = extract(fn);
if (!Object.is(this.text, txt) && (typeof txt === 'string' || isNull(txt)))
this.text = txt;
} catch (e) {
con.err(e, c.hook);
}
}
const scriptCards = storyCards.filter(
({ type }) => isValid(type) && //i.test(type.trim())
);
const results = [];
for (const card of scriptCards) {
try {
const code = `${card.entry}\n${card.description}`;
if (isBlank(code)) continue;
const hook = (card.title ?? 'context')
.toLowerCase()
.split(',')
.find((hook) => hook.trim() === c.hook);
if (!hook) continue;
const parse = code.startsWith('return ') ? code : `return ${code}`;
results.push(eval(`(() => { ${parse} })()`));
} catch (e) {
con.err(e, c.hook);
}
}
if (!isBlank(results)) {
this.message(...results.map((entry) => `${c.hook}: ${entry}`));
}
}
if (c.hook === 'output' && !isBlank(state.messageHistory)) {
const message = rmDup(state.messageHistory).join('\nāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
console.log(message);
state.message = message;
state.messageHistory = [];
}
this.#hook(false);
this.#save();
const { text, stop } = this;
return { text, stop };
};
globalThis.modifier = modifier;
Object.freeze(globalThis.modifier);
return this;
}
/**
* @template S
* @param {...S} messages
*/
message(...messages) {
for (const m of messages) {
if (typeof m === 'string') {
con.msg(`š“ MagicCards => ${m}`);
} else if (isObj(m) && 'emoji' in m) {
con.msg(`${m.emoji} MagicCards => ${m.text}`);
}
}
return this;
}
refresh(resetCooldown = true) {
const cache = this.cache;
const $db = Options.createDB();
// This could be optimized
const database = cache.database.map((data) => {
const db = {
...$db,
...data
};
for (const [key, value] of toArray(db, { entries: true })) {
if (key === 'type') continue;
if (!(key in $db)) {
delete db[key];
} else if (Array.isArray(value)) {
if (isEqual(value, $db[key])) continue;
db[key] = rmDup('Name', value);
} else if (isObj(value)) {
if (isEqual(value, $db[key])) continue;
for (const k of toArray($db[key], { keys: true })) {
if (!(k in value)) value[k] = $db[key][k];
}
} else if (!Object.is(value, $db[key])) {
db[key] = $db[key];
}
}
db.limit.category = db.category.length;
return db;
});
cache.database = rmDup(database);
if (resetCooldown) {
cache.settings.cooldown =
OPTIONS?.settings?.cooldown ?? Options.createDefault().settings.cooldown;
}
cache.generating = !(isEmpty(cache.dataQueue) && isEmpty(cache.data));
if (!Object.is(cache.stop, globalThis.stop)) globalThis.stop = cache.stop;
return this;
}
/**
* Save changed values of `this.cache` into `state` object
*/
#save() {
const defaultOptions = Options.createDefault();
const config = Options.copy(this.cache);
if (!isEqual(config, defaultOptions)) {
for (const [key, value] of toArray(config, { entries: true })) {
if (!(key in defaultOptions)) {
delete config[key];
} else if ((Array.isArray(value) || isObj(value)) && isEqual(value, defaultOptions[key])) {
delete config[key];
} else if (Object.is(value, defaultOptions[key])) {
delete config[key];
}
}
state.MagicCards = config;
}
return config;
}
print(message = '', emoji = 'š“', name = this.cache.data.name) {
const data = this.cache.data;
if (!isEmpty(data)) {
return `\n- ${emoji} ā${isEmpty(name) ? 'MagicCards' : name}ā => ${message}\n`;
}
return `\n- ${emoji} => ${message}\n`;
}
/**
* @param {Partial} data - create data query object
* @param {boolean} [autoAdd=false] - push into `this.cache.dataQueue`
*/
queue(data = {}, autoAdd = false) {
data.type ??= 'Characters';
const db =
this.cache.database.find(({ type }) => data.type === type) ?? Options.createDB(data.type);
const _default = {
name: '',
entry: '',
extra: [],
output: '',
progress: 0,
loaded: {},
...db
};
const init = (...params) => {
const obj = {};
for (const p of params.filter(isObj)) {
for (const [k, v] of toArray(p, { entries: true })) {
if (isObj(v) && !isEqual(p[k], _default[k])) {
for (const key of toArray(_default[k], { keys: true })) {
if (!(key in v)) v[key] = _default[k][key];
}
}
Object.assign(obj, { [k]: v });
}
}
return obj;
};
/** @type {dataQueue} */
const resp = init(_default, data);
const searchValue = /[^\w\s]+/g;
for (let e of [resp.name, resp.entry].filter((i) => !isEmpty(i))) {
if (typeof e === 'string') e = Words.split(e.trim()).join(' ').replaceAll(searchValue, '');
}
resp.name = resp.name.replace(/\.$/, '').trim();
resp.entry = resp.entry.replace(/\.$/, '').trim();
if (autoAdd && !this.cache.dataQueue.includes(resp)) this.cache.dataQueue.push(resp);
return resp;
}
get storyCards() {
const settings = this.cache.settings;
const StoryCard = this.StoryCard;
return {
/**
* @param { Partial & { magicId?: MagicId } } $StoryCard
*/
create($StoryCard = {}) {
const { keys, title = keys, magicId = {} } = $StoryCard;
const { index, card, id } = this.get(title);
if (card) {
return {
id,
index,
card
};
}
magicId.id ??= title;
magicId.sync ??= true;
magicId.autoHistory ??=
magicId.sync && settings.autoHistory ? settings.useSmallModel === false : false;
magicId.defaultCooldown ??= settings.cooldown;
magicId.cooldown ??=
!magicId.sync && isNum(magicId.defaultCooldown)
? magicId.defaultCooldown
: settings.cooldown;
magicId.summary ??= '';
delete $StoryCard.magicId;
const toKeys = () => {
const key =
$StoryCard.entry && /locations?/i.test($StoryCard.entry)
? title
: title.split(/ |_/)[0];
const arr =
key.length < 6
? [
` ${key} `,
` ${key}'`,
`"${key} `,
` ${key}.`,
` ${key}?`,
` ${key}!`,
` ${key};`,
`'${key} `,
`(${key} `,
` ${key})`,
` ${key}:`,
` ${key}"`,
`[${key} `,
` ${key}]`,
`ā${key} `,
` ${key}ā`,
`{${key} `,
` ${key}}`
]
: [
`${key} `,
` ${key}`,
`${key}'`,
`"${key}`,
`${key}.`,
`${key}?`,
`${key}!`,
`${key};`,
`'${key}`,
`(${key}`,
`${key})`,
`${key}:`,
`${key}"`,
`[${key}`,
`${key}]`,
`ā${key}`,
`${key}ā`,
`{${key}`,
`${key}}`
];
arr.unshift(key);
let TEXT = '';
while (TEXT.length <= 100) {
const k = arr.shift();
if (!k) break;
TEXT += `${k};%`;
}
TEXT = TEXT.split(';%')
.filter((v) => !isBlank(v))
.join(',');
while (TEXT.length > 100) {
const t = TEXT.split(',').filter((v) => !isBlank(v));
const p = t.pop();
if (!p) break;
TEXT = t.join(',');
}
return TEXT;
};
return StoryCard({
...$StoryCard,
id: JSON.stringify(magicId),
keys: toKeys(),
title
});
},
/**
* @param {string} [TITLE]
*/
get(TITLE) {
const i = Words.toLowerCase(TITLE);
const card = storyCards.find((sc) => {
try {
const { card } = this.edit(sc, true);
return Words.toLowerCase(card.id) === i;
} catch {
return false;
}
});
if (card)
return {
id: card.id,
index: storyCards.indexOf(card),
card
};
return {
id: TITLE,
index: -2,
card: null
};
},
/**
* @template { StoryCard } S
* @template { number | undefined } T
* @template { T extends number ? OptionId : MagicId } C
* @param { S } sc
* @param { T } [type]
* @param { C } _default
* @returns { T extends number ? { card: C } : { card: C; save(): S; toString(): string } }
*/
edit(sc, type, _default = {}) {
/** @type {OptionId & MagicId} */
const obj = sc.id.startsWith('{') && sc.id.endsWith('}') ? JSON.parse(sc.id) : _default;
if (type === 1 || type === true)
return {
card: obj
};
if (typeof type === 'number') {
obj.pin ??= true;
} else {
obj.sync ??= true;
obj.autoHistory ??=
obj.sync && settings.autoHistory ? settings.useSmallModel === false : false;
obj.defaultCooldown ??= settings.cooldown;
obj.cooldown ??=
!obj.sync && isNum(obj.defaultCooldown) ? obj.defaultCooldown : settings.cooldown;
obj.summary ??= '';
if (obj.sync) {
if (obj.autoHistory !== settings.autoHistory) {
obj.autoHistory = settings.useSmallModel === false;
}
if (isNum(obj.defaultCooldown) && obj.defaultCooldown !== settings.cooldown) {
obj.cooldown = obj.defaultCooldown ?? settings.cooldown;
}
}
}
sc.description ??= '';
const desc = sc.description.matchAll(/^([\w\s]+):\x20?(.*)/gm) || [];
for (const [, key, value] of desc) {
const k = key.trim();
const v = value.trim();
if (!(k in obj)) continue;
let val;
if (/(true|false)$/im.test(v)) {
val = Words.toLowerCase(v) === 'true';
} else if (isNum(v)) {
val = Number(v);
} else {
val = v;
}
if (Object.is(obj[k], val)) continue;
obj[k] = val;
}
return {
card: obj,
save() {
sc.id = JSON.stringify(obj);
return sc;
},
toString() {
return Object.entries(obj)
.sort()
.map(([k, v]) => `${k}: ${v}`)
.join('\n');
}
};
}
};
}
get PList() {
const cache = this.cache;
return {
/**
* Transform strings into PList object
* @param {...string} strings
*/
from(...strings) {
/**
* @type { { [key: string]: string; } }
*/
const list = {};
for (const s of strings.filter((i) => typeof i === 'string')) {
for (const [, key, value, endof] of s.matchAll(/([\w\s]+):\s?([^;\]]+)(;|\])/g) || []) {
const k = key.trim();
const v = value.trim();
list[k] =
k in list && list[k] !== v ? rmDup(list[k].split(','), v.split(',')).join(',') : v;
if (/\]/.test(endof ?? '')) break;
}
}
const minimize = (a = []) => {
const j = `[${a.join(';')}]`;
if (j.length > MagicCards.constant.storyCard) {
a.shift();
return minimize(a);
}
return j.replace(';]', ']').trim();
};
return {
list,
toString() {
const arr = toArray(list, { entries: true });
return minimize(arr.map(([k, v]) => `${k}: ${v}`));
}
};
},
data: {
category: {
get() {
cache.data = isEmpty(cache.data) ? cache.dataQueue.shift() || {} : cache.data;
if (isEmpty(cache.data.category)) return [];
const a = toArray(cache.data.loaded, { keys: true });
return cache.data.category.filter((c) => !a.includes(c));
},
toString() {
return this.get().join(',');
}
},
/**
* @param { boolean } [isOutput=false]
* @param { ...{ text: string } } arr
*/
get(isOutput = false, ...arr) {
cache.data = isEmpty(cache.data) ? cache.dataQueue.shift() || {} : cache.data;
const data = cache.data;
const reg = /([\w\s]+):\s?([^;[\]\n]+)(;|\]|\n)/g;
for (const { text } of rmDup(arr).filter(({ text }) => isValid(text))) {
for (const [, key, value, endof] of text.matchAll(reg) || []) {
const k = key
.trim()
.split('')
.map((val, i) => (i === 0 ? val.toUpperCase().trim() : Words.toLowerCase(val)))
.join('');
if (data.category.includes(k) && !(k in data.loaded)) {
data.loaded[k] = value.trim();
}
if (/\]/.test(endof)) break;
}
}
const loaded = data.category.filter((i) => i in data.loaded).length;
const progress = () => +((loaded / data.limit.category) * 100).toFixed(2);
let extra = '';
if (isOutput && Object.is(progress(), data.progress)) {
if (data.limit.retry > 0) {
data.limit.retry -= 1;
data.limit.category -= 1;
} else if (data.limit.retry <= 0) {
data.limit.category = loaded;
}
extra = ` āŗ = ${data.limit.retry}`;
}
const complete = loaded >= data.limit.category;
const raw = this.toString();
data.progress = complete ? 100 : progress();
const minimize = (str = '') => {
const arr = [...data.category].reverse();
while (str.length > data.limit.card) {
const r = new RegExp(`(${arr.shift()}):\\s?[^;\\]\\n]+(;|\\]\\n)`, 'g');
str = str.replaceAll(r, '');
}
return str;
};
return {
complete,
list: (complete ? minimize(`[${raw}]`) : `[${raw}`).replace(';]', ']'),
raw,
extra
};
},
toString() {
cache.data = isEmpty(cache.data) ? cache.dataQueue.shift() || {} : cache.data;
if (isEmpty(cache.data.category)) return '';
cache.data.loaded.Name ??= cache.data.name;
const arr = toArray(cache.data.loaded, { entries: true })
.filter(([k]) => cache.data.category.includes(k))
.sort(([a], [b]) => cache.data.category.indexOf(a) - cache.data.category.indexOf(b));
cache.data.loaded = Object.fromEntries(arr);
return arr.map(([k, v]) => `${k}: ${v}`).join(';');
}
}
};
}
#hook(load = true) {
const hooks = {
input: false,
context: false,
output: false
};
const cache = this.cache;
if (load) {
if (isNum(info.maxChars)) {
hooks.input = false;
hooks.context = true;
hooks.output = false;
} else if (cache.hook === 'input') {
hooks.input = true;
hooks.context = false;
hooks.output = false;
} else if (cache.hook === 'output') {
hooks.input = false;
hooks.context = false;
hooks.output = true;
}
} else {
hooks.input = cache.hook === 'output';
hooks.context = cache.hook === 'input';
hooks.output = cache.hook === 'context';
}
cache.hook = Object.keys(hooks).find((k) => hooks[k]) ?? 'input';
return cache.hook;
}
//#region Hooks
input() {
const cache = this.cache;
const t = this.text.trim();
if (
/^(||\[system:)/i.test(t) ||
t.startsWith('##') ||
(t.startsWith('**') && t.endsWith('**'))
) {
cache.disabled = true;
}
if (cache.disabled) return this;
/**
* @type { { name: string; cmd: string; emoji: string; message: string; }[] }
*/
const queues = [];
const reserved = new RegExp(`(${cache.database.map(({ type }) => type).join('|')})$`, 'i');
for (const [, cmd, name = '', entry = ''] of t.matchAll(
/(\/[am\s]+[cl])\s+([^"'/;]+);?([^"'/;]+)?/gi
) || []) {
const type = /l$/.test(cmd.toLowerCase().trim()) ? 'Locations' : 'Characters';
const data = this.queue({ name, entry, type });
if (isBlank(data.name)) continue;
const r = new RegExp(RegExp.escape(data.name.split(/ |_/)[0]), 'i');
const inQueue =
queues.some((dq) => r.test(dq.name)) || cache.dataQueue.some((dq) => r.test(dq.name));
const messages = [];
let emoji = 'š“';
if (inQueue) {
emoji = 'ā ļø';
messages.push('Already in queue');
} else if (/(retrieve|get)$/.test(data.name)) {
emoji = 'ā ļø';
messages.push('Executing entry check...');
const excludes = [];
for (const sc of this.magicCards()) {
const { card } = this.storyCards.edit(sc, undefined);
excludes.push(card.id.trim());
}
this.queue(
{
entry: rmDup(excludes.filter((i) => !isEmpty(i))).join(','),
type: 'Retrieve'
},
true
);
} else if (/((dis|en)able|toggle)$/.test(data.name)) {
emoji = 'ā ļø';
this.cache.settings.enabled = !this.cache.settings.enabled;
messages.push(`Toggling MagicCards: ${this.cache.settings.enabled ? 'on' : 'off'}`);
} else if (/(reset|restart|restore)$/.test(data.name)) {
emoji = 'ā ļø';
messages.push('Restoring settings...');
this.cache = Options.createDefault();
} else if (/(clear|clr|cls)$/.test(data.name)) {
emoji = 'ā ļø';
messages.push('Clearing cache...');
cache.dataQueue = [];
cache.data = {};
cache.generating = false;
} else if (/(summarize|compress)$/.test(data.name)) {
emoji = 'ā ļø';
messages.push('Compressing Story Cards...');
for (const sc of this.magicCards()) {
const edit = this.storyCards.edit(sc, undefined);
if (edit.card.autoHistory) edit.card.cooldown = 0;
sc.description = edit.toString();
edit.save();
}
} else if (reserved.test(data.name)) {
emoji = 'ā ļø';
messages.push('Reserved name');
} else {
const { card } = this.storyCards.get(data.name);
if (isNull(card)) {
messages.push('Preparing...');
} else {
emoji = 'ā ļø';
messages.push('Already exists');
queues.push({
name: data.name,
cmd: cmd.toUpperCase(),
emoji,
message: messages.join(' => ')
});
continue;
}
if (cache.generating === false) cache.generating = true;
cache.dataQueue.push(data);
}
queues.push({
name: data.name,
cmd: cmd.toUpperCase(),
emoji,
message: messages.join(' => ')
});
}
const combine = (() => {
const v = queues.values();
for (const q of v) {
if (isEqual(q, v.next().value)) {
return true;
}
}
return false;
})();
const dq = queues.at(0);
let TEXT = '';
if (combine && dq) {
const names = queues.map(({ name }) => `ā${name}ā`);
TEXT = `${dq.emoji} ${dq.cmd} ${names.join(' ')} => ${dq.message}`;
} else {
TEXT = queues
.map(({ emoji, cmd, name, message }) => `${emoji} ${cmd} ā${name}ā => ${message}`)
.join('\n');
}
if (!isBlank(TEXT)) {
this.text = TEXT;
this.message(TEXT);
}
return this;
}
context() {
const cache = this.cache;
if (isEmpty(cache) || cache.stop || cache.disabled) return this;
/* Change World Lore, Recent Story, Story Summary into World_Lore, Recent_Story, Story_Summary */
{
const rsReg = /(World Lore|Recent Story|Story Summary):\s?/g;
const $t = Words.split(this.text)
.filter((i) => !MagicCards.regExp.test(i))
.map((i) => {
return rsReg.test(i) ? i.split(' ').join('_') : i;
})
.join('\n');
if (!isBlank($t)) this.text = $t;
}
const wlReg = /World_Lore:\s*([\s\S]*?)$/i;
const rsReg = /Recent_Story:\s*([\s\S]*?)$/i;
const [, wl = ''] = wlReg.exec(this.text) || [];
const [, rs = ''] = rsReg.exec(this.text) || [];
const excludes = [];
for (const sc of this.magicCards()) {
const { card, save, toString } = this.storyCards.edit(sc, undefined);
const name = card.id.trim();
excludes.push(name);
const s = name.split(/ |_/);
const wlMention = s.some((i) => wl.includes(i));
const rsMention = s.some((i) => rs.includes(i));
if (rsMention && !wlMention) {
this.text = this.text.replace(wlReg, `World_Lore:\n${sc.entry}`);
} else if (card.autoHistory) {
const { list } = this.PList.from(sc.entry, sc.description);
const getCooldown = () => {
const v = isNum(list.defaultCooldown) && Number(list.defaultCooldown);
const def = card.defaultCooldown ?? cache.settings.cooldown;
return v && v !== def ? v : def;
};
if (!isBlank(card.summary)) {
const getLimit = () => {
const v = isNum(list.cardLimit) && Number(list.cardLimit);
const def = isEmpty(cache.data) ? 800 : cache.data.limit.card;
return v && v !== def ? v : def;
};
list.Events = Words.limit(card.summary.replace(/\.;/, ';'), Math.abs(2000 - getLimit()));
this.text = this.text.replaceAll(new RegExp(RegExp.escape(sc.entry), 'g'), `${list}`);
}
if (card.cooldown <= 0) {
/* only compress if loaded in context */
if (rsMention || wlMention) {
this.queue(
{
name,
entry: sc.entry,
extra: [list.Events],
type: 'Compress'
},
true
);
}
card.cooldown = getCooldown();
sc.description = toString();
save();
}
}
}
cache.data = isEmpty(cache.data) ? cache.dataQueue.shift() || {} : cache.data;
cache.generating = !(isEmpty(cache.dataQueue) && isEmpty(cache.data));
if (!isNull(this.debugCard)) {
this.debugCard.card.entry = JSON.stringify(cache.data, null, ' ');
this.debugCard.card.description = Words.limit(
cache.errors.join('\n').trim(),
MagicCards.constant.storyCard
);
}
if (cache.generating === false && this.turn > 0) {
const cd = this.actionCount % cache.settings.cooldown;
if (cd === 0) {
cache.generating = !(isEmpty(cache.dataQueue) && isEmpty(cache.data));
} else if (cd + 1 === cache.settings.cooldown && cache.settings.autoRetrieve) {
if (isEmpty(cache.data.name)) {
/* We do not specify a `name` */
this.queue(
{
entry: rmDup(excludes.filter((i) => !isEmpty(i))).join(','),
type: 'Retrieve'
},
true
);
}
this.message('Executing entry check on next turn...');
}
}
if (cache.generating) {
cache.turnsSpent += 1;
const data = cache.data;
const parts = {
$1: data.name,
$2: data.entry,
$3: `[${this.PList.data}`,
$00: 'event1',
$990: '',
$991: '',
$992: data.instruction.example
};
for (const n of data.extra.keys()) parts[`$0${n}`] = data.extra.at(0);
const wrapper = () => {
/** Debug: Reset in-case of instruction change/update */
const db = (isNull(this.debugCard) ? cache : Options.createDefault()).database.find(
(i) => i.type === data.type
);
if (db) data.instruction = Options.copy(db.instruction);
const instruction = () => {
if (isEmpty(data.loaded)) return data.instruction.ai;
return (data.instruction.ai = data.instruction.ai.replaceAll(
/Category:[^\n]+/g,
`Category:${this.PList.data.category}`
));
};
return prose('', instruction(), data.instruction.user)
.split('\\n')
.join('\n')
.replaceAll(/(\$\d+)/g, (_) => parts[_] ?? _);
};
const INT = wrapper();
this.text += INT;
}
return this;
}
output() {
const cache = this.cache;
if (Object.is(cache.stop, true)) this.stop = false;
if (cache.disabled) {
delete cache.disabled;
return this;
}
/**
* Must have valid `type`, `entry`, `keys`, or `title`
* @type {StoryCard[]}
*/
const cards = storyCards
.filter(({ id }) => isValid(id) && !(id.startsWith('{') && id.endsWith('}')))
.filter(({ type, entry, keys, title = keys, description }) => {
const { list } = this.PList.from(entry, description);
return (
isValid(type) && /character|location/i.test(type) && isValid(title) && !isBlank(list)
);
});
if (!isBlank(cards)) {
this.message(`Converted "${cards.length}" StoryCards...`);
for (const sc of cards) {
const defaultId = sc.title ?? sc.keys ?? '';
const keys = defaultId.split(',');
if (isBlank(keys)) continue;
const defaultCooldown = Math.abs(cache.settings.cooldown + storyCards.indexOf(sc) * 2);
const edit = this.storyCards.edit(sc, undefined, {
sync: false,
defaultId,
defaultCooldown,
id: keys[0].trim()
});
sc.description = `${edit.toString()}\n${sc.description}`.trim();
edit.save();
}
}
if (cache.generating) {
const progressBar = (upNext = cache.dataQueue.at(0)) => {
const data = cache.data;
upNext ??= data;
const num = data.progress;
if (Number.isNaN(num)) return '';
const current = Math.round(Math.round(num) / 10);
const loaded = 'ā'.repeat(current);
const remaining = 'ā'.repeat(Math.abs(10 - current));
const endof = `\n=> [ ${Words.getUniHex('1fa84')} CONTINUE ]`;
let TEXT = '';
TEXT += `\n- š“ ${num}%: ${loaded}${remaining}`;
if (data.type === 'Characters' || data.type === 'Locations') {
const catTotal = data.limit.category;
const catLoaded = data.category.filter((i) => i in data.loaded).length;
TEXT += `\n- š“ Categories: ${catLoaded}/${catTotal}`;
}
if (num === 100) {
TEXT += `\n- š“ Summarize in ${cache.settings.cooldown} turns`;
}
if (isEmpty(upNext)) {
TEXT += `${endof}\nContinue story from exact pre-interruption point.`;
} else if (
!isEmpty(upNext) &&
!isEmpty(upNext.name) &&
!Object.is(upNext.name, data.name)
) {
TEXT += `\n- š“ Up Next: ${upNext.type === 'Locations' ? 'šļø' : ''}${upNext.name}${endof}`;
} else {
TEXT += endof;
}
return TEXT;
};
if (isEmpty(cache.data.name)) {
const t = this.text.trim();
const messages = [];
if (/Retrieve/.test(cache.data.type)) {
const iterator = t.matchAll(/(Characters|Locations):\s?([^;\n\]]+)/g) || [];
for (const [, type, entries] of iterator) {
for (const n of entries.split(',')) {
const name = n.trim();
if (!/[A-Z]/.test(name)) continue;
if (/none|n\/a|unknown|not\s?found/i.test(name)) continue;
const scExists = Array.from(this.magicCards()).some((sc) => {
const { card } = this.storyCards.edit(sc, true);
return new RegExp(RegExp.escape(card.id.trim()), 'i').test(name);
});
if (scExists) continue;
const data = this.queue({ name, type });
if (isEmpty(data.name)) continue;
const r = new RegExp(RegExp.escape(data.name.split(/ |_/).join('|')), 'i');
if (cache.dataQueue.some((dq) => r.test(dq.name.trim()))) continue;
const { card } = this.storyCards.get(data.name);
if (isNull(card)) cache.dataQueue.push(data);
}
}
}
if (isBlank(cache.dataQueue)) {
messages.push('No new entries found');
} else {
const dataQueue = cache.dataQueue
.filter(({ type }) => type !== 'Retrieve')
.map(({ name, type }) => `ā${type === 'Locations' ? 'šļø' : ''}${name}ā`)
.join(' ');
if (!isBlank(dataQueue)) messages.push(`Added to queue: ${dataQueue}`);
}
if (!isBlank(messages)) {
this.text = messages.map((i) => this.print(i, 'š“', 'MagicCards')).join('');
}
cache.data = cache.dataQueue.shift() || {};
this.refresh();
} else {
const { complete, list, extra } = this.PList.data.get(
true,
this.history,
{ text: cache.data.output },
{ text: this.text }
);
if (complete) {
const sc = this.storyCards.create({
title: cache.data.name,
entry: list,
type: cache.data.type === 'Locations' ? MagicCards.constant.location : undefined
});
if (isNull(sc.card)) throw sc.error;
const edit = this.storyCards.edit(sc.card, undefined);
if (
/Compress/.test(cache.data.type) &&
!isEmpty(cache.data.loaded) &&
!isEmpty(cache.data.loaded.Events)
) {
edit.card.summary = cache.data.loaded.Events;
}
sc.card.cooldown = sc.card.defaultCooldown ?? cache.settings.cooldown;
sc.card.description = edit.toString();
edit.save();
const upNext = cache.dataQueue.shift() || {};
if (/Compress/.test(cache.data.type)) {
this.message(`Compressed ${cache.data.name}`);
} else {
this.message(`Created ${cache.data.name}`);
}
this.text = this.print(`Done! (${list.length}/1,000)${progressBar(upNext)}`);
cache.data = upNext;
this.refresh();
} else {
cache.data.output = list;
const t = /Compress/.test(cache.data.type)
? 'Compressing...'
: `Generating ${Words.toLowerCase(cache.data.type).replace(/s$/, '')}...`;
this.message(t);
this.text = this.print(`${t} ${extra}${progressBar()}`);
}
}
} else {
if (typeof history !== 'undefined')
history = history.filter(({ text }) => !MagicCards.regExp.test(text));
for (const sc of this.magicCards()) {
const edit = this.storyCards.edit(sc, undefined);
if (edit.card.autoHistory) {
edit.card.cooldown--;
if (edit.card.cooldown === 0) {
this.message(`Compressing ${sc.title} on the next turn...`);
}
}
sc.description = edit.toString();
edit.save();
}
}
return this;
}
//#endregion
*magicCards() {
const cards = storyCards.filter(
({ id }) => isValid(id) && id.startsWith('{') && id.endsWith('}') && !id.includes('"pin"')
);
for (const sc of cards) {
yield sc;
}
}
}
const mc = new MagicCards(OPTIONS);
globalThis.MagicCards = MagicCards;
globalThis.mc = mc;
//#endregion