Advanced Lorebook — by Icehellionx
Advanced Lorebook
— by Icehellionx
⚠️The Advanced Lorebook script is a derivative of the original Advanced Lorebook script that was used during testing. This script is kept as a legacy option in case someone is brave enough to fork it and create a new variant!
Icehellionx's v12 version of this script includes guardrails and extra features.
/* ============================================================================
ADVANCED LORE BOOK SYSTEM v12
Author: Icehellionx
//#region HEADER
==========================================================================
Inputs (read-only): context.chat.last_message (or lastMessage), context.chat.message_count
Outputs (write-only): context.character.personality, context.character.scenario
AUTHOR CHEAT-SHEET (ASCII-safe):
- keywords: real user words/phrases; supports suffix wildcard "welcom*" -> welcome/welcomed/welcoming.
- tag: internal label for this entry (e.g., "base_open"); never matched against text.
- triggers: list of tags to emit when this entry hits (e.g., ["base_open"]).
Text gates (any of these aliases are accepted):
- requireAny / andAny / requires: { any: [...] }
- requireAll / andAll / requires: { all: [...] }
- requireNone / notAny / block / requires: { none: [...] }
- notAll // reject only if *all* listed words are present simultaneously
Tag gates (cross-entry by fired tags):
- andAnyTags, andAllTags, notAnyTags, notAllTags
Time gates:
- minMessages / maxMessages
Name block:
- nameBlock: ["jamie"] // blocks if active bot name equals any listed (case-insensitive)
Priority and selection:
- priority: 1..5 (default 3; clamped)
- APPLY_LIMIT caps how many entries apply per turn (engine-level)
Probability:
- probability: 0..1 or "40%" (both supported)
Shifts:
- optional sub-entries with same fields as entries; evaluated after the parent entry hits
Multi-message window (engine behavior summary):
- Engine normalizes a joined window of recent messages (WINDOW_DEPTH) for keyword checks.
- Whole-word matching with optional suffix wildcard "stem*".
- Hyphen/underscore treated as spaces during normalization.
Output formatting:
- Engine prepends "\n\n" before each applied personality/scenario fragment.
========================================================================== */
/* ============================================================================
[SECTION] GLOBAL KNOBS
SAFE TO EDIT: Yes
========================================================================== */
//#region GLOBAL_KNOBS
var DEBUG = 0; // 1 -> emit [DBG] lines inline in personality
var APPLY_LIMIT = 6; // cap applied entries per turn; higher priorities win
/* ============================================================================
[SECTION] OUTPUT GUARDS
SAFE TO EDIT: Yes (keep behavior)
========================================================================== */
//#region OUTPUT_GUARDS
context.character = context.character || {};
context.character.personality = (typeof context.character.personality === "string")
? context.character.personality : "";
context.character.scenario = (typeof context.character.scenario === "string")
? context.character.scenario : "";
/* ============================================================================
[SECTION] INPUT NORMALIZATION
SAFE TO EDIT: Yes (tune WINDOW_DEPTH; keep normalization rules)
========================================================================== */
//#region INPUT_NORMALIZATION
// --- How many recent messages to scan together (tune as needed) -------------
var WINDOW_DEPTH = (function (n) {
n = parseInt(n, 10);
if (isNaN(n)) n = 5;
if (n < 1) n = 1;
if (n > 20) n = 20; // safety cap
return n;
})(typeof WINDOW_DEPTH === "number" ? WINDOW_DEPTH : 5);
// --- Utilities ---------------------------------------------------------------
function _str x) { return (x == null ? "" : String x) ; }
function _normalizeText(s) {
s = _str(s).toLowerCase();
s = s.replace(/[^a-z0-9_\s-]/g, " "); // keep letters/digits/underscore/hyphen/space
s = s.replace(/[-_]+/g, " "); // treat hyphen/underscore as spaces
s = s.replace(/\s+/g, " ").trim(); // collapse spaces
return s;
}
// --- Build multi-message window ---------------------------------------------
var _lmArr = (context && context.chat && context.chat.last_messages && typeof context.chat.last_messages.length === "number")
? context.chat.last_messages : null;
var _joinedWindow = "";
var _rawLastSingle = "";
if (_lmArr && _lmArr.length > 0) {
var startIdx = Math.max(0, _lmArr.length - WINDOW_DEPTH);
var segs = [];
for (var i = startIdx; i < _lmArr.length; i++) {
var item = _lmArr[i];
var msg = (item && typeof item.message === "string") ? item.message : _str(item);
segs.push(_str(msg));
}
_joinedWindow = segs.join(" ");
var lastItem = _lmArr[_lmArr.length - 1];
_rawLastSingle = _str((lastItem && typeof lastItem.message === "string") ? lastItem.message : lastItem);
} else {
var _lastMsgA = (context && context.chat && typeof context.chat.lastMessage === "string") ? context.chat.lastMessage : "";
var _lastMsgB = (context && context.chat && typeof context.chat.last_message === "string") ? context.chat.last_message : "";
_rawLastSingle = _str(_lastMsgA || _lastMsgB);
_joinedWindow = _rawLastSingle;
}
// --- Public struct + haystack ------------------------------------------------
var CHAT_WINDOW = {
depth: WINDOW_DEPTH,
count_available: (_lmArr && _lmArr.length) ? _lmArr.length : (_rawLastSingle ? 1 : 0),
text_joined: _joinedWindow,
text_last_only: _rawLastSingle,
text_joined_norm: _normalizeText(_joinedWindow),
text_last_only_norm: _normalizeText(_rawLastSingle)
};
var last = " " + CHAT_WINDOW.text_joined_norm + " ";
// --- Message count -----------------------------------------------------------
var messageCount = 0;
if (_lmArr && typeof _lmArr.length === "number") {
messageCount = _lmArr.length;
} else if (context && context.chat && typeof context.chat.message_count === "number") {
messageCount = context.chat.message_count;
} else if (typeof context_chat_message_count === "number") {
messageCount = context_chat_message_count;
}
// --- Active character name ---------------------------------------------------
var activeName = _normalizeText(
(context && context.character && typeof context.character.name === "string")
? context.character.name
: ""
);
/* ============================================================================
[SECTION] UTILITIES
SAFE TO EDIT: Yes
========================================================================== */
//#region UTILITIES
function dbg(msg) {
try {
if (typeof DEBUG !== "undefined" && DEBUG) {
context.character.personality += "\n\n[DBG] " + String(msg);
}
} catch (e) {}
}
function arr x) { return Array.isArray x) ? x : (x == null ? [] : [x]); }
function clamp01(v) { v = +v; if (!isFinite(v)) return 0; return Math.max(0, Math.min(1, v)); }
function parseProbability(v) {
if (v == null) return 1;
if (typeof v === "number") return clamp01(v);
var s = String(v).trim().toLowerCase();
var n = parseFloat(s.replace("%", ""));
if (!isFinite(n)) return 1;
return s.indexOf("%") !== -1 ? clamp01(n / 100) : clamp01(n);
}
function prio(e) {
var p = (e && isFinite(e.priority)) ? +e.priority : 3;
if (p < 1) p = 1;
if (p > 5) p = 5;
return p;
}
function getMin(e) { return (e && isFinite(e.minMessages)) ? +e.minMessages : -Infinity; }
function getMax(e) { return (e && isFinite(e.maxMessages)) ? +e.maxMessages : Infinity; }
function getKW(e) { return (e && Array.isArray(e.keywords)) ? e.keywords.slice(0) : []; }
function getTrg(e) { return (e && Array.isArray(e.triggers)) ? e.triggers.slice(0) : []; }
function getBlk(e) {
if (!e) return [];
if (Array.isArray(e.block)) return e.block.slice(0);
if (Array.isArray(e.Block)) return e.Block.slice(0);
return [];
}
function getNameBlock(e) { return (e && Array.isArray(e.nameBlock)) ? e.nameBlock.slice(0) : []; }
function normName(s) { return _normalizeText(s); }
function isNameBlocked(e) {
if (!activeName) return false;
var nb = getNameBlock(e);
for (var i = 0; i < nb.length; i++) {
var n = normName(nb[i]);
if (!n) continue;
if (n === activeName) return true;
if (activeName.indexOf(n) !== -1) return true;
if (n.indexOf(activeName + " ") === 0) return true;
}
return false;
}
function reEsc(s) { return String(s).replace(/[.*+?^${}()|[\]\]/g, "\$&"); }
function hasTerm(hay, term) {
var t = (term == null ? "" : String(term)).toLowerCase().trim();
if (!t) return false;
if (t.charAt(t.length - 1) === "*") {
var stem = reEsc(t.slice(0, -1));
var re1 = new RegExp("(?:^|\s)" + stem + "[a-z]*?(?=\s|$)");
return re1.test(hay);
}
var w = reEsc(t);
var re2 = new RegExp("(?:^|\s)" + w + "(?=\s|$)");
return re2.test(hay);
}
function collectWordGates(e) {
var r = (e && e.requires) ? e.requires : {};
var any = [].concat(arr(e && e.requireAny), arr(e && e.andAny), arr(r.any));
var all = [].concat(arr(e && e.requireAll), arr(e && e.andAll), arr(r.all));
var none = [].concat(arr(e && e.requireNone), arr(e && e.notAny), arr(r.none), arr(getBlk(e)));
var nall = [].concat(arr(e && e.notAll));
return { any: any, all: all, none: none, nall: nall };
}
function wordGatesPass(e) {
var g = collectWordGates(e);
if (g.any.length && !g.any.some(function (w) { return hasTerm(last, w); })) return false;
if (g.all.length && !g.all.every(function (w) { return hasTerm(last, w); })) return false;
if (g.none.length && g.none.some(function (w) { return hasTerm(last, w); })) return false;
if (g.nall.length && g.nall.every(function (w) { return hasTerm(last, w); })) return false;
return true;
}
function tagsPass(e, activeTagsSet) {
var anyT = arr(e && e.andAnyTags);
var allT = arr(e && e.andAllTags);
var noneT = arr(e && e.notAnyTags);
var nallT = arr(e && e.notAllTags);
var hasT = function (t) { return !!activeTagsSet && activeTagsSet[String(t)] === 1; };
if (anyT.length && !anyT.some(hasT)) return false;
if (allT.length && !allT.every(hasT)) return false;
if (noneT.length && noneT.some(hasT)) return false;
if (nallT.length && nallT.every(hasT)) return false;
return true;
}
function isAlwaysOn(e) {
var hasKW = !!(e && e.keywords && e.keywords.length);
var hasTag = !!(e && e.tag);
var hasMin = (e && e.minMessages != null);
var hasMax = (e && e.maxMessages != null);
return !hasKW && !hasTag && !hasMin && !hasMax;
}
function entryPasses(e, activeTagsSet) {
if (!(messageCount >= getMin(e) && messageCount <= getMax(e))) return false;
if (isNameBlocked(e)) return false;
if (!wordGatesPass(e)) return false;
if (!tagsPass(e, activeTagsSet || {})) return false;
if (Math.random() > parseProbability(e && e.probability)) return false;
return true;
}
/* ============================================================================
[SECTION] AUTHOR ENTRIES
SAFE TO EDIT: Yes
========================================================================== */
//#region AUTHOR_ENTRIES
var dynamicLore = [
// 🟢🟢🟢 SAFE TO EDIT BELOW THIS LINE 🟢🟢🟢
/* L0 — Always-on demo
What it does: Fires every turn because there are no keywords, no time gates, and no tag.
Why use: Bootstrap a baseline voice or a harmless always-on nudge.
*/
{ personality: " This entry will always fire." },
/* L1 — Basics: greeting keywords
New tools: simple keyword list.
Why use: Straightforward mapping from "hello/hi/hey" to a friendly behavior.
*/
{
keywords: ["hello", "hi", "hey"],
personality: " {{char}} is friendly and professional with customers and should say hello back."
},
/* L2a — Time-of-day greetings, with exclusion and trigger emission
New tools: priority bump (4), requireNone exclusion, triggers emission.
What it does: If welcome/good morning/etc appears and it's NOT a refund/complaint, greet and emit base_greeting.
Why use: Fan-out pattern—one keyword entry activates a cleaner follow-up via a tag.
*/
{
keywords: ["welcome", "good morning", "good afternoon", "good evening"],
priority: 4,
triggers: ["base_greeting"],
requireNone: ["refund", "complaint"],
personality: " {{char}} should greet for the time of day and should ask how they can help."
},
/* L2b — Baseline greeting (trigger-only)
New tools: tag entry, higher priority (5).
What it does: Fires only if 'base_greeting' tag is present.
Why use: Keep layered structure tidy—separate core greeting confirmation from raw keyword hit.
*/
{
tag: "base_greeting",
priority: 5,
personality: " {{char}} should confirm the customer's name if it was given and should restate the greeting clearly."
},
/* L2c — Courtesy echo: always-on gated by politeness signals
New tools: andAny (alias of requireAny), triggers emission.
What it does: If courtesy terms appear anywhere, mirror a polite tone and also emit base_greeting for cohesion.
Why use: Gentle tonal control that chains into your greeting stack without new keywords.
*/
{
andAny: ["please", "thank", "thanks"],
priority: 3,
triggers: ["base_greeting"],
personality: " {{char}} acknowledges the courtesy and mirrors the polite tone."
},
/* L3a — Espresso request with block and requires
New tools: block (exclusion), andAny, triggers fan-out, explicit scenario.
What it does: For "espresso" and any of ["dial","grind"], unless blocked by "decaf-only", emit 'base_espresso'
and output concrete personality+scenario steps.
Why use: Demonstrates negative gating and skill instruction (dial-in details).
*/
{
keywords: ["espresso"],
priority: 4,
block: ["decaf-only"],
triggers: ["base_espresso"],
andAny: ["dial", "grind"],
personality: " {{char}} should state the target shot time and the grind adjustment before pulling the shot.",
scenario: " {{char}} times the shot to 25-30 seconds and states the exact grind change used."
},
/* L3b — Espresso baseline (trigger-only)
What it does: Ensures order clarifications are surfaced once 'base_espresso' is set.
Why use: Centralizes the common preflight questions for all espresso variants.
*/
{
tag: "base_espresso",
priority: 5,
personality: " {{char}} should confirm single or double, desired volume or ratio, and for-here or to-go before preparing the shot."
},
/* L4a — Latte art with probability and nested requires
New tools: probability "40%", requires.any + requires.none, triggers.
What it does: If "latte art" or "art", and we have art/heart/design cues, and not in a rush,
then sometimes (40%) propose art and emit base_latte_art.
Why use: Teaches controlled randomness and queue-aware behavior.
*/
{
keywords: ["latte art", "art"],
priority: 4,
probability: "40%",
triggers: ["base_latte_art"],
requires: { any: ["art", "heart", "design"], none: ["rush", "busy"] },
personality: " {{char}} should check the queue length and should offer a simple heart if the line is short; otherwise {{char}} should explain that speed takes priority."
},
/* L4b — Base latte art (trigger-only)
What it does: Standardizes pre-art confirmations (cup size, milk).
Why use: Keeps your latte art flow consistent and centrally adjustable.
*/
{
tag: "base_latte_art",
priority: 5,
personality: " {{char}} should confirm cup size and milk choice before attempting latte art."
},
/* L5a — Opening routine with time gating + exclusion
New tools: minMessages/maxMessages, notAny.
What it does: Only in the first 3 messages (0..3), if opening cues appear and not at night,
emit base_open and list initial tasks.
Why use: Scenario-appropriate pacing—front-load opening steps early in a chat session.
*/
{
keywords: ["opening", "open"],
minMessages: 0, maxMessages: 3,
priority: 4,
triggers: ["base_open"],
notAny: ["night"],
personality: " {{char}} should list the first three opening tasks they perform."
},
/* L5b — Base opening (trigger-only)
What it does: A fixed ordered checklist for consistency during open.
Why use: Enforces a canonical order of steps separate from detection logic.
*/
{
tag: "base_open",
priority: 5,
personality: " {{char}} should calibrate the grinder, flush the group heads, and restock cups in that order."
},
/* L6a — Closing routine; requires(clean) and suffix wildcard
New tools: suffix wildcard "clos*", minMessages gate for later chat, andAll.
What it does: After at least 4 messages, if closing cues and "clean" are present, emit base_close and summarize.
Why use: Late-session operational wrap-up with explicit cleanliness requirement.
*/
{
keywords: ["closing", "clos*"],
minMessages: 4,
priority: 4,
triggers: ["base_close"],
andAll: ["clean"],
personality: " {{char}} should summarize how they clean and how they log at the end of the day."
},
/* L6b — Base closing (trigger-only)
What it does: Standard close checklist.
Why use: Codifies the close routine that other entries can build on.
*/
{
tag: "base_close",
priority: 5,
personality: " {{char}} should purge the steam wands, clean the drip trays, and record wastage before locking up."
},
/* L7a — Inventory with multiple triggers and nested requires
New tools: multiple triggers in one entry; requires.any + requires.none bundle.
What it does: When stock/inventory discussed, emit both 'base_inventory' and 'order_supplies';
summarize levels and whether reorder is needed.
Why use: Forks into two coordinated flows: assessing stock then placing orders.
*/
{
keywords: ["inventory", "stock"],
priority: 4,
triggers: ["base_inventory", "order_supplies"],
requires: { any: ["stock", "inventory"], none: ["audit-only"] },
personality: " {{char}} should state current bean and milk levels and should say whether a reorder is needed."
},
/* L7b — Base inventory (trigger-only)
What it does: Prompts a check and heuristic planning for tomorrow.
Why use: Keeps the inventory conversation concrete (logs, estimates, flags).
*/
{
tag: "base_inventory",
priority: 5,
personality: " {{char}} should check the log, estimate tomorrow's usage, and flag low items."
},
/* L7c — Order supplies (trigger-only)
What it does: Converts assessment into explicit quantities and an action (PO).
Why use: Ensures conversations end with a clear operational decision.
*/
{
tag: "order_supplies",
priority: 4,
personality: " {{char}} should specify exact quantities for beans and milk and should submit the purchase order."
},
/* L8a — Milk steaming with Shifts (branching refinements)
New tools: Shifts array (child entries), per-shift probability, per-shift gates including nameBlock, block, and andAny.
What it does: A base milk steaming behavior emits 'base_milk' and sets default technique outputs,
while Shifts refine based on drink type and constraints.
Why use: Structured specialization—shared base plus targeted adjustments without duplicating the base rule.
*/
{
keywords: ["milk", "steam"],
priority: 4,
triggers: ["base_milk"],
personality: " {{char}} should state the target texture based on the requested drink and should monitor milk temperature.",
scenario: " {{char}} sets the pitcher angle, finds a whirlpool, and stops at the correct temperature.",
Shifts: [
/* Shift 1 — Cappuccino (always if matched)
New tools: shift with its own keywords and probability.
Why use: Guarantees classic cappuccino foam profile when requested.
*/
{
keywords: ["cappuccino"],
probability: 1.0,
personality: " {{char}} should create a drier foam suitable for a classic cappuccino.",
scenario: " {{char}} keeps the foam airy and maintains a stable cap."
},
/* Shift 2 — Latte (subsampled, avoids rush/busy)
New tools: probability 0.7, notAny exclusion inside a shift.
Why use: Offers microfoam and art if pace allows; defers when busy.
*/
{
keywords: ["latte"],
probability: 0.7,
notAny: ["rush", "busy"],
personality: " {{char}} should create smooth microfoam suitable for a latte.",
scenario: " {{char}} aims for a glossy texture that allows simple latte art."
},
/* Shift 3 — Non-dairy handling with block and nameBlock
New tools: block ("sold out"), nameBlock (e.g., prevent cameo self-mentions from altering flow),
andAny to catch non-dairy signals.
Why use: Precise constraints on alternative milks and a safe temperature tweak.
*/
{
keywords: ["oat", "almond"],
block: ["sold out"],
nameBlock: ["jamie"],
andAny: ["oat", "almond", "non-dairy"],
personality: " {{char}} should reduce the final temperature slightly to prevent splitting for non-dairy milk.",
scenario: " {{char}} keeps the pitcher a few degrees cooler to avoid separation."
}
]
},
/* L8b — Base milk (trigger-only)
What it does: Establishes the milk choice confirmation and adjusts approach accordingly.
Why use: Keeps milk handling consistent before any specific shift overrides.
*/
{
tag: "base_milk",
priority: 5,
personality: " {{char}} should confirm dairy or non-dairy milk and should adjust the steaming approach accordingly."
},
/* L9a — Operations cameo with nameBlock and exclusion
New tools: nameBlock prevents self-referential loops if the character is "jamie".
What it does: If user mentions "jamie" or "manager" (but character named 'jamie' is blocked from acting on it),
and not off-duty, emit base_ops and assign roles.
Why use: Avoids awkward self-cameo; still supports talking about someone else named Jamie.
*/
{
keywords: ["jamie", "manager"],
nameBlock: ["jamie"],
priority: 4,
triggers: ["base_ops"],
notAny: ["off-duty"],
personality: " {{char}} should assign roles during peak hours and should confirm the plan."
},
/* L9b — Base ops (trigger-only)
What it does: Defines the stations and the handoff checkpoints.
Why use: Operational clarity during busy periods.
*/
{
tag: "base_ops",
priority: 5,
personality: " {{char}} should assign register, bar, and runner positions and should confirm handoff points."
},
/* L10a — Inspection flow with multi-triggers and requires(all)
New tools: multiple triggers and andAll; chains into a health sub-flow.
What it does: For inspection/health with labels present, emit base_inspection and health_check.
Why use: Parallel checklists: sanitation and cold-chain checks in one pass.
*/
{
keywords: ["inspection", "health"],
priority: 4,
triggers: ["base_inspection", "health_check"],
andAll: ["labels"],
personality: " {{char}} should confirm sanitizer strength and should verify that all milk jugs have current labels."
},
/* L10b — Base inspection (trigger-only)
What it does: Details the sanitizer and labeling checks.
Why use: Keeps inspectors’ expectations visible and precise.
*/
{
tag: "base_inspection",
priority: 5,
personality: " {{char}} should verify sanitizer ppm, check date labels, and should note any required corrections."
},
/* L10c — Health check (trigger-only) with its own requires bundle
New tools: requires.none + requires.any in one object.
What it does: If not told to skip, and temperature/fridge/thermometer is in scope,
record fridge temps and list corrective actions.
Why use: Encodes a simple HACCP-style gate without cluttering the parent entry.
*/
{
tag: "health_check",
priority: 4,
requires: { none: ["skip"], any: ["temperature", "fridge", "thermometer"] },
personality: " {{char}} should record fridge temperatures and should list any corrective actions completed."
}
// 🛑🛑🛑 DO NOT EDIT BELOW THIS LINE 🛑🛑🛑
];
/* ============================================================================
[SECTION] COMPILATION
DO NOT EDIT: Behavior-sensitive
========================================================================== */
//#region COMPILATION
function compileAuthorLore(authorLore) {
var src = Array.isArray(authorLore) ? authorLore : [];
var out = new Array(src.length);
for (var i = 0; i < src.length; i++) out[i] = normalizeEntry(src[i]);
return out;
}
function normalizeEntry(e) {
if (!e) return {};
var out = {};
for (var k in e) if (Object.prototype.hasOwnProperty.call(e, k)) out[k] = e[k];
out.keywords = Array.isArray(e.keywords) ? e.keywords.slice(0) : [];
if (Array.isArray(e.Shifts) && e.Shifts.length) {
var shArr = new Array(e.Shifts.length);
for (var i = 0; i < e.Shifts.length; i++) {
var sh = e.Shifts[i] || {};
var shOut = {};
for (var sk in sh) if (Object.prototype.hasOwnProperty.call(sh, sk)) shOut[sk] = sh[sk];
shOut.keywords = Array.isArray(sh.keywords) ? sh.keywords.slice(0) : [];
shArr[i] = shOut;
}
out.Shifts = shArr;
} else if (out.hasOwnProperty("Shifts")) {
delete out.Shifts;
}
return out;
}
var _ENGINE_LORE = compileAuthorLore(typeof dynamicLore !== "undefined" ? dynamicLore : []);
/* ============================================================================
[SECTION] SELECTION PIPELINE
DO NOT EDIT: Behavior-sensitive
========================================================================== */
//#region SELECTION_PIPELINE
// --- State -------------------------------------------------------------------
var buckets = [null, [], [], [], [], []];
var picked = new Array(_ENGINE_LORE.length);
for (var __i = 0; __i < picked.length; __i++) picked[__i] = 0;
function makeTagSet() { return Object.create(null); }
var trigSet = makeTagSet();
var postShiftTrigSet = makeTagSet();
function addTag(set, key) { set[String(key)] = 1; }
function hasTag(set, key) { return set[String(key)] === 1; }
// --- 1) Direct pass ----------------------------------------------------------
for (var i1 = 0; i1 < _ENGINE_LORE.length; i1++) {
var e1 = _ENGINE_LORE[i1];
var hit = isAlwaysOn(e1) || getKW(e1).some(function (kw) { return hasTerm(last, kw); });
if (!hit) continue;
if (!entryPasses(e1, undefined)) { dbg("filtered entry[" + i1 + "]"); continue; }
buckets[prio(e1)].push(i1);
picked[i1] = 1;
var trg1 = getTrg(e1);
for (var t1 = 0; t1 < trg1.length; t1++) addTag(trigSet, trg1[t1]);
dbg("hit entry[" + i1 + "] p=" + prio(e1));
}
// --- 2) Trigger pass ---------------------------------------------------------
for (var i2 = 0; i2 < _ENGINE_LORE.length; i2++) {
if (picked[i2]) continue;
var e2 = _ENGINE_LORE[i2];
if (!(e2 && e2.tag && hasTag(trigSet, e2.tag))) continue;
if (!entryPasses(e2, trigSet)) { dbg("filtered triggered entry[" + i2 + "]"); continue; }
buckets[prio(e2)].push(i2);
picked[i2] = 1;
var trg2 = getTrg(e2);
for (var t2 = 0; t2 < trg2.length; t2++) addTag(trigSet, trg2[t2]);
dbg("triggered entry[" + i2 + "] p=" + prio(e2));
}
// --- 3) Priority selection (capped) -----------------------------------------
var selected = [];
var pickedCount = 0;
var __APPLY_LIMIT = (typeof APPLY_LIMIT === "number" && APPLY_LIMIT >= 1) ? APPLY_LIMIT : 99999;
for (var p = 5; p >= 1 && pickedCount < __APPLY_LIMIT; p--) {
var bucket = buckets[p];
for (var bi = 0; bi < bucket.length && pickedCount < __APPLY_LIMIT; bi++) {
selected.push(bucket[bi]);
pickedCount++;
}
}
if (pickedCount === __APPLY_LIMIT) dbg("APPLY_LIMIT reached");
/* ============================================================================
[SECTION] APPLY + SHIFTS + POST-SHIFT
DO NOT EDIT: Behavior-sensitive
========================================================================== */
//#region APPLY_AND_SHIFTS
var bufP = "";
var bufS = "";
for (var si = 0; si < selected.length; si++) {
var idx = selected[si];
var e3 = _ENGINE_LORE[idx];
if (e3 && e3.personality) bufP += "\n\n" + e3.personality;
if (e3 && e3.scenario) bufS += "\n\n" + e3.scenario;
if (!(e3 && Array.isArray(e3.Shifts) && e3.Shifts.length)) continue;
for (var shI = 0; shI < e3.Shifts.length; shI++) {
var sh = e3.Shifts[shI];
var activated = isAlwaysOn(sh) || getKW(sh).some(function (kw) { return hasTerm(last, kw); });
if (!activated) continue;
var trgSh = getTrg(sh);
for (var tt = 0; tt < trgSh.length; tt++) addTag(postShiftTrigSet, trgSh[tt]);
if (!entryPasses(sh, trigSet)) { dbg("shift filtered"); continue; }
if (sh.personality) bufP += "\n\n" + sh.personality;
if (sh.scenario) bufS += "\n\n" + sh.scenario;
}
}
// --- Post-shift triggers -----------------------------------------------------
var unionTags = (function () {
var dst = makeTagSet(), k;
for (k in trigSet) if (trigSet[k] === 1) dst[k] = 1;
for (k in postShiftTrigSet) if (postShiftTrigSet[k] === 1) dst[k] = 1;
return dst;
})();
for (var i3 = 0; i3 < _ENGINE_LORE.length; i3++) {
if (picked[i3]) continue;
var e4 = _ENGINE_LORE[i3];
if (!(e4 && e4.tag && hasTag(postShiftTrigSet, e4.tag))) continue;
if (!entryPasses(e4, unionTags)) { dbg("post-filter entry[" + i3 + "]"); continue; }
if (e4.personality) bufP += "\n\n" + e4.personality;
if (e4.scenario) bufS += "\n\n" + e4.scenario;
dbg("post-shift triggered entry[" + i3 + "] p=" + prio(e4));
}
/* ============================================================================
[SECTION] FLUSH
DO NOT EDIT: Behavior-sensitive
========================================================================== */
//#region FLUSH
if (bufP) context.character.personality += bufP;
if (bufS) context.character.scenario += bufS;
Updated on: 07/10/2025
Thank you!