-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathlibrary.js
More file actions
8783 lines (8590 loc) · 429 KB
/
library.js
File metadata and controls
8783 lines (8590 loc) · 429 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// 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;