@dsh-agentic-roleplay/state 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/index.d.ts +281 -15
- package/lib/index.js +970 -69
- package/lib/index.js.map +1 -1
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -141,6 +141,15 @@ const MEMORY_KINDS = [
|
|
|
141
141
|
"relationship",
|
|
142
142
|
"knowledge"
|
|
143
143
|
];
|
|
144
|
+
/** How sure a character is of what it believes, from least to most. */
|
|
145
|
+
const MEMORY_CONFIDENCES = [
|
|
146
|
+
"doubtful",
|
|
147
|
+
"likely",
|
|
148
|
+
"sure"
|
|
149
|
+
];
|
|
150
|
+
const DEFAULT_CONFIDENCE = "likely";
|
|
151
|
+
/** Whether a perception speaks for a belief or against it. */
|
|
152
|
+
const EVIDENCE_RELATIONS = ["supports", "contradicts"];
|
|
144
153
|
const THREAD_STATUSES = ["open", "resolved"];
|
|
145
154
|
/** A digest covers a stretch of recent turns closely; a chapter covers older turns briefly. */
|
|
146
155
|
const SUMMARY_LEVELS = ["digest", "chapter"];
|
|
@@ -346,6 +355,8 @@ const COMPARISONS = [
|
|
|
346
355
|
"gt",
|
|
347
356
|
"ge"
|
|
348
357
|
];
|
|
358
|
+
/** In a cue, the key of the character it is said to: in a `while` condition and in the words of the cue. */
|
|
359
|
+
const SELF = "@self";
|
|
349
360
|
const TRIGGER_STATUSES = [
|
|
350
361
|
"dormant",
|
|
351
362
|
"fired",
|
|
@@ -401,6 +412,213 @@ const PLOT_NUDGE_MODES = [
|
|
|
401
412
|
"breathing_room"
|
|
402
413
|
];
|
|
403
414
|
//#endregion
|
|
415
|
+
//#region ../dsh-roleplay-model/src/messages-zh.ts
|
|
416
|
+
/** The Chinese of the texts built in code, by the English that stands in the code. A `{name}` is filled in and must stand in both. */
|
|
417
|
+
const ZH = {
|
|
418
|
+
", ": ",",
|
|
419
|
+
"; ": ";",
|
|
420
|
+
": ": ":",
|
|
421
|
+
".": "。",
|
|
422
|
+
" and ": "和",
|
|
423
|
+
"doubtful": "存疑",
|
|
424
|
+
"likely": "较有把握",
|
|
425
|
+
"sure": "确信",
|
|
426
|
+
"open": "畅通",
|
|
427
|
+
"locked": "已上锁",
|
|
428
|
+
"blocked": "被堵住",
|
|
429
|
+
"night": "夜里",
|
|
430
|
+
"morning": "上午",
|
|
431
|
+
"afternoon": "下午",
|
|
432
|
+
"evening": "傍晚",
|
|
433
|
+
"spring": "春季",
|
|
434
|
+
"summer": "夏季",
|
|
435
|
+
"autumn": "秋季",
|
|
436
|
+
"winter": "冬季",
|
|
437
|
+
"today is {holidays}": "今天是{holidays}",
|
|
438
|
+
"Summary": "概述",
|
|
439
|
+
"Personality": "性格",
|
|
440
|
+
"Background": "背景",
|
|
441
|
+
"Appearance": "外貌",
|
|
442
|
+
"How you speak": "说话方式",
|
|
443
|
+
"You are {name}.": "你是{name}。",
|
|
444
|
+
"You are also called {names}.": "你也被称作{names}。",
|
|
445
|
+
"age: {value}": "年龄:{value}",
|
|
446
|
+
"gender: {value}": "性别:{value}",
|
|
447
|
+
"pronouns: {value}": "代词:{value}",
|
|
448
|
+
"Situation:": "当前情形:",
|
|
449
|
+
"Now: {time}.": "现在是:{time}。",
|
|
450
|
+
"Where you are:": "你所在之处:",
|
|
451
|
+
"Present:": "在场的人:",
|
|
452
|
+
" (you)": "(你)",
|
|
453
|
+
"What you know is coming:": "你知道即将发生的事:",
|
|
454
|
+
"Just now you notice:": "此刻你注意到:",
|
|
455
|
+
"How you are (what you can tell of yourself):": "你的状态(你自己能感觉到的):",
|
|
456
|
+
"What you can tell of the others here:": "你能看出在场其他人的情况:",
|
|
457
|
+
"What you have on you:": "你身上带着:",
|
|
458
|
+
"Things here: ": "这里有:",
|
|
459
|
+
"Ways out: ": "出路:",
|
|
460
|
+
"Lying about: ": "散落在周围的:",
|
|
461
|
+
"hidden": "隐蔽",
|
|
462
|
+
" holding {things}": ",里面有{things}",
|
|
463
|
+
"; and {count} more": ";另有{count}项",
|
|
464
|
+
"at {feature}": "在{feature}",
|
|
465
|
+
"; and {count} more you can recall if needed": ";另有{count}项,需要时你可以回想起来",
|
|
466
|
+
"What you know exists (this does not tell you where it is now or what state it is in):": "你知道存在的事物(这并不说明它现在在哪里、处于什么状态):",
|
|
467
|
+
"People: ": "人物:",
|
|
468
|
+
"Factions: ": "组织:",
|
|
469
|
+
"Places: ": "地点:",
|
|
470
|
+
"Routes: ": "路径:",
|
|
471
|
+
"Things: ": "物品:",
|
|
472
|
+
"Your day and what you are after (plans, not orders: keep them, put them off or drop them as the moment and your character suggest):": "你的日程与你想达成的事(只是计划,不是命令:依当下情形和你的性格,可以照做、推迟或放弃):",
|
|
473
|
+
"Your day, now: {activity}{place}, until {until}{note}.": "你此刻的日程:{activity}{place},持续到{until}{note}。",
|
|
474
|
+
"Your day, next: {activity}{place}, from {from}.": "你接下来的日程:{activity}{place},{from}开始。",
|
|
475
|
+
" at {place}": ",在{place}",
|
|
476
|
+
" (not something you would drop lightly)": "(你不会轻易放下)",
|
|
477
|
+
", with {names}": ",与{names}一起",
|
|
478
|
+
"You were to: {title}{place}{with}, at {at}, which has passed and is still open.": "你本应:{title}{place}{with},时间是{at},已经过了,事情仍未了结。",
|
|
479
|
+
"You are due to: {title}{place}{with}, at {at}.": "你约定要:{title}{place}{with},时间是{at}。",
|
|
480
|
+
"What you are after:": "你想达成的事:",
|
|
481
|
+
", by {deadline}, which has passed": ",限期{deadline},已过",
|
|
482
|
+
", by {deadline}": ",限期{deadline}",
|
|
483
|
+
" Next: {plan}.": " 下一步:{plan}。",
|
|
484
|
+
"How you stand with people who bear on this moment (your own view, not objective fact):": "你与和此刻相关的人的关系(是你自己的看法,不是客观事实):",
|
|
485
|
+
" (here)": "(在场)",
|
|
486
|
+
" (not here)": "(不在场)",
|
|
487
|
+
"knows them well": "很了解对方",
|
|
488
|
+
"barely knows them": "几乎不了解对方",
|
|
489
|
+
"trusts them": "信任对方",
|
|
490
|
+
"distrusts them": "不信任对方",
|
|
491
|
+
"is fond of them": "喜欢对方",
|
|
492
|
+
"dislikes them": "不喜欢对方",
|
|
493
|
+
"things are strained between them": "彼此关系紧张",
|
|
494
|
+
"Your affiliations (their goals are pressures and commitments, not commands):": "你的所属组织(其目标是压力和承诺,不是命令):",
|
|
495
|
+
"member": "成员",
|
|
496
|
+
" Goals: {goals}.": " 目标:{goals}。",
|
|
497
|
+
"- {name}: {position}; {status}; commitment {commitment}.": "- {name}:{position};{status};投入程度 {commitment}。",
|
|
498
|
+
" Your view: {attitude}.": " 你的看法:{attitude}。",
|
|
499
|
+
"What you remember that bears on this (only what you took in yourself; do not claim to remember more, and be unsure of what is fuzzy):": "你记得的、与此相关的事(只限你自己亲身经历的;不要声称记得更多,模糊的地方要保持不确定):",
|
|
500
|
+
"What you remember that bears on this: nothing comes to mind.": "你记得的、与此相关的事:想不起任何事。",
|
|
501
|
+
"some time ago": "一段时间以前",
|
|
502
|
+
"just now": "刚才",
|
|
503
|
+
"1 day ago": "1天前",
|
|
504
|
+
"{amount} days ago": "{amount}天前",
|
|
505
|
+
"1 hour ago": "1小时前",
|
|
506
|
+
"{amount} hours ago": "{amount}小时前",
|
|
507
|
+
"1 minute ago": "1分钟前",
|
|
508
|
+
"{amount} minutes ago": "{amount}分钟前",
|
|
509
|
+
"the details are fuzzy": "细节已经模糊",
|
|
510
|
+
"you barely remember": "你几乎记不清了",
|
|
511
|
+
"you are sure of it": "你对此确信无疑",
|
|
512
|
+
"you doubt it": "你对此存疑",
|
|
513
|
+
"something you have seen since speaks against it": "此后你所见所闻有与之相悖的地方",
|
|
514
|
+
"it comes to mind with the rest": "它和其余的事一起浮现在脑海",
|
|
515
|
+
"Earlier in this scene, before this moment:": "这一幕此刻之前发生的事:",
|
|
516
|
+
"What just happened, and what you are reacting to:": "刚才发生的事,以及你在回应的是什么:",
|
|
517
|
+
"- {name} intended: {intent} ({result}) {summary}": "- {name}打算:{intent}({result}){summary}",
|
|
518
|
+
"- {name} said \"{utterance}\" and intended: {intent} ({result}) {summary}": "- {name}说了“{utterance}”,并打算:{intent}({result}){summary}",
|
|
519
|
+
"Your earlier intent did not work out: {intent}\nYou are trying again, knowing the above. Decide again what you do now.": "你先前的打算没有成功:{intent}\n你带着上面这些再试一次。重新决定你现在做什么。",
|
|
520
|
+
"Character: {name}{summary}": "角色:{name}{summary}",
|
|
521
|
+
"What they believe now:": "他们现在的看法:",
|
|
522
|
+
"(nothing yet)": "(暂时没有)",
|
|
523
|
+
"What they have taken in since:": "他们此后经历的事:",
|
|
524
|
+
"Sort their memory.": "请整理他们的记忆。",
|
|
525
|
+
"not acceptable: {problems}": "不可接受:{problems}",
|
|
526
|
+
"{kind}, importance {importance}, {confidence}{about}{dispute}": "{kind},重要性 {importance},{confidence}{about}{dispute}",
|
|
527
|
+
", about {names}": ",关于{names}",
|
|
528
|
+
", something taken in since speaks against it": ",此后经历的事与之相悖",
|
|
529
|
+
"add: at most {max} new beliefs in one pass; merge or choose": "add:一次最多 {max} 条新看法;请合并或取舍",
|
|
530
|
+
"at most {max} changes in one pass": "一次最多 {max} 处修改",
|
|
531
|
+
"{where}: there is no thing it took in numbered {id}": "{where}:没有编号为 {id} 的经历",
|
|
532
|
+
"{where}: confidence is one of {options}": "{where}:confidence 须为 {options} 之一",
|
|
533
|
+
"{where}: unknown kind \"{kind}\"": "{where}:未知的 kind “{kind}”",
|
|
534
|
+
"{where}: say what it believes": "{where}:请写出它相信什么",
|
|
535
|
+
"{where}: at most {max} characters; say it shorter": "{where}:最多 {max} 个字符;请写得更短",
|
|
536
|
+
"{where}: importance runs from {min} to {max}": "{where}:importance 取值为 {min} 到 {max}",
|
|
537
|
+
"{where}: there is no belief numbered {id}": "{where}:没有编号为 {id} 的看法",
|
|
538
|
+
"{where}: one passing moment is not yet a belief; rest it on things from at least two different turns, or on one event that turned the story": "{where}:一个转瞬即逝的瞬间还不是看法;请以至少两个不同回合的经历为依据,或以一件扭转了故事的事件为依据",
|
|
539
|
+
"{where}: to be sure of something it must rest on things from at least two different turns": "{where}:要对某事确信,必须以至少两个不同回合的经历为依据",
|
|
540
|
+
"{where}: \"{name}\" is not a character; name them as they are named": "{where}:“{name}”不是角色;请使用他们的名字",
|
|
541
|
+
"{where}: belief {id} is already changed in this pass": "{where}:看法 {id} 在这一轮中已经修改过",
|
|
542
|
+
"{where}: say what it took in since that bears on it": "{where}:请写出此后与之相关的经历",
|
|
543
|
+
"{where}: thing {id} came before the belief was formed; cite what came after": "{where}:经历 {id} 发生在看法形成之前;请引用之后发生的事",
|
|
544
|
+
"{where}: say what changes": "{where}:请写出有何改变",
|
|
545
|
+
"{where}: the text must be there and at most {max} characters": "{where}:必须有文字,且最多 {max} 个字符",
|
|
546
|
+
"{where}: a belief that something contradicts must be doubted: lower its confidence": "{where}:被事实相悖的看法必须存疑:请降低其 confidence",
|
|
547
|
+
"{where}: to doubt a belief, name what contradicts it in contradicted_by": "{where}:要对看法存疑,请在 contradicted_by 中写明与之相悖的经历",
|
|
548
|
+
"{where}: merge into a belief that stays": "{where}:请合并到保留的看法中",
|
|
549
|
+
"Time: {time}.": "时间:{time}。",
|
|
550
|
+
"The story's time moves only as you say: each round takes what you give it (elapsed_seconds in resolve_round, or the default), and advance_time skips ahead.": "故事的时间只按你说的推进:每一轮耗时由你给定(resolve_round 的 elapsed_seconds,或取默认值),advance_time 可以跳过一段时间。",
|
|
551
|
+
"The player was away for {duration} of real time and the story went on meanwhile: characters have been about their own business, so do not assume anyone is where or as the player left them unless the records say so.": "玩家离开了{duration}的真实时间,其间故事仍在继续:角色们各忙各的事,除非记录另有说明,不要假定任何人还在玩家离开时的位置或状态。",
|
|
552
|
+
"The story's time follows the real clock: it cannot be skipped, and each round takes as long as it really took.": "故事时间跟随真实时钟:无法跳过,每一轮耗时就是它实际花费的时间。",
|
|
553
|
+
"By their routines since you last looked: {moves}.": "按各自的日程,自你上次查看以来:{moves}。",
|
|
554
|
+
"{name} went to {place} ({activity})": "{name}去了{place}({activity})",
|
|
555
|
+
"With the player's character, and their days: {days}. The characters decide for themselves whether to keep to their day; the moment may keep them.": "与玩家角色在一起的人及其日程:{days}。是否照日程行事由角色自己决定;当下的情形也可能拖住他们。",
|
|
556
|
+
", at {place}": ",在{place}",
|
|
557
|
+
"\"{title}\" ({with}{place}) was due {when} and is still open": "“{title}”({with}{place})原定{when},仍未了结",
|
|
558
|
+
"\"{title}\" ({with}{place}) is due {when}": "“{title}”({with}{place})定于{when}",
|
|
559
|
+
"Commitments due soon or overdue: {entries}.": "即将到期或已逾期的约定:{entries}。",
|
|
560
|
+
"named in the move": "被这一步行动点到",
|
|
561
|
+
"in the scene": "在场",
|
|
562
|
+
"own goal: {goal}": "自身目标:{goal}",
|
|
563
|
+
"faction pressure: {goal}": "组织压力:{goal}",
|
|
564
|
+
"high tension toward the player": "对玩家高度紧张",
|
|
565
|
+
"distrusts the player": "不信任玩家",
|
|
566
|
+
"cares about the player": "在意玩家",
|
|
567
|
+
"dislikes the player": "不喜欢玩家",
|
|
568
|
+
"Likely characters to activate, in priority order (advice only; activate only whoever the moment actually reaches):": "可能需要启动的角色,按优先级排列(仅供参考;只启动当下真正牵涉到的人):",
|
|
569
|
+
"socially implicated": "社交上被牵涉",
|
|
570
|
+
"The player's character is in {place}": "玩家角色在{place}",
|
|
571
|
+
"Characters here:": "在这里的角色:",
|
|
572
|
+
"Characters elsewhere (they perceive nothing here unless the moment reaches them):": "在别处的角色(除非当下波及他们,否则感知不到这里的事):",
|
|
573
|
+
"{member} - in {place} ({key})": "{member} - 在{place}({key})",
|
|
574
|
+
"Characters with no place (not part of this scene; they cannot be asked to act):": "没有所在地点的角色(不属于这一幕;不能让他们行动):",
|
|
575
|
+
"the player's character, \"{key}\"": "玩家角色,“{key}”",
|
|
576
|
+
"acts through \"{key}\"": "通过“{key}”行动",
|
|
577
|
+
"Currently: {state}": "目前:{state}",
|
|
578
|
+
"Scene before turn {turn}:": "第 {turn} 回合之前的场景:",
|
|
579
|
+
"What characters have on them:": "角色们身上带着:",
|
|
580
|
+
"The world acted since the last turn, by itself:": "自上一回合以来,世界自行发生的事:",
|
|
581
|
+
"Known beforehand, still to come (who knows of it; it will happen by itself when its time comes):": "事先已知、尚未发生的事(谁知道;时候到了会自行发生):",
|
|
582
|
+
"[known to {who}]": "[知情者:{who}]",
|
|
583
|
+
"Weave these into the story when it fits, then say you did with addressed in narrate (each stays for a few turns and is then let go):": "合适时把这些织入故事,然后在 narrate 的 addressed 中说明已处理(每条会保留几个回合,之后放弃):",
|
|
584
|
+
"What the story keeps track of (change it with variable_changes in an outcome when what happened calls for it):": "故事追踪的内容(发生的事需要时,在结果中用 variable_changes 修改):",
|
|
585
|
+
"Still open in the story:": "故事中仍未了结的事:",
|
|
586
|
+
"Earlier events that bear on what the player just did (from the records of the story):": "与玩家刚才的行动相关的早先事件(来自故事记录):",
|
|
587
|
+
"turn {turn}": "第 {turn} 回合",
|
|
588
|
+
"You were to {title}, at {when}": "你本应{title},时间是{when}",
|
|
589
|
+
"You were to {title} at {place}, at {when}": "你本应在{place}:{title},时间是{when}",
|
|
590
|
+
" ({note})": "({note})",
|
|
591
|
+
"It is {now} now and the time has passed.": "现在是{now},时间已经过了。",
|
|
592
|
+
"It is {now} now.": "现在是{now}。",
|
|
593
|
+
"It was with: {who}.": "同你一起的有:{who}。",
|
|
594
|
+
"You are not with the player's character, and nothing of this happens in front of anyone: decide what you do about it.": "你不和玩家的角色在一起,这件事也不会发生在任何人面前:决定你要怎么处理。",
|
|
595
|
+
"The time you had for \"{goal}\" has passed ({when}; it is {now} now)": "你为“{goal}”预留的时间已过({when};现在是{now})",
|
|
596
|
+
"You are not with the player's character and nothing of this happens in front of anyone: decide what you do about it.": "你不和玩家的角色在一起,这件事也不会发生在任何人面前:决定你要怎么处理。",
|
|
597
|
+
"{names} noticed: {text}": "{names}注意到:{text}",
|
|
598
|
+
"{name} went to {place}.": "{name}去了{place}。"
|
|
599
|
+
};
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region ../dsh-roleplay-model/src/messages.ts
|
|
602
|
+
/** The languages the texts a model reads are written in. English is the source: a text is its own key. */
|
|
603
|
+
const LANGUAGES = ["en", "zh"];
|
|
604
|
+
/** The language a setting such as `zh-CN` or `en` stands for; English when it is none that is known. */
|
|
605
|
+
function languageOf(code) {
|
|
606
|
+
return (code ?? "").trim().toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
607
|
+
}
|
|
608
|
+
const fill = (template, values) => values === void 0 ? template : template.replace(/\{(\w+)\}/g, (whole, name) => name in values ? String(values[name]) : whole);
|
|
609
|
+
const CATALOGUES = { zh: ZH };
|
|
610
|
+
/**
|
|
611
|
+
* The texts the engine builds for a model (briefs, reports, refusals) in a language. The text written in the code is the English one and the key of
|
|
612
|
+
* the others; a text a language lacks is told in English, and a test keeps that from happening.
|
|
613
|
+
*/
|
|
614
|
+
function translator(language) {
|
|
615
|
+
const catalogue = language === "en" ? void 0 : CATALOGUES[language];
|
|
616
|
+
return (text, values) => fill(catalogue?.[text] ?? text, values);
|
|
617
|
+
}
|
|
618
|
+
/** The texts with a translation, for the test that every text has one. */
|
|
619
|
+
const translated = (language) => Object.keys(CATALOGUES[language]);
|
|
620
|
+
const translation = (language, text) => CATALOGUES[language][text];
|
|
621
|
+
//#endregion
|
|
404
622
|
//#region src/clock.ts
|
|
405
623
|
const systemClock = () => /* @__PURE__ */ new Date();
|
|
406
624
|
//#endregion
|
|
@@ -560,6 +778,7 @@ const toWindow = (row) => ({
|
|
|
560
778
|
id: row.id,
|
|
561
779
|
characterId: row.character_id,
|
|
562
780
|
days: parseNumberList(row.days, "routine_windows.days"),
|
|
781
|
+
on: parseStringList(row.filters, "routine_windows.filters"),
|
|
563
782
|
start: row.start_minute,
|
|
564
783
|
end: row.end_minute,
|
|
565
784
|
activity: row.activity,
|
|
@@ -593,10 +812,11 @@ var SqliteRoutineRepository = class {
|
|
|
593
812
|
if (this.db.get("SELECT 1 FROM characters WHERE id = ?", [characterId]) === void 0) throw new NotFoundError(`character ${characterId} does not exist`);
|
|
594
813
|
this.db.run("DELETE FROM routine_windows WHERE character_id = ?", [characterId]);
|
|
595
814
|
windows.forEach((window, index) => {
|
|
596
|
-
this.db.run("INSERT INTO routine_windows (character_id, ordinal, days, start_minute, end_minute, activity, location_id, feature_key, position, interruptible) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
815
|
+
this.db.run("INSERT INTO routine_windows (character_id, ordinal, days, filters, start_minute, end_minute, activity, location_id, feature_key, position, interruptible) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
597
816
|
characterId,
|
|
598
817
|
index,
|
|
599
818
|
toJson([...new Set(window.days)].sort()),
|
|
819
|
+
toJson([...new Set(window.on ?? [])]),
|
|
600
820
|
window.start,
|
|
601
821
|
window.end,
|
|
602
822
|
window.activity.trim(),
|
|
@@ -705,6 +925,226 @@ var SqliteEngagementRepository = class {
|
|
|
705
925
|
}
|
|
706
926
|
};
|
|
707
927
|
//#endregion
|
|
928
|
+
//#region src/records/calendar.ts
|
|
929
|
+
/**
|
|
930
|
+
* Which days a thing applies on, beyond the days of the week: whole months, a date every year or once, a stretch of dates, the day of the month,
|
|
931
|
+
* the first or last Monday of a month, a season, and the holidays a world names. A day is told by tokens (`dec`, `12-24`, `1912-09-21..1912-09-22`,
|
|
932
|
+
* `dom:1`, `nth:mon:1`, `season:winter`, `holiday:founders-day`, `fri`); a token with `!` before it is a day it does not apply on. A list holds on a
|
|
933
|
+
* day when it names no day to apply on or one of them is today, and none of the days to leave out is.
|
|
934
|
+
*/
|
|
935
|
+
const MONTH_NAMES = [
|
|
936
|
+
"jan",
|
|
937
|
+
"feb",
|
|
938
|
+
"mar",
|
|
939
|
+
"apr",
|
|
940
|
+
"may",
|
|
941
|
+
"jun",
|
|
942
|
+
"jul",
|
|
943
|
+
"aug",
|
|
944
|
+
"sep",
|
|
945
|
+
"oct",
|
|
946
|
+
"nov",
|
|
947
|
+
"dec"
|
|
948
|
+
];
|
|
949
|
+
const SEASONS = [
|
|
950
|
+
"spring",
|
|
951
|
+
"summer",
|
|
952
|
+
"autumn",
|
|
953
|
+
"winter"
|
|
954
|
+
];
|
|
955
|
+
const NO_CALENDAR = {
|
|
956
|
+
hemisphere: "north",
|
|
957
|
+
holidays: []
|
|
958
|
+
};
|
|
959
|
+
const MAX_TOKEN = 60;
|
|
960
|
+
const daysIn = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
961
|
+
const DATE = /^(?:(\d{4})-)?(\d{2})-(\d{2})$/;
|
|
962
|
+
function dateOf(text) {
|
|
963
|
+
const found = DATE.exec(text);
|
|
964
|
+
if (found === null) return void 0;
|
|
965
|
+
const [, year, month, day] = found;
|
|
966
|
+
const m = Number(month);
|
|
967
|
+
const d = Number(day);
|
|
968
|
+
if (m < 1 || m > 12 || d < 1 || d > daysIn(year === void 0 ? 2024 : Number(year), m)) return void 0;
|
|
969
|
+
return {
|
|
970
|
+
...year === void 0 ? {} : { year: Number(year) },
|
|
971
|
+
month: m,
|
|
972
|
+
day: d
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
/** What a token says, or why it says nothing. */
|
|
976
|
+
function parseDayToken(raw) {
|
|
977
|
+
const negated = raw.startsWith("!");
|
|
978
|
+
const text = (negated ? raw.slice(1) : raw).trim().toLowerCase();
|
|
979
|
+
const fail = (problem) => ({ problem: `"${raw}": ${problem}` });
|
|
980
|
+
if (text === "" || text.length > MAX_TOKEN) return fail("a day is told in a few words");
|
|
981
|
+
const weekday = WEEKDAY_NAMES.indexOf(text);
|
|
982
|
+
if (weekday >= 0) return {
|
|
983
|
+
token: {
|
|
984
|
+
kind: "weekday",
|
|
985
|
+
weekday
|
|
986
|
+
},
|
|
987
|
+
negated
|
|
988
|
+
};
|
|
989
|
+
const month = MONTH_NAMES.indexOf(text);
|
|
990
|
+
if (month >= 0) return {
|
|
991
|
+
token: {
|
|
992
|
+
kind: "month",
|
|
993
|
+
month: month + 1
|
|
994
|
+
},
|
|
995
|
+
negated
|
|
996
|
+
};
|
|
997
|
+
const one = dateOf(text);
|
|
998
|
+
if (one !== void 0) return {
|
|
999
|
+
token: {
|
|
1000
|
+
kind: "date",
|
|
1001
|
+
...one
|
|
1002
|
+
},
|
|
1003
|
+
negated
|
|
1004
|
+
};
|
|
1005
|
+
if (text.includes("..")) {
|
|
1006
|
+
const [from, to, ...rest] = text.split("..");
|
|
1007
|
+
const start = from === void 0 ? void 0 : dateOf(from);
|
|
1008
|
+
const end = to === void 0 ? void 0 : dateOf(to);
|
|
1009
|
+
if (rest.length > 0 || start === void 0 || end === void 0 || start.year === void 0 !== (end.year === void 0)) return fail("a stretch is two dates of the same kind: `12-20..01-05` every year, or `1912-09-21..1912-09-22` once");
|
|
1010
|
+
return {
|
|
1011
|
+
token: {
|
|
1012
|
+
kind: "span",
|
|
1013
|
+
from: start,
|
|
1014
|
+
to: end
|
|
1015
|
+
},
|
|
1016
|
+
negated
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
const [head, a, b] = text.split(":");
|
|
1020
|
+
if (head === "dom" && a !== void 0 && b === void 0) {
|
|
1021
|
+
if (a === "last") return {
|
|
1022
|
+
token: {
|
|
1023
|
+
kind: "dom",
|
|
1024
|
+
day: "last"
|
|
1025
|
+
},
|
|
1026
|
+
negated
|
|
1027
|
+
};
|
|
1028
|
+
const day = Number(a);
|
|
1029
|
+
return Number.isInteger(day) && day >= 1 && day <= 31 ? {
|
|
1030
|
+
token: {
|
|
1031
|
+
kind: "dom",
|
|
1032
|
+
day
|
|
1033
|
+
},
|
|
1034
|
+
negated
|
|
1035
|
+
} : fail("the day of the month is 1 to 31, or `dom:last`");
|
|
1036
|
+
}
|
|
1037
|
+
if (head === "nth" && a !== void 0 && b !== void 0) {
|
|
1038
|
+
const day = WEEKDAY_NAMES.indexOf(a);
|
|
1039
|
+
const n = b === "last" ? "last" : Number(b);
|
|
1040
|
+
return day >= 0 && (n === "last" || Number.isInteger(n) && n >= 1 && n <= 5) ? {
|
|
1041
|
+
token: {
|
|
1042
|
+
kind: "nth",
|
|
1043
|
+
weekday: day,
|
|
1044
|
+
n
|
|
1045
|
+
},
|
|
1046
|
+
negated
|
|
1047
|
+
} : fail("`nth:mon:1` is the first Monday of a month, `nth:fri:last` the last Friday");
|
|
1048
|
+
}
|
|
1049
|
+
if (head === "season" && a !== void 0 && b === void 0) {
|
|
1050
|
+
const season = SEASONS.find((candidate) => candidate === a);
|
|
1051
|
+
return season === void 0 ? fail(`a season is one of ${SEASONS.join(", ")}`) : {
|
|
1052
|
+
token: {
|
|
1053
|
+
kind: "season",
|
|
1054
|
+
season
|
|
1055
|
+
},
|
|
1056
|
+
negated
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
if (head === "holiday" && a !== void 0 && b === void 0 && a !== "") return {
|
|
1060
|
+
token: {
|
|
1061
|
+
kind: "holiday",
|
|
1062
|
+
id: a
|
|
1063
|
+
},
|
|
1064
|
+
negated
|
|
1065
|
+
};
|
|
1066
|
+
return fail("not a day: use a weekday (`mon`), a month (`dec`), a date (`12-24`, `1912-09-21`), a stretch (`12-20..01-05`), `dom:1`, `nth:mon:1`, `season:winter` or `holiday:<id>`");
|
|
1067
|
+
}
|
|
1068
|
+
/** What is wrong with a list of day tokens, each with where it is. `holidays` are those the world names, when `holiday:` may be used. */
|
|
1069
|
+
function dayTokenProblems(tokens, path, holidays) {
|
|
1070
|
+
return tokens.flatMap((raw) => {
|
|
1071
|
+
const parsed = parseDayToken(raw);
|
|
1072
|
+
if ("problem" in parsed) return [`${path}: ${parsed.problem}`];
|
|
1073
|
+
if (parsed.token.kind === "holiday") {
|
|
1074
|
+
if (holidays === void 0) return [`${path}: "${raw}": a holiday cannot be named here`];
|
|
1075
|
+
if (holidays !== "any" && !holidays.has(parsed.token.id)) return [`${path}: "${raw}": the world names no holiday "${parsed.token.id}"`];
|
|
1076
|
+
}
|
|
1077
|
+
return [];
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
/** The season a day is in, by the months of its hemisphere (winter is December to February in the north, June to August in the south). */
|
|
1081
|
+
function seasonOf(day, hemisphere = "north") {
|
|
1082
|
+
const north = day.month === 12 || day.month <= 2 ? "winter" : day.month <= 5 ? "spring" : day.month <= 8 ? "summer" : "autumn";
|
|
1083
|
+
if (hemisphere === "north") return north;
|
|
1084
|
+
return north === "winter" ? "summer" : north === "summer" ? "winter" : north === "spring" ? "autumn" : "spring";
|
|
1085
|
+
}
|
|
1086
|
+
const key = (month, day) => month * 100 + day;
|
|
1087
|
+
function within(day, from, to) {
|
|
1088
|
+
if (from.year !== void 0 && to.year !== void 0) {
|
|
1089
|
+
const at = day.year * 1e4 + key(day.month, day.day);
|
|
1090
|
+
return at >= from.year * 1e4 + key(from.month, from.day) && at <= to.year * 1e4 + key(to.month, to.day);
|
|
1091
|
+
}
|
|
1092
|
+
const at = key(day.month, day.day);
|
|
1093
|
+
const start = key(from.month, from.day);
|
|
1094
|
+
const end = key(to.month, to.day);
|
|
1095
|
+
return start <= end ? at >= start && at <= end : at >= start || at <= end;
|
|
1096
|
+
}
|
|
1097
|
+
function matches(token, day, calendar) {
|
|
1098
|
+
switch (token.kind) {
|
|
1099
|
+
case "weekday": return day.weekday === token.weekday;
|
|
1100
|
+
case "month": return day.month === token.month;
|
|
1101
|
+
case "date": return day.month === token.month && day.day === token.day && (token.year === void 0 || day.year === token.year);
|
|
1102
|
+
case "span": return within(day, token.from, token.to);
|
|
1103
|
+
case "dom": return token.day === "last" ? day.day === daysIn(day.year, day.month) : day.day === token.day;
|
|
1104
|
+
case "nth": return day.weekday === token.weekday && (token.n === "last" ? day.day + 7 > daysIn(day.year, day.month) : Math.ceil(day.day / 7) === token.n);
|
|
1105
|
+
case "season": return seasonOf(day, calendar.hemisphere) === token.season;
|
|
1106
|
+
case "holiday": return holidaysOn(calendar, day).some((holiday) => holiday.id === token.id);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/** Whether a list of day tokens holds on a day; tokens that say nothing sensible count as not matching. An empty list holds on every day. */
|
|
1110
|
+
function dayMatches(tokens, day, calendar = NO_CALENDAR) {
|
|
1111
|
+
const parsed = tokens.flatMap((raw) => {
|
|
1112
|
+
const found = parseDayToken(raw);
|
|
1113
|
+
return "problem" in found ? [] : [found];
|
|
1114
|
+
});
|
|
1115
|
+
const wanted = parsed.filter((entry) => !entry.negated);
|
|
1116
|
+
const unwanted = parsed.filter((entry) => entry.negated);
|
|
1117
|
+
return (wanted.length === 0 || wanted.some((entry) => matches(entry.token, day, calendar))) && !unwanted.some((entry) => matches(entry.token, day, calendar));
|
|
1118
|
+
}
|
|
1119
|
+
/** The holidays that fall on a day. A holiday is told by days that are no holidays themselves, so that none can lead back to itself. */
|
|
1120
|
+
function holidaysOn(calendar, day) {
|
|
1121
|
+
return calendar.holidays.filter((holiday) => holiday.on.length > 0 && dayMatches(holiday.on, day, {
|
|
1122
|
+
...calendar,
|
|
1123
|
+
holidays: []
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
const CALENDAR_KEY = "calendar";
|
|
1127
|
+
/** The calendar a story was started with, from its metadata; none when the world has none. */
|
|
1128
|
+
function readCalendar(metadata) {
|
|
1129
|
+
const stored = metadata.getValue(CALENDAR_KEY);
|
|
1130
|
+
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return NO_CALENDAR;
|
|
1131
|
+
const { hemisphere, holidays } = stored;
|
|
1132
|
+
return {
|
|
1133
|
+
hemisphere: hemisphere === "south" ? "south" : "north",
|
|
1134
|
+
holidays: Array.isArray(holidays) ? holidays.flatMap((entry) => {
|
|
1135
|
+
const { id, name, on } = entry ?? {};
|
|
1136
|
+
return typeof id === "string" && typeof name === "string" && Array.isArray(on) ? [{
|
|
1137
|
+
id,
|
|
1138
|
+
name,
|
|
1139
|
+
on: on.filter((token) => typeof token === "string")
|
|
1140
|
+
}] : [];
|
|
1141
|
+
}) : []
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
const writeCalendar = (metadata, calendar) => {
|
|
1145
|
+
metadata.setValue(CALENDAR_KEY, calendar);
|
|
1146
|
+
};
|
|
1147
|
+
//#endregion
|
|
708
1148
|
//#region src/records/time.ts
|
|
709
1149
|
/** Instants are UTC milliseconds; a time zone only decides how they read. */
|
|
710
1150
|
const SECOND = 1e3;
|
|
@@ -832,17 +1272,27 @@ function parseInstant(text, zone) {
|
|
|
832
1272
|
}
|
|
833
1273
|
throw new RangeError(`"${text}" is not a date and time: write it as 1912-10-04T19:30 (or with an offset, 1912-10-04T19:30+01:00)`);
|
|
834
1274
|
}
|
|
835
|
-
|
|
836
|
-
|
|
1275
|
+
const WEEKDAYS_ZH = [
|
|
1276
|
+
"星期日",
|
|
1277
|
+
"星期一",
|
|
1278
|
+
"星期二",
|
|
1279
|
+
"星期三",
|
|
1280
|
+
"星期四",
|
|
1281
|
+
"星期五",
|
|
1282
|
+
"星期六"
|
|
1283
|
+
];
|
|
1284
|
+
/** `Tuesday 4 October 1912, 21:40`, or `1912年10月4日 星期二 21:40`. */
|
|
1285
|
+
function describeInstant(instant, zone, language = "en") {
|
|
837
1286
|
const local = localTime(instant, zone);
|
|
838
|
-
return `${
|
|
1287
|
+
if (language === "zh") return `${local.year}年${local.month}月${local.day}日 ${WEEKDAYS_ZH[local.weekday]} ${pad$1(local.hour)}:${pad$1(local.minute)}`;
|
|
1288
|
+
return `${WEEKDAYS[local.weekday]} ${local.day} ${MONTHS[local.month - 1]} ${local.year}, ${pad$1(local.hour)}:${pad$1(local.minute)}`;
|
|
839
1289
|
}
|
|
840
1290
|
/** `1912-10-04T21:40:00` as the wall clock of a zone reads, the form a file or a person can write back. */
|
|
841
1291
|
function localIso(instant, zone) {
|
|
842
1292
|
const local = localTime(instant, zone);
|
|
843
|
-
return `${String(local.year).padStart(4, "0")}-${pad(local.month)}-${pad(local.day)}T${pad(local.hour)}:${pad(local.minute)}:${pad(local.second)}`;
|
|
1293
|
+
return `${String(local.year).padStart(4, "0")}-${pad$1(local.month)}-${pad$1(local.day)}T${pad$1(local.hour)}:${pad$1(local.minute)}:${pad$1(local.second)}`;
|
|
844
1294
|
}
|
|
845
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
1295
|
+
const pad$1 = (value) => String(value).padStart(2, "0");
|
|
846
1296
|
const DAY_PARTS = [
|
|
847
1297
|
"night",
|
|
848
1298
|
"morning",
|
|
@@ -854,9 +1304,25 @@ function dayPart(minuteOfDay) {
|
|
|
854
1304
|
const hour = Math.floor(minuteOfDay / 60);
|
|
855
1305
|
return hour < 5 || hour >= 22 ? "night" : hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening";
|
|
856
1306
|
}
|
|
857
|
-
/** In words and rounded to what matters: `45 seconds`, `3 hours 12 minutes`, `2 days 3 hours
|
|
858
|
-
function describeDuration(milliseconds) {
|
|
1307
|
+
/** In words and rounded to what matters: `45 seconds`, `3 hours 12 minutes`, `2 days 3 hours`; or `45秒`, `3小时12分钟`, `2天3小时`. */
|
|
1308
|
+
function describeDuration(milliseconds, language = "en") {
|
|
859
1309
|
const total = Math.max(0, Math.round(milliseconds / SECOND));
|
|
1310
|
+
if (language === "zh") {
|
|
1311
|
+
if (total < 60) return `${total}秒`;
|
|
1312
|
+
const parts = [];
|
|
1313
|
+
let left = total;
|
|
1314
|
+
for (const [name, size] of [
|
|
1315
|
+
["天", 86400],
|
|
1316
|
+
["小时", 3600],
|
|
1317
|
+
["分钟", 60]
|
|
1318
|
+
]) {
|
|
1319
|
+
const count = Math.floor(left / size);
|
|
1320
|
+
left -= count * size;
|
|
1321
|
+
if (count > 0) parts.push(`${count}${name}`);
|
|
1322
|
+
if (parts.length === 2) break;
|
|
1323
|
+
}
|
|
1324
|
+
return parts.join("");
|
|
1325
|
+
}
|
|
860
1326
|
if (total < 60) return `${total} second${total === 1 ? "" : "s"}`;
|
|
861
1327
|
const units = [
|
|
862
1328
|
["day", 86400],
|
|
@@ -946,7 +1412,7 @@ function catchUpPlan(cursor, now, limits = {}) {
|
|
|
946
1412
|
/** `21:40`: the clock of a zone at an instant. */
|
|
947
1413
|
function clockTime(instant, zone) {
|
|
948
1414
|
const local = localTime(instant, zone);
|
|
949
|
-
return `${pad(local.hour)}:${pad(local.minute)}`;
|
|
1415
|
+
return `${pad$1(local.hour)}:${pad$1(local.minute)}`;
|
|
950
1416
|
}
|
|
951
1417
|
/** The local calendar day `days` after the one the instant falls on, as year, month and day. */
|
|
952
1418
|
function addLocalDays(instant, zone, days) {
|
|
@@ -964,7 +1430,7 @@ function addLocalDays(instant, zone, days) {
|
|
|
964
1430
|
};
|
|
965
1431
|
}
|
|
966
1432
|
/** `07:30` for a minute of the day. */
|
|
967
|
-
const minuteText = (minute) => `${pad(Math.floor(minute / 60))}:${pad(minute % 60)}`;
|
|
1433
|
+
const minuteText = (minute) => `${pad$1(Math.floor(minute / 60))}:${pad$1(minute % 60)}`;
|
|
968
1434
|
//#endregion
|
|
969
1435
|
//#region src/records/agenda-edit.ts
|
|
970
1436
|
const MAX_PLAN_STEPS = 8;
|
|
@@ -1112,6 +1578,8 @@ function prepareAgenda(target, edit, context) {
|
|
|
1112
1578
|
if (found < 0) problems.push(`${here}.days: "${day}" is not a day (${WEEKDAY_NAMES.join(", ")})`);
|
|
1113
1579
|
return found < 0 ? [] : [found];
|
|
1114
1580
|
});
|
|
1581
|
+
const holidays = new Set(readCalendar(target.metadata).holidays.map((holiday) => holiday.id));
|
|
1582
|
+
problems.push(...dayTokenProblems(list$1(window.on), `${here}.on`, holidays));
|
|
1115
1583
|
if (window.location !== void 0) {
|
|
1116
1584
|
placeKnown(window.location, `${here}.location`);
|
|
1117
1585
|
const place = context.places.get(window.location);
|
|
@@ -1120,6 +1588,7 @@ function prepareAgenda(target, edit, context) {
|
|
|
1120
1588
|
if (start === void 0 || end === void 0) return [];
|
|
1121
1589
|
return [{
|
|
1122
1590
|
days,
|
|
1591
|
+
on: [...list$1(window.on)],
|
|
1123
1592
|
start,
|
|
1124
1593
|
end,
|
|
1125
1594
|
activity: window.activity.trim(),
|
|
@@ -1664,6 +2133,78 @@ function moveCharacter(context, characterId, move) {
|
|
|
1664
2133
|
return result.plan;
|
|
1665
2134
|
}
|
|
1666
2135
|
//#endregion
|
|
2136
|
+
//#region src/records/template.ts
|
|
2137
|
+
/**
|
|
2138
|
+
* What the words of a cue or a beat may name by `{kind:key}`, filled in from the story when they are told. The key is how the world's files
|
|
2139
|
+
* name the thing (`@self` in a cue is the character it is said to), and a name follows the story: a renamed character is renamed in the words.
|
|
2140
|
+
*/
|
|
2141
|
+
const TEMPLATE_KINDS = {
|
|
2142
|
+
/** The name of a character. */
|
|
2143
|
+
character: {
|
|
2144
|
+
args: 1,
|
|
2145
|
+
about: "a character"
|
|
2146
|
+
},
|
|
2147
|
+
/** The name of a place. */
|
|
2148
|
+
place: {
|
|
2149
|
+
args: 1,
|
|
2150
|
+
about: "a place"
|
|
2151
|
+
},
|
|
2152
|
+
/** The name of an item. */
|
|
2153
|
+
item: {
|
|
2154
|
+
args: 1,
|
|
2155
|
+
about: "an item"
|
|
2156
|
+
},
|
|
2157
|
+
/** The name of the place a character is in now. */
|
|
2158
|
+
where: {
|
|
2159
|
+
args: 1,
|
|
2160
|
+
about: "a character"
|
|
2161
|
+
},
|
|
2162
|
+
/** Who holds an item now, or the place it lies in. */
|
|
2163
|
+
holder: {
|
|
2164
|
+
args: 1,
|
|
2165
|
+
about: "an item"
|
|
2166
|
+
},
|
|
2167
|
+
/** What a variable is now, in words: `{value:key}` for a world variable, `{value:key:subject}` for the character, item or place it is about. */
|
|
2168
|
+
value: {
|
|
2169
|
+
args: 2,
|
|
2170
|
+
about: "a variable"
|
|
2171
|
+
},
|
|
2172
|
+
/** The time of day in the story, as `19:30`. */
|
|
2173
|
+
time: {
|
|
2174
|
+
args: 0,
|
|
2175
|
+
about: ""
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
const PATTERN = /\{([a-z]+)(?::([^{}]*))?\}/g;
|
|
2179
|
+
/** The placeholders in some words, and what is wrong with any that is not one. Anything that does not look like `{word}` or `{word:key}` is plain text. */
|
|
2180
|
+
function placeholdersOf(text) {
|
|
2181
|
+
const found = [];
|
|
2182
|
+
const problems = [];
|
|
2183
|
+
for (const match of text.matchAll(PATTERN)) {
|
|
2184
|
+
const [raw, word, rest] = match;
|
|
2185
|
+
if (!(word in TEMPLATE_KINDS)) {
|
|
2186
|
+
problems.push(`${raw}: there is no "${word}"; the words may name ${Object.keys(TEMPLATE_KINDS).join(", ")}`);
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const kind = word;
|
|
2190
|
+
const args = rest === void 0 ? [] : rest.split(":").map((part) => part.trim());
|
|
2191
|
+
const { args: wanted } = TEMPLATE_KINDS[kind];
|
|
2192
|
+
if (!(kind === "value" ? args.length >= 1 && args.length <= 2 : args.length === wanted) || args.some((part) => part === "")) {
|
|
2193
|
+
problems.push(`${raw}: ${kind === "time" ? "{time} takes nothing" : kind === "value" ? "{value:variable} or {value:variable:subject}" : `{${kind}:key} takes the key of ${TEMPLATE_KINDS[kind].about}`}`);
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
found.push({
|
|
2197
|
+
raw,
|
|
2198
|
+
kind,
|
|
2199
|
+
args
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
return {
|
|
2203
|
+
found,
|
|
2204
|
+
problems
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
//#endregion
|
|
1667
2208
|
//#region src/sqlite/variable-repository.ts
|
|
1668
2209
|
const MAX_BANDS = 8;
|
|
1669
2210
|
const MAX_OPTIONS = 12;
|
|
@@ -2069,8 +2610,11 @@ function parseCondition(value, path, problems, count, depth = 0) {
|
|
|
2069
2610
|
to: text(value["window"], "to") ?? ""
|
|
2070
2611
|
} : void 0;
|
|
2071
2612
|
const days = Array.isArray(value["days"]) ? value["days"].filter((day) => typeof day === "string") : void 0;
|
|
2072
|
-
|
|
2073
|
-
|
|
2613
|
+
const on = Array.isArray(value["on"]) ? value["on"].filter((day) => typeof day === "string") : void 0;
|
|
2614
|
+
if (value["on"] !== void 0 && on?.length !== value["on"]?.length) problems.push(`${path}.on: a list of days (months, dates, seasons, holidays)`);
|
|
2615
|
+
if (on !== void 0) problems.push(...dayTokenProblems(on, `${path}.on`, "any"));
|
|
2616
|
+
if (from === void 0 && to === void 0 && window === void 0 && days === void 0 && on === void 0) {
|
|
2617
|
+
problems.push(`${path}: a time condition needs from, to, window, days or on`);
|
|
2074
2618
|
return;
|
|
2075
2619
|
}
|
|
2076
2620
|
if (window !== void 0 && (parseTimeOfDay(window.from) === void 0 || parseTimeOfDay(window.to) === void 0 || window.from === window.to)) problems.push(`${path}.window: from and to are different times of day, HH:MM`);
|
|
@@ -2081,7 +2625,8 @@ function parseCondition(value, path, problems, count, depth = 0) {
|
|
|
2081
2625
|
...from === void 0 ? {} : { from },
|
|
2082
2626
|
...to === void 0 ? {} : { to },
|
|
2083
2627
|
...window === void 0 ? {} : { window },
|
|
2084
|
-
...days === void 0 ? {} : { days }
|
|
2628
|
+
...days === void 0 ? {} : { days },
|
|
2629
|
+
...on === void 0 ? {} : { on }
|
|
2085
2630
|
};
|
|
2086
2631
|
}
|
|
2087
2632
|
case "place": {
|
|
@@ -2206,6 +2751,21 @@ function parseAnnouncement(value, path, problems) {
|
|
|
2206
2751
|
...at === void 0 ? {} : { at }
|
|
2207
2752
|
};
|
|
2208
2753
|
}
|
|
2754
|
+
/** Whether words or a condition name the character a cue is said to. */
|
|
2755
|
+
function usesSelf(words, condition) {
|
|
2756
|
+
return placeholdersOf(words).found.some((placeholder) => placeholder.args.includes("@self")) || condition !== void 0 && mentionsSelf(condition);
|
|
2757
|
+
}
|
|
2758
|
+
function mentionsSelf(condition) {
|
|
2759
|
+
switch (condition.type) {
|
|
2760
|
+
case "place": return condition.character === SELF;
|
|
2761
|
+
case "variable": return condition.subject === SELF;
|
|
2762
|
+
case "item": return condition.item === "@self" || condition.heldBy === "@self";
|
|
2763
|
+
case "all":
|
|
2764
|
+
case "any": return condition.conditions.some(mentionsSelf);
|
|
2765
|
+
case "not": return mentionsSelf(condition.condition);
|
|
2766
|
+
default: return false;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2209
2769
|
function parseEffect(value, path, problems, checkEdit) {
|
|
2210
2770
|
if (!isObject$2(value)) {
|
|
2211
2771
|
problems.push(`${path}: an effect is an object`);
|
|
@@ -2225,6 +2785,10 @@ function parseEffect(value, path, problems, checkEdit) {
|
|
|
2225
2785
|
problems.push(`${path}.edit: ${error instanceof InvalidEditError ? error.problems.join("; ") : String(error)}`);
|
|
2226
2786
|
return;
|
|
2227
2787
|
}
|
|
2788
|
+
if (value["while"] !== void 0) {
|
|
2789
|
+
problems.push(`${path}.while: only a beat or a cue can wait on a condition when it is told`);
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2228
2792
|
return {
|
|
2229
2793
|
type: "edit",
|
|
2230
2794
|
edit: value["edit"]
|
|
@@ -2239,16 +2803,26 @@ function parseEffect(value, path, problems, checkEdit) {
|
|
|
2239
2803
|
problems.push(`${path}.text: required text of at most ${MAX_TEXT$1} characters`);
|
|
2240
2804
|
return;
|
|
2241
2805
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2806
|
+
const template = placeholdersOf(words);
|
|
2807
|
+
for (const problem of template.problems) problems.push(`${path}.text: ${problem}`);
|
|
2808
|
+
const waiting = value["while"] === void 0 ? void 0 : parseCondition(value["while"], `${path}.while`, problems, { nodes: 0 });
|
|
2809
|
+
if (value["while"] !== void 0 && waiting === void 0) return void 0;
|
|
2810
|
+
if (waiting !== void 0 && judgedStatements(waiting).length > 0) problems.push(`${path}.while: a statement a model judges (judged) cannot be waited on when something is told; use what the story records`);
|
|
2811
|
+
if (value["type"] === "beat") {
|
|
2812
|
+
if (usesSelf(words, waiting)) problems.push(`${path}: only a cue is said to someone, so only a cue can name ${SELF}`);
|
|
2813
|
+
return template.problems.length > 0 ? void 0 : {
|
|
2814
|
+
type: "beat",
|
|
2815
|
+
text: words,
|
|
2816
|
+
...waiting === void 0 ? {} : { while: waiting }
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2246
2819
|
if (value["type"] === "cue") {
|
|
2247
2820
|
const audience = value["audience"] !== void 0 ? parseAudience(value["audience"], `${path}.audience`, problems) : parseAudience({ characters: value["characters"] }, `${path}.characters`, problems);
|
|
2248
|
-
return audience === void 0 ? void 0 : {
|
|
2821
|
+
return audience === void 0 || template.problems.length > 0 ? void 0 : {
|
|
2249
2822
|
type: "cue",
|
|
2250
2823
|
audience,
|
|
2251
|
-
text: words
|
|
2824
|
+
text: words,
|
|
2825
|
+
...waiting === void 0 ? {} : { while: waiting }
|
|
2252
2826
|
};
|
|
2253
2827
|
}
|
|
2254
2828
|
problems.push(`${path}.type: one of edit, beat, cue`);
|
|
@@ -2300,21 +2874,21 @@ function parseTriggerDefinition(value, path, checkEdit) {
|
|
|
2300
2874
|
...announce === void 0 ? {} : { announce }
|
|
2301
2875
|
} };
|
|
2302
2876
|
}
|
|
2303
|
-
function conditionProblems(target, condition, path, known) {
|
|
2304
|
-
const character = (key) => target.characters.list().some((entry) => characterKey(entry) === key) ? [] : [`${path}: there is no character "${key}"`];
|
|
2877
|
+
function conditionProblems(target, condition, path, known, self = false) {
|
|
2878
|
+
const character = (key) => self && key === "@self" || target.characters.list().some((entry) => characterKey(entry) === key) ? [] : [`${path}: there is no character "${key}"`];
|
|
2305
2879
|
const place = (key) => target.locations.list().some((entry) => locationKey(entry) === key) ? [] : [`${path}: there is no place "${key}"`];
|
|
2306
2880
|
switch (condition.type) {
|
|
2307
|
-
case "time": return [];
|
|
2881
|
+
case "time": return dayTokenProblems(condition.on ?? [], `${path}.on`, new Set(readCalendar(target.metadata).holidays.map((holiday) => holiday.id)));
|
|
2308
2882
|
case "place": return [...character(condition.character), ...place(condition.location)];
|
|
2309
2883
|
case "variable": {
|
|
2310
2884
|
const def = target.variables.def(condition.variable);
|
|
2311
2885
|
if (def === void 0) return [`${path}: there is no variable "${condition.variable}"`];
|
|
2312
2886
|
if (def.subject === "world" ? condition.subject !== void 0 : condition.subject === void 0) return [`${path}: ${def.subject === "world" ? "a world variable has no subject" : `give the ${def.subject} it is about as subject`}`];
|
|
2313
|
-
const subject = condition.subject === void 0 ? void 0 : subjectOf(target, def.subject, condition.subject);
|
|
2314
|
-
return [...condition.subject !== void 0 && subject === void 0 ? [`${path}: there is no ${def.subject} "${condition.subject}"`] : [], ...def.kind !== "number" && condition.op !== "eq" && condition.op !== "ne" ? [`${path}: ${def.kind} variables compare with eq or ne only`] : []];
|
|
2887
|
+
const subject = condition.subject === void 0 || self && condition.subject === "@self" ? void 0 : subjectOf(target, def.subject, condition.subject);
|
|
2888
|
+
return [...condition.subject !== void 0 && subject === void 0 && !(self && condition.subject === "@self") ? [`${path}: there is no ${def.subject} "${condition.subject}"`] : [], ...def.kind !== "number" && condition.op !== "eq" && condition.op !== "ne" ? [`${path}: ${def.kind} variables compare with eq or ne only`] : []];
|
|
2315
2889
|
}
|
|
2316
2890
|
case "item": return [
|
|
2317
|
-
...target.items.list().some((entry) => itemKey(entry) === condition.item) ? [] : [`${path}: there is no item "${condition.item}"`],
|
|
2891
|
+
...self && condition.item === "@self" || target.items.list().some((entry) => itemKey(entry) === condition.item) ? [] : [`${path}: there is no item "${condition.item}"`],
|
|
2318
2892
|
...condition.heldBy === void 0 ? [] : character(condition.heldBy),
|
|
2319
2893
|
...condition.at === void 0 ? [] : place(condition.at)
|
|
2320
2894
|
];
|
|
@@ -2322,9 +2896,33 @@ function conditionProblems(target, condition, path, known) {
|
|
|
2322
2896
|
case "fired": return known.has(condition.trigger) ? [] : [`${path}: there is no trigger "${condition.trigger}"`];
|
|
2323
2897
|
case "judged": return [];
|
|
2324
2898
|
case "all":
|
|
2325
|
-
case "any": return condition.conditions.flatMap((part, index) => conditionProblems(target, part, `${path}.conditions[${index}]`, known));
|
|
2326
|
-
case "not": return conditionProblems(target, condition.condition, `${path}.condition`, known);
|
|
2327
|
-
}
|
|
2899
|
+
case "any": return condition.conditions.flatMap((part, index) => conditionProblems(target, part, `${path}.conditions[${index}]`, known, self));
|
|
2900
|
+
case "not": return conditionProblems(target, condition.condition, `${path}.condition`, known, self);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
/** What the words of a cue or beat name by `{kind:key}` that the story does not have. */
|
|
2904
|
+
function wordsProblems(target, words, path, self) {
|
|
2905
|
+
return placeholdersOf(words).found.flatMap((placeholder) => {
|
|
2906
|
+
const key = placeholder.args[0];
|
|
2907
|
+
const selfKey = self && key === "@self";
|
|
2908
|
+
const missing = (what) => [`${path}: ${placeholder.raw}: there is no ${what} "${key}"`];
|
|
2909
|
+
switch (placeholder.kind) {
|
|
2910
|
+
case "character":
|
|
2911
|
+
case "where": return selfKey || target.characters.list().some((entry) => characterKey(entry) === key) ? [] : missing("character");
|
|
2912
|
+
case "place": return target.locations.list().some((entry) => locationKey(entry) === key) ? [] : missing("place");
|
|
2913
|
+
case "item":
|
|
2914
|
+
case "holder": return target.items.list().some((entry) => itemKey(entry) === key) ? [] : missing("item");
|
|
2915
|
+
case "value": {
|
|
2916
|
+
const def = target.variables.def(key);
|
|
2917
|
+
if (def === void 0) return missing("variable");
|
|
2918
|
+
const subject = placeholder.args[1];
|
|
2919
|
+
if (def.subject === "world") return subject === void 0 ? [] : [`${path}: ${placeholder.raw}: a world variable has no subject`];
|
|
2920
|
+
if (subject === void 0) return [`${path}: ${placeholder.raw}: give the ${def.subject} it is about`];
|
|
2921
|
+
return self && subject === "@self" || subjectOf(target, def.subject, subject) !== void 0 ? [] : [`${path}: ${placeholder.raw}: there is no ${def.subject} "${subject}"`];
|
|
2922
|
+
}
|
|
2923
|
+
case "time": return [];
|
|
2924
|
+
}
|
|
2925
|
+
});
|
|
2328
2926
|
}
|
|
2329
2927
|
/**
|
|
2330
2928
|
* What in a trigger names something the story does not have: a character, place, item or variable in a condition, the characters of a cue,
|
|
@@ -2338,10 +2936,12 @@ function triggerReferenceProblems(target, definition, otherKeys = []) {
|
|
|
2338
2936
|
return who.characters.flatMap((key) => target.characters.list().some((entry) => characterKey(entry) === key) ? [] : [`${path}: there is no character "${key}"`]);
|
|
2339
2937
|
};
|
|
2340
2938
|
const cues = definition.effects.flatMap((effect, index) => effect.type !== "cue" ? [] : audience(effect.audience, `effects[${index}]`));
|
|
2939
|
+
const said = definition.effects.flatMap((effect, index) => effect.type === "edit" ? [] : [...effect.while === void 0 ? [] : conditionProblems(target, effect.while, `effects[${index}].while`, known, effect.type === "cue"), ...wordsProblems(target, effect.text, `effects[${index}].text`, effect.type === "cue")]);
|
|
2341
2940
|
const announced = definition.announce === void 0 ? [] : audience(definition.announce.audience, "announce");
|
|
2342
2941
|
return [
|
|
2343
2942
|
...conditionProblems(target, definition.condition, "when", known),
|
|
2344
2943
|
...cues,
|
|
2944
|
+
...said,
|
|
2345
2945
|
...announced
|
|
2346
2946
|
].map((problem) => `${definition.key}: ${problem}`);
|
|
2347
2947
|
}
|
|
@@ -2374,14 +2974,20 @@ function holds(target, condition, clock) {
|
|
|
2374
2974
|
if (condition.from !== void 0 && now < parseInstant(condition.from, zone)) return false;
|
|
2375
2975
|
if (condition.to !== void 0 && now >= parseInstant(condition.to, zone)) return false;
|
|
2376
2976
|
const local = localTime(now, zone);
|
|
2977
|
+
const calendar = readCalendar(target.metadata);
|
|
2978
|
+
/** The day a time belongs to: the morning part of a window past midnight belongs to the day before. */
|
|
2979
|
+
const dayAt = (shift) => addLocalDays(now, zone, shift);
|
|
2980
|
+
const onDay = (shift) => {
|
|
2981
|
+
const day = dayAt(shift);
|
|
2982
|
+
return (condition.days === void 0 || condition.days.length === 0 || condition.days.includes(WEEKDAY_NAMES[day.weekday] ?? "")) && (condition.on === void 0 || dayMatches(condition.on, day, calendar));
|
|
2983
|
+
};
|
|
2377
2984
|
if (condition.window !== void 0) {
|
|
2378
2985
|
const start = parseTimeOfDay(condition.window.from) ?? 0;
|
|
2379
2986
|
const end = parseTimeOfDay(condition.window.to) ?? 0;
|
|
2380
2987
|
if (!inWindow(local.minuteOfDay, start, end)) return false;
|
|
2381
|
-
|
|
2382
|
-
return condition.days === void 0 || condition.days.length === 0 || condition.days.includes(WEEKDAY_NAMES[weekday] ?? "");
|
|
2988
|
+
return onDay(start > end && local.minuteOfDay < end ? -1 : 0);
|
|
2383
2989
|
}
|
|
2384
|
-
return
|
|
2990
|
+
return onDay(0);
|
|
2385
2991
|
}
|
|
2386
2992
|
case "place": {
|
|
2387
2993
|
const who = target.characters.list().find((entry) => characterKey(entry) === condition.character);
|
|
@@ -2503,6 +3109,15 @@ function parseRecordsEdit(value) {
|
|
|
2503
3109
|
}
|
|
2504
3110
|
return found;
|
|
2505
3111
|
};
|
|
3112
|
+
const numbersOf = (entry, key) => {
|
|
3113
|
+
const found = entry[key];
|
|
3114
|
+
if (found === void 0 || found === null) return void 0;
|
|
3115
|
+
if (!Array.isArray(found) || found.some((item) => typeof item !== "number")) {
|
|
3116
|
+
problems.push(`${key} must be a list of numbers`);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
return found;
|
|
3120
|
+
};
|
|
2506
3121
|
const text = (entry, key) => typeof entry[key] === "string" ? entry[key] : void 0;
|
|
2507
3122
|
/** A key, slot or position the model may leave blank instead of leaving out: blank is nothing given, not a name that does not exist. */
|
|
2508
3123
|
const named = (entry, key) => {
|
|
@@ -2795,13 +3410,19 @@ function parseRecordsEdit(value) {
|
|
|
2795
3410
|
kind: text(entry, "kind") ?? "",
|
|
2796
3411
|
text: text(entry, "text") ?? "",
|
|
2797
3412
|
about: strings(entry, "about"),
|
|
2798
|
-
importance: num(entry, "importance") ?? 0
|
|
3413
|
+
importance: num(entry, "importance") ?? 0,
|
|
3414
|
+
confidence: text(entry, "confidence"),
|
|
3415
|
+
evidenceFor: numbersOf(entry, "evidence_for"),
|
|
3416
|
+
evidenceAgainst: numbersOf(entry, "evidence_against")
|
|
2799
3417
|
})),
|
|
2800
3418
|
beliefEdits: array("belief_edits").map((entry) => defined({
|
|
2801
3419
|
id: num(entry, "id") ?? 0,
|
|
2802
3420
|
text: text(entry, "text"),
|
|
2803
3421
|
about: strings(entry, "about"),
|
|
2804
|
-
importance: num(entry, "importance")
|
|
3422
|
+
importance: num(entry, "importance"),
|
|
3423
|
+
confidence: text(entry, "confidence"),
|
|
3424
|
+
evidenceFor: numbersOf(entry, "evidence_for"),
|
|
3425
|
+
evidenceAgainst: numbersOf(entry, "evidence_against")
|
|
2805
3426
|
})),
|
|
2806
3427
|
beliefRetractions: numbers("belief_retractions"),
|
|
2807
3428
|
threadsOpened: Array.isArray(value["threads_opened"]) ? value["threads_opened"].filter((entry) => typeof entry === "string") : [],
|
|
@@ -2886,6 +3507,7 @@ function parseRecordsEdit(value) {
|
|
|
2886
3507
|
character: text(entry, "character") ?? "",
|
|
2887
3508
|
windows: Array.isArray(entry["windows"]) ? entry["windows"].filter(isObject$1).map((window) => defined({
|
|
2888
3509
|
days: strings(window, "days"),
|
|
3510
|
+
on: strings(window, "on"),
|
|
2889
3511
|
from: text(window, "from") ?? "",
|
|
2890
3512
|
to: text(window, "to") ?? "",
|
|
2891
3513
|
activity: text(window, "activity") ?? "",
|
|
@@ -2907,6 +3529,21 @@ const scalarValue = {
|
|
|
2907
3529
|
],
|
|
2908
3530
|
description: "A number, true or false, or one of the words of a choice."
|
|
2909
3531
|
};
|
|
3532
|
+
const CONFIDENCE = {
|
|
3533
|
+
type: "string",
|
|
3534
|
+
enum: [...MEMORY_CONFIDENCES],
|
|
3535
|
+
description: "How sure the character is: doubtful, likely (the default) or sure. Sure only for what it has seen more than once or cannot doubt."
|
|
3536
|
+
};
|
|
3537
|
+
const EVIDENCE_FOR = {
|
|
3538
|
+
type: "array",
|
|
3539
|
+
items: { type: "integer" },
|
|
3540
|
+
description: "The numbers of perceptions of the character (from read_records) that this rests on."
|
|
3541
|
+
};
|
|
3542
|
+
const EVIDENCE_AGAINST = {
|
|
3543
|
+
type: "array",
|
|
3544
|
+
items: { type: "integer" },
|
|
3545
|
+
description: "The numbers of perceptions of the character that speak against this; lower the confidence then."
|
|
3546
|
+
};
|
|
2910
3547
|
const IMPORTANCE = {
|
|
2911
3548
|
type: "integer",
|
|
2912
3549
|
enum: [
|
|
@@ -3067,7 +3704,10 @@ function recordsEditSchema(options = {}) {
|
|
|
3067
3704
|
},
|
|
3068
3705
|
text: { type: "string" },
|
|
3069
3706
|
about: keys,
|
|
3070
|
-
importance: IMPORTANCE
|
|
3707
|
+
importance: IMPORTANCE,
|
|
3708
|
+
confidence: CONFIDENCE,
|
|
3709
|
+
evidence_for: EVIDENCE_FOR,
|
|
3710
|
+
evidence_against: EVIDENCE_AGAINST
|
|
3071
3711
|
},
|
|
3072
3712
|
required: [
|
|
3073
3713
|
"character",
|
|
@@ -3683,7 +4323,7 @@ function recordsEditSchema(options = {}) {
|
|
|
3683
4323
|
},
|
|
3684
4324
|
effects: {
|
|
3685
4325
|
type: "array",
|
|
3686
|
-
description: "What happens, one to six: {type:\"edit\", edit:{...}} a change of the records in the same format as edit_records (events with a location are seen by those there, variable_changes, exit_changes, item_changes, moves, intents_added, engagements); {type:\"beat\", text} something the game master is told to weave into the story; {type:\"cue\", characters:[keys] (or audience: \"everyone\" | {place:key}), text} something those characters notice; in the events of an edit, perceived_by may be \"everyone\".",
|
|
4326
|
+
description: "What happens, one to six: {type:\"edit\", edit:{...}} a change of the records in the same format as edit_records (events with a location are seen by those there, variable_changes, exit_changes, item_changes, moves, intents_added, engagements); {type:\"beat\", text, while?} something the game master is told to weave into the story; {type:\"cue\", characters:[keys] (or audience: \"everyone\" | {place:key}), text, while?} something those characters notice; a beat or cue is written when it is told, which may be turns after the trigger fired: its text may name things as {character:key}, {place:key}, {item:key}, {where:character} (the place a character is in), {holder:item} (who has it or where it lies), {value:variable} or {value:variable:subject}, {time}, and is told as the story stands then, and `while` is a condition (as above, nothing judged) that must still hold then or it is withdrawn; in a cue \"@self\" stands for the character it is said to (for example while:{type:\"place\", character:\"@self\", location:\"cellar\"}); in the events of an edit, perceived_by may be \"everyone\".",
|
|
3687
4327
|
items: {
|
|
3688
4328
|
type: "object",
|
|
3689
4329
|
properties: { type: {
|
|
@@ -3764,6 +4404,11 @@ function recordsEditSchema(options = {}) {
|
|
|
3764
4404
|
},
|
|
3765
4405
|
description: "Leave out for every day."
|
|
3766
4406
|
},
|
|
4407
|
+
on: {
|
|
4408
|
+
type: "array",
|
|
4409
|
+
items: { type: "string" },
|
|
4410
|
+
description: "Other days it applies on, as well as days: months (dec), dates (12-24, 1912-09-21), stretches (12-20..01-05), dom:1, nth:mon:1 (first Monday), season:winter, holiday:<id>; a ! before one is a day it does not apply on (!holiday:festival). Leave out for every day."
|
|
4411
|
+
},
|
|
3767
4412
|
from: {
|
|
3768
4413
|
type: "string",
|
|
3769
4414
|
description: "HH:MM, 24-hour."
|
|
@@ -4129,7 +4774,10 @@ function recordsEditSchema(options = {}) {
|
|
|
4129
4774
|
id: { type: "integer" },
|
|
4130
4775
|
text: { type: "string" },
|
|
4131
4776
|
about: keys,
|
|
4132
|
-
importance: IMPORTANCE
|
|
4777
|
+
importance: IMPORTANCE,
|
|
4778
|
+
confidence: CONFIDENCE,
|
|
4779
|
+
evidence_for: EVIDENCE_FOR,
|
|
4780
|
+
evidence_against: EVIDENCE_AGAINST
|
|
4133
4781
|
},
|
|
4134
4782
|
required: ["id"],
|
|
4135
4783
|
additionalProperties: false
|
|
@@ -4475,26 +5123,49 @@ function applyChanges(target, edit) {
|
|
|
4475
5123
|
perception: one
|
|
4476
5124
|
}];
|
|
4477
5125
|
});
|
|
5126
|
+
const confidence = (value, where) => {
|
|
5127
|
+
if (value !== void 0 && !MEMORY_CONFIDENCES.includes(value)) problems.push(`${where}: confidence is one of ${MEMORY_CONFIDENCES.join(", ")}`);
|
|
5128
|
+
};
|
|
5129
|
+
/** Perceptions a belief of one character is said to rest on, or to be contradicted by: they must be the character's own. */
|
|
5130
|
+
const evidence = (value, characterId, where) => (value ?? []).flatMap((id) => {
|
|
5131
|
+
const seen = target.events.getPerception(id);
|
|
5132
|
+
if (seen === void 0) {
|
|
5133
|
+
problems.push(`${where}: there is no perception ${id}`);
|
|
5134
|
+
return [];
|
|
5135
|
+
}
|
|
5136
|
+
if (characterId !== void 0 && seen.characterId !== characterId) {
|
|
5137
|
+
problems.push(`${where}: perception ${id} is not one the character took in`);
|
|
5138
|
+
return [];
|
|
5139
|
+
}
|
|
5140
|
+
return [id];
|
|
5141
|
+
});
|
|
4478
5142
|
const beliefs = list(edit.beliefs).flatMap((belief, index) => {
|
|
4479
5143
|
const where = `beliefs[${index}]`;
|
|
4480
5144
|
const character = idOf(belief.character, where);
|
|
4481
5145
|
if (!MEMORY_KINDS.includes(belief.kind)) problems.push(`${where}: unknown kind "${String(belief.kind)}"`);
|
|
4482
5146
|
if (!present(belief.text)) problems.push(`${where}: say what the character believes or knows`);
|
|
4483
5147
|
importance(belief.importance, where);
|
|
5148
|
+
confidence(belief.confidence, where);
|
|
4484
5149
|
return character === void 0 ? [] : [{
|
|
4485
5150
|
belief,
|
|
4486
5151
|
character,
|
|
4487
|
-
about: ids(belief.about, `${where}.about`)
|
|
5152
|
+
about: ids(belief.about, `${where}.about`),
|
|
5153
|
+
evidenceFor: evidence(belief.evidenceFor, character, `${where}.evidence_for`),
|
|
5154
|
+
evidenceAgainst: evidence(belief.evidenceAgainst, character, `${where}.evidence_against`)
|
|
4488
5155
|
}];
|
|
4489
5156
|
});
|
|
4490
5157
|
const beliefEdits = list(edit.beliefEdits).map((change, index) => {
|
|
4491
5158
|
const where = `belief_edits[${index}]`;
|
|
4492
|
-
|
|
5159
|
+
const current = target.memories.get(change.id);
|
|
5160
|
+
if (current === void 0) problems.push(`${where}: there is no belief ${change.id}`);
|
|
4493
5161
|
if (change.text !== void 0 && !present(change.text)) problems.push(`${where}: say what the character believes or knows`);
|
|
4494
5162
|
importance(change.importance, where);
|
|
5163
|
+
confidence(change.confidence, where);
|
|
4495
5164
|
return {
|
|
4496
5165
|
change,
|
|
4497
|
-
about: change.about === void 0 ? void 0 : ids(change.about, `${where}.about`)
|
|
5166
|
+
about: change.about === void 0 ? void 0 : ids(change.about, `${where}.about`),
|
|
5167
|
+
evidenceFor: evidence(change.evidenceFor, current?.characterId, `${where}.evidence_for`),
|
|
5168
|
+
evidenceAgainst: evidence(change.evidenceAgainst, current?.characterId, `${where}.evidence_against`)
|
|
4498
5169
|
};
|
|
4499
5170
|
});
|
|
4500
5171
|
for (const id of list(edit.beliefRetractions)) if (target.memories.get(id) === void 0) problems.push(`belief_retractions: there is no belief ${id}`);
|
|
@@ -4769,23 +5440,30 @@ function applyChanges(target, edit) {
|
|
|
4769
5440
|
learnEventEntities(target.events.get(eventId), perception.characterId);
|
|
4770
5441
|
result.writes += 1;
|
|
4771
5442
|
}
|
|
4772
|
-
for (const { belief, character, about } of beliefs) {
|
|
4773
|
-
|
|
5443
|
+
for (const { belief, character, about, evidenceFor, evidenceAgainst } of beliefs) {
|
|
5444
|
+
const added = target.memories.add({
|
|
4774
5445
|
characterId: character,
|
|
4775
5446
|
kind: belief.kind,
|
|
4776
5447
|
text: belief.text.trim(),
|
|
4777
5448
|
about,
|
|
4778
5449
|
importance: belief.importance,
|
|
5450
|
+
...belief.confidence === void 0 ? {} : { confidence: belief.confidence },
|
|
4779
5451
|
turn: edit.turn
|
|
4780
|
-
})
|
|
5452
|
+
});
|
|
5453
|
+
result.beliefIds.push(added.id);
|
|
5454
|
+
for (const id of evidenceFor) target.memories.addEvidence(added.id, id, "supports", edit.turn);
|
|
5455
|
+
for (const id of evidenceAgainst) target.memories.addEvidence(added.id, id, "contradicts", edit.turn);
|
|
4781
5456
|
result.writes += 1;
|
|
4782
5457
|
}
|
|
4783
|
-
for (const { change, about } of beliefEdits) {
|
|
5458
|
+
for (const { change, about, evidenceFor, evidenceAgainst } of beliefEdits) {
|
|
4784
5459
|
target.memories.update(change.id, {
|
|
4785
5460
|
...change.text === void 0 ? {} : { text: change.text.trim() },
|
|
4786
5461
|
...about === void 0 ? {} : { about },
|
|
4787
|
-
...change.importance === void 0 ? {} : { importance: change.importance }
|
|
5462
|
+
...change.importance === void 0 ? {} : { importance: change.importance },
|
|
5463
|
+
...change.confidence === void 0 ? {} : { confidence: change.confidence }
|
|
4788
5464
|
});
|
|
5465
|
+
for (const id of evidenceFor) target.memories.addEvidence(change.id, id, "supports", edit.turn);
|
|
5466
|
+
for (const id of evidenceAgainst) target.memories.addEvidence(change.id, id, "contradicts", edit.turn);
|
|
4789
5467
|
result.writes += 1;
|
|
4790
5468
|
}
|
|
4791
5469
|
for (const id of list(edit.beliefRetractions)) {
|
|
@@ -5211,6 +5889,139 @@ function lapseNotes(target, turn, kept) {
|
|
|
5211
5889
|
return lapsed;
|
|
5212
5890
|
}
|
|
5213
5891
|
//#endregion
|
|
5892
|
+
//#region src/records/deferred.ts
|
|
5893
|
+
/** The words cannot be written as the story stands: something they name is not there. */
|
|
5894
|
+
var UnwritableError = class extends Error {};
|
|
5895
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
5896
|
+
function resolve$1(target, placeholder, clock, self) {
|
|
5897
|
+
const key = (index) => placeholder.args[index] === "@self" ? self ?? "@self" : placeholder.args[index];
|
|
5898
|
+
const character = (wanted) => {
|
|
5899
|
+
const found = target.characters.list().find((entry) => characterKey(entry) === wanted);
|
|
5900
|
+
if (found === void 0) throw new UnwritableError(`${placeholder.raw}: there is no character "${wanted}"`);
|
|
5901
|
+
return found;
|
|
5902
|
+
};
|
|
5903
|
+
const place = (wanted) => {
|
|
5904
|
+
const found = target.locations.list().find((entry) => locationKey(entry) === wanted);
|
|
5905
|
+
if (found === void 0) throw new UnwritableError(`${placeholder.raw}: there is no place "${wanted}"`);
|
|
5906
|
+
return found;
|
|
5907
|
+
};
|
|
5908
|
+
const item = (wanted) => {
|
|
5909
|
+
const found = target.items.list().find((entry) => itemKey(entry) === wanted);
|
|
5910
|
+
if (found === void 0) throw new UnwritableError(`${placeholder.raw}: there is no item "${wanted}"`);
|
|
5911
|
+
return found;
|
|
5912
|
+
};
|
|
5913
|
+
switch (placeholder.kind) {
|
|
5914
|
+
case "character": return character(key(0)).name;
|
|
5915
|
+
case "place": return place(key(0)).name;
|
|
5916
|
+
case "item": return item(key(0)).name;
|
|
5917
|
+
case "where": {
|
|
5918
|
+
const who = character(key(0));
|
|
5919
|
+
const at = target.placements.get("character", who.id);
|
|
5920
|
+
const found = at === void 0 ? void 0 : target.locations.get(at.locationId);
|
|
5921
|
+
if (found === void 0) throw new UnwritableError(`${placeholder.raw}: ${who.name} is nowhere in particular`);
|
|
5922
|
+
return found.name;
|
|
5923
|
+
}
|
|
5924
|
+
case "holder": {
|
|
5925
|
+
const thing = item(key(0));
|
|
5926
|
+
const where = target.items.where(thing.id);
|
|
5927
|
+
const holder = where.characterId !== void 0 ? target.characters.get(where.characterId)?.name : where.locationId !== void 0 ? target.locations.get(where.locationId)?.name : void 0;
|
|
5928
|
+
if (holder === void 0) throw new UnwritableError(`${placeholder.raw}: ${thing.name} is nowhere that is known`);
|
|
5929
|
+
return holder;
|
|
5930
|
+
}
|
|
5931
|
+
case "value": {
|
|
5932
|
+
const def = target.variables.def(placeholder.args[0]);
|
|
5933
|
+
if (def === void 0) throw new UnwritableError(`${placeholder.raw}: there is no variable "${placeholder.args[0]}"`);
|
|
5934
|
+
const subject = def.subject === "world" ? { id: 0 } : placeholder.args[1] === void 0 ? void 0 : subjectOf(target, def.subject, key(1));
|
|
5935
|
+
if (subject === void 0) throw new UnwritableError(`${placeholder.raw}: ${def.subject === "world" ? "a world variable has no subject" : `give the ${def.subject} it is about`}`);
|
|
5936
|
+
return describeValue(def, target.variables.value(def.key, def.subject, subject.id));
|
|
5937
|
+
}
|
|
5938
|
+
case "time": {
|
|
5939
|
+
const local = localTime(clock.now, clock.zone);
|
|
5940
|
+
return `${pad(local.hour)}:${pad(local.minute)}`;
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
/** Words as the story stands now. @throws {UnwritableError} when something they name is not there. */
|
|
5945
|
+
function renderWords(target, text, clock, self) {
|
|
5946
|
+
const { found } = placeholdersOf(text);
|
|
5947
|
+
let at = 0;
|
|
5948
|
+
let out = "";
|
|
5949
|
+
for (const placeholder of found) {
|
|
5950
|
+
const start = text.indexOf(placeholder.raw, at);
|
|
5951
|
+
out += text.slice(at, start) + resolve$1(target, placeholder, clock, self);
|
|
5952
|
+
at = start + placeholder.raw.length;
|
|
5953
|
+
}
|
|
5954
|
+
return out + text.slice(at);
|
|
5955
|
+
}
|
|
5956
|
+
/** A condition with `@self` taken as a character. */
|
|
5957
|
+
function bindSelf(condition, self) {
|
|
5958
|
+
const bound = (value) => value === "@self" ? self : value;
|
|
5959
|
+
switch (condition.type) {
|
|
5960
|
+
case "place": return {
|
|
5961
|
+
...condition,
|
|
5962
|
+
character: bound(condition.character)
|
|
5963
|
+
};
|
|
5964
|
+
case "variable": return condition.subject === void 0 ? condition : {
|
|
5965
|
+
...condition,
|
|
5966
|
+
subject: bound(condition.subject)
|
|
5967
|
+
};
|
|
5968
|
+
case "item": return {
|
|
5969
|
+
...condition,
|
|
5970
|
+
item: bound(condition.item),
|
|
5971
|
+
...condition.heldBy === void 0 ? {} : { heldBy: bound(condition.heldBy) }
|
|
5972
|
+
};
|
|
5973
|
+
case "all":
|
|
5974
|
+
case "any": return {
|
|
5975
|
+
...condition,
|
|
5976
|
+
conditions: condition.conditions.map((part) => bindSelf(part, self))
|
|
5977
|
+
};
|
|
5978
|
+
case "not": return {
|
|
5979
|
+
...condition,
|
|
5980
|
+
condition: bindSelf(condition.condition, self)
|
|
5981
|
+
};
|
|
5982
|
+
default: return condition;
|
|
5983
|
+
}
|
|
5984
|
+
}
|
|
5985
|
+
/** The effect of a trigger that a note was made from: the beat or cue whose words it carries. */
|
|
5986
|
+
function noteEffect(trigger, note) {
|
|
5987
|
+
return trigger?.effects.find((effect) => effect.type === note.kind && effect.text === note.text);
|
|
5988
|
+
}
|
|
5989
|
+
/**
|
|
5990
|
+
* Decides, at the moment notes are about to be told, what is still to be told and in what words. A note whose effect says something must
|
|
5991
|
+
* still hold and does not hold any more is withdrawn; so is one whose words name something that is not there. Nothing is changed.
|
|
5992
|
+
*/
|
|
5993
|
+
function deliverNotes(target, notes, clock) {
|
|
5994
|
+
const delivery = {
|
|
5995
|
+
shown: [],
|
|
5996
|
+
withdrawn: []
|
|
5997
|
+
};
|
|
5998
|
+
for (const note of notes) {
|
|
5999
|
+
const effect = noteEffect(target.triggers.get(note.triggerId), note);
|
|
6000
|
+
const self = note.kind === "cue" && note.characterIds[0] !== void 0 ? target.characters.list().find((entry) => entry.id === note.characterIds[0]) : void 0;
|
|
6001
|
+
const who = self === void 0 ? void 0 : characterKey(self);
|
|
6002
|
+
if (effect?.while !== void 0 && !conditionHolds(target, who === void 0 ? effect.while : bindSelf(effect.while, who), clock)) {
|
|
6003
|
+
delivery.withdrawn.push({
|
|
6004
|
+
note,
|
|
6005
|
+
reason: "what it was waiting on no longer holds"
|
|
6006
|
+
});
|
|
6007
|
+
continue;
|
|
6008
|
+
}
|
|
6009
|
+
try {
|
|
6010
|
+
delivery.shown.push({
|
|
6011
|
+
note,
|
|
6012
|
+
text: renderWords(target, note.text, clock, who)
|
|
6013
|
+
});
|
|
6014
|
+
} catch (error) {
|
|
6015
|
+
if (!(error instanceof UnwritableError)) throw error;
|
|
6016
|
+
delivery.withdrawn.push({
|
|
6017
|
+
note,
|
|
6018
|
+
reason: error.message
|
|
6019
|
+
});
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
return delivery;
|
|
6023
|
+
}
|
|
6024
|
+
//#endregion
|
|
5214
6025
|
//#region src/memory/recall.ts
|
|
5215
6026
|
const DAY$1 = 864e5;
|
|
5216
6027
|
const DEFAULT_MEMORY_POLICY = {
|
|
@@ -5218,7 +6029,8 @@ const DEFAULT_MEMORY_POLICY = {
|
|
|
5218
6029
|
recall: 12,
|
|
5219
6030
|
overRetrieve: 4,
|
|
5220
6031
|
associate: 2,
|
|
5221
|
-
diversity: .3
|
|
6032
|
+
diversity: .3,
|
|
6033
|
+
foldedFade: .5
|
|
5222
6034
|
};
|
|
5223
6035
|
/** Story days for the memory of an unimportant event to halve; each step of importance doubles it. */
|
|
5224
6036
|
const HALF_LIFE_DAYS = 6;
|
|
@@ -5307,7 +6119,7 @@ function describeEvent({ perception, event }, names, at) {
|
|
|
5307
6119
|
vague: about === "" ? "something happened, but you cannot say what" : `something to do with ${about}`
|
|
5308
6120
|
};
|
|
5309
6121
|
}
|
|
5310
|
-
function describeBelief(memory, at) {
|
|
6122
|
+
function describeBelief(memory, at, disputed) {
|
|
5311
6123
|
return {
|
|
5312
6124
|
turn: memory.turn,
|
|
5313
6125
|
...at === void 0 ? {} : { at },
|
|
@@ -5315,6 +6127,8 @@ function describeBelief(memory, at) {
|
|
|
5315
6127
|
kind: "memory",
|
|
5316
6128
|
id: memory.id
|
|
5317
6129
|
},
|
|
6130
|
+
confidence: memory.confidence,
|
|
6131
|
+
...disputed ? { disputed: true } : {},
|
|
5318
6132
|
clear: memory.text,
|
|
5319
6133
|
faded: first(memory.text),
|
|
5320
6134
|
vague: first(memory.text)
|
|
@@ -5349,7 +6163,9 @@ const DENSE_RANGE = .5;
|
|
|
5349
6163
|
const denseScore = (similarity) => Math.min(1, Math.max(0, (similarity - DENSE_FLOOR) / DENSE_RANGE));
|
|
5350
6164
|
/** Being where something happened calls it to mind, about as much as sharing two words with it. */
|
|
5351
6165
|
const PLACE_CUE = 2;
|
|
5352
|
-
function gather(source, { characterId, characters, situation, locationId, about }) {
|
|
6166
|
+
function gather(source, { characterId, characters, situation, locationId, about, policy }) {
|
|
6167
|
+
const evidence = source.memories.evidenceIndex?.(characterId);
|
|
6168
|
+
const folding = Math.min(1, Math.max(.05, policy?.foldedFade ?? 1));
|
|
5353
6169
|
const names = new Map(characters.map((character) => [character.id, character.name]));
|
|
5354
6170
|
const peers = mentioned(situation, characters);
|
|
5355
6171
|
peers.delete(characterId);
|
|
@@ -5389,7 +6205,8 @@ function gather(source, { characterId, characters, situation, locationId, about
|
|
|
5389
6205
|
ageDays: ageDays(at),
|
|
5390
6206
|
recalls: seen.perception.recalls,
|
|
5391
6207
|
seed: seen.perception.id,
|
|
5392
|
-
durability: 1
|
|
6208
|
+
durability: evidence?.folded.has(seen.perception.id) === true ? folding : 1,
|
|
6209
|
+
...evidence?.folded.has(seen.perception.id) === true ? { covered: evidence.folded.get(seen.perception.id) } : {}
|
|
5393
6210
|
};
|
|
5394
6211
|
}), ...source.memories.list(characterId).filter((belief) => concerns(belief.about)).map((belief) => {
|
|
5395
6212
|
const at = atTurn(belief.turn);
|
|
@@ -5404,7 +6221,7 @@ function gather(source, { characterId, characters, situation, locationId, about
|
|
|
5404
6221
|
people: belief.about.filter((id) => id !== characterId),
|
|
5405
6222
|
tags: []
|
|
5406
6223
|
},
|
|
5407
|
-
recalled: describeBelief(belief, at),
|
|
6224
|
+
recalled: describeBelief(belief, at, evidence?.disputed.has(belief.id) === true),
|
|
5408
6225
|
cues: cue,
|
|
5409
6226
|
relevance: cue.people + cue.words,
|
|
5410
6227
|
importance: belief.importance,
|
|
@@ -5564,6 +6381,28 @@ function diversified(ranked, count, diversity) {
|
|
|
5564
6381
|
}
|
|
5565
6382
|
return chosen;
|
|
5566
6383
|
}
|
|
6384
|
+
/** How sure the character is of a belief, and whether something since speaks against it. */
|
|
6385
|
+
function beliefOf(candidate) {
|
|
6386
|
+
const { confidence, disputed } = candidate.recalled;
|
|
6387
|
+
return {
|
|
6388
|
+
...confidence === void 0 ? {} : { confidence },
|
|
6389
|
+
...disputed === void 0 ? {} : { disputed }
|
|
6390
|
+
};
|
|
6391
|
+
}
|
|
6392
|
+
/**
|
|
6393
|
+
* Chooses as {@link diversified} does, but does not tell what the character took in when a belief resting on it is told as well: the belief says it,
|
|
6394
|
+
* and the place goes to the next best.
|
|
6395
|
+
*/
|
|
6396
|
+
function chooseUncovered(standing, count, diversity) {
|
|
6397
|
+
let pool = standing;
|
|
6398
|
+
for (;;) {
|
|
6399
|
+
const chosen = diversified(pool, count, diversity);
|
|
6400
|
+
const told = new Set(chosen.filter((entry) => entry.candidate.recalled.source.kind === "memory").map((entry) => entry.candidate.recalled.source.id));
|
|
6401
|
+
const repeated = chosen.filter((entry) => entry.candidate.covered?.some((id) => told.has(id)) === true);
|
|
6402
|
+
if (repeated.length === 0) return chosen;
|
|
6403
|
+
pool = pool.filter((entry) => !repeated.includes(entry));
|
|
6404
|
+
}
|
|
6405
|
+
}
|
|
5567
6406
|
/**
|
|
5568
6407
|
* The last stage: choose the `policy.recall` best of the pool, spread by `policy.diversity`. With a reranker's judgement each
|
|
5569
6408
|
* candidate's relevance is mostly that (a candidate it left out counts as 0), and the rest is what
|
|
@@ -5592,13 +6431,14 @@ function finishRecall(pool, rerank) {
|
|
|
5592
6431
|
why
|
|
5593
6432
|
}];
|
|
5594
6433
|
}).sort((a, b) => b.score - a.score || b.candidate.recalled.turn - a.candidate.recalled.turn);
|
|
5595
|
-
const chosen =
|
|
6434
|
+
const chosen = chooseUncovered(standing, policy.recall, policy.diversity ?? 0);
|
|
5596
6435
|
const result = chosen.map(({ candidate, clarity, text, why }) => ({
|
|
5597
6436
|
clarity,
|
|
5598
6437
|
text,
|
|
5599
6438
|
turn: candidate.recalled.turn,
|
|
5600
6439
|
...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
|
|
5601
6440
|
source: candidate.recalled.source,
|
|
6441
|
+
...beliefOf(candidate),
|
|
5602
6442
|
...why === void 0 ? {} : { why }
|
|
5603
6443
|
}));
|
|
5604
6444
|
const anchors = chosen.filter((entry) => entry.relevance >= CUE_RELEVANCE);
|
|
@@ -5620,6 +6460,7 @@ function finishRecall(pool, rerank) {
|
|
|
5620
6460
|
turn: candidate.recalled.turn,
|
|
5621
6461
|
...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
|
|
5622
6462
|
source: candidate.recalled.source,
|
|
6463
|
+
...beliefOf(candidate),
|
|
5623
6464
|
linked: true,
|
|
5624
6465
|
...pool.query.explain === true ? { why: {
|
|
5625
6466
|
cues: candidate.cues,
|
|
@@ -5769,15 +6610,21 @@ function readRecords(target, scope = {}) {
|
|
|
5769
6610
|
gist: entry.gist
|
|
5770
6611
|
}))
|
|
5771
6612
|
})),
|
|
5772
|
-
beliefs: characters.flatMap((character) => target.memories.list(character.id).filter((belief) => inScope(belief.turn)).map((belief) =>
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
6613
|
+
beliefs: characters.flatMap((character) => target.memories.list(character.id).filter((belief) => inScope(belief.turn)).map((belief) => {
|
|
6614
|
+
const evidence = target.memories.evidenceOf(belief.id);
|
|
6615
|
+
return {
|
|
6616
|
+
id: belief.id,
|
|
6617
|
+
character: characterKey(character),
|
|
6618
|
+
kind: belief.kind,
|
|
6619
|
+
text: belief.text,
|
|
6620
|
+
about: belief.about.map(keyOf),
|
|
6621
|
+
importance: belief.importance,
|
|
6622
|
+
confidence: belief.confidence,
|
|
6623
|
+
evidence_for: evidence.filter((entry) => entry.relation === "supports").map((entry) => entry.perceptionId),
|
|
6624
|
+
evidence_against: evidence.filter((entry) => entry.relation === "contradicts").map((entry) => entry.perceptionId),
|
|
6625
|
+
turn: belief.turn
|
|
6626
|
+
};
|
|
6627
|
+
})),
|
|
5781
6628
|
open_threads: target.threads.list("open").map((thread) => ({
|
|
5782
6629
|
id: thread.id,
|
|
5783
6630
|
text: thread.text,
|
|
@@ -6138,6 +6985,7 @@ const toMemory = (row) => ({
|
|
|
6138
6985
|
text: row.text,
|
|
6139
6986
|
about: parseNumberList(row.about, "memories.about"),
|
|
6140
6987
|
importance: row.importance,
|
|
6988
|
+
confidence: row.confidence,
|
|
6141
6989
|
turn: row.turn,
|
|
6142
6990
|
recalls: row.recalls,
|
|
6143
6991
|
...row.last_recalled_turn === null ? {} : { lastRecalledTurn: row.last_recalled_turn },
|
|
@@ -6154,12 +7002,13 @@ var SqliteMemoryRepository = class {
|
|
|
6154
7002
|
}
|
|
6155
7003
|
add(memory) {
|
|
6156
7004
|
return guard(() => {
|
|
6157
|
-
const { lastInsertRowid } = this.db.run("INSERT INTO memories (character_id, kind, text, about, importance, turn, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [
|
|
7005
|
+
const { lastInsertRowid } = this.db.run("INSERT INTO memories (character_id, kind, text, about, importance, confidence, turn, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
6158
7006
|
memory.characterId,
|
|
6159
7007
|
memory.kind,
|
|
6160
7008
|
memory.text,
|
|
6161
7009
|
toJson(memory.about),
|
|
6162
7010
|
memory.importance,
|
|
7011
|
+
memory.confidence ?? "likely",
|
|
6163
7012
|
memory.turn,
|
|
6164
7013
|
this.clock().toISOString()
|
|
6165
7014
|
]);
|
|
@@ -6183,10 +7032,11 @@ var SqliteMemoryRepository = class {
|
|
|
6183
7032
|
update(id, patch) {
|
|
6184
7033
|
return guard(() => {
|
|
6185
7034
|
const current = this.require(id);
|
|
6186
|
-
this.db.run("UPDATE memories SET text = ?, about = ?, importance = ? WHERE id = ?", [
|
|
7035
|
+
this.db.run("UPDATE memories SET text = ?, about = ?, importance = ?, confidence = ? WHERE id = ?", [
|
|
6187
7036
|
patch.text ?? current.text,
|
|
6188
7037
|
toJson(patch.about ?? current.about),
|
|
6189
7038
|
patch.importance ?? current.importance,
|
|
7039
|
+
patch.confidence ?? current.confidence,
|
|
6190
7040
|
id
|
|
6191
7041
|
]);
|
|
6192
7042
|
const changed = this.require(id);
|
|
@@ -6208,6 +7058,36 @@ var SqliteMemoryRepository = class {
|
|
|
6208
7058
|
this.index.remove("memory", id);
|
|
6209
7059
|
return this.db.run("DELETE FROM memories WHERE id = ?", [id]).changes > 0;
|
|
6210
7060
|
}
|
|
7061
|
+
addEvidence(memoryId, perceptionId, relation, turn) {
|
|
7062
|
+
this.require(memoryId);
|
|
7063
|
+
guard(() => this.db.run("INSERT INTO memory_evidence (memory_id, perception_id, relation, turn) VALUES (?, ?, ?, ?) ON CONFLICT (memory_id, perception_id) DO UPDATE SET relation = excluded.relation, turn = excluded.turn", [
|
|
7064
|
+
memoryId,
|
|
7065
|
+
perceptionId,
|
|
7066
|
+
relation,
|
|
7067
|
+
turn
|
|
7068
|
+
]));
|
|
7069
|
+
}
|
|
7070
|
+
evidenceOf(memoryId) {
|
|
7071
|
+
return this.db.all("SELECT * FROM memory_evidence WHERE memory_id = ? ORDER BY perception_id", [memoryId]).map((row) => ({
|
|
7072
|
+
memoryId: row.memory_id,
|
|
7073
|
+
perceptionId: row.perception_id,
|
|
7074
|
+
relation: row.relation,
|
|
7075
|
+
turn: row.turn
|
|
7076
|
+
}));
|
|
7077
|
+
}
|
|
7078
|
+
evidenceIndex(characterId) {
|
|
7079
|
+
const rows = this.db.all("SELECT e.memory_id, e.perception_id, e.relation FROM memory_evidence e JOIN memories m ON m.id = e.memory_id WHERE m.character_id = ? ORDER BY e.memory_id", [characterId]);
|
|
7080
|
+
const folded = /* @__PURE__ */ new Map();
|
|
7081
|
+
const disputed = /* @__PURE__ */ new Set();
|
|
7082
|
+
for (const row of rows) {
|
|
7083
|
+
folded.set(row.perception_id, [...folded.get(row.perception_id) ?? [], row.memory_id]);
|
|
7084
|
+
if (row.relation === "contradicts") disputed.add(row.memory_id);
|
|
7085
|
+
}
|
|
7086
|
+
return {
|
|
7087
|
+
folded,
|
|
7088
|
+
disputed
|
|
7089
|
+
};
|
|
7090
|
+
}
|
|
6211
7091
|
require(id) {
|
|
6212
7092
|
const memory = this.get(id);
|
|
6213
7093
|
if (memory === void 0) throw new NotFoundError(`memory ${id} does not exist`);
|
|
@@ -8365,6 +9245,21 @@ const MIGRATIONS = [
|
|
|
8365
9245
|
INSERT INTO entity_knowledge SELECT * FROM old_entity_knowledge;
|
|
8366
9246
|
DROP TABLE old_entity_knowledge;
|
|
8367
9247
|
CREATE INDEX entity_knowledge_subject ON entity_knowledge (subject_kind, subject_id);
|
|
9248
|
+
`
|
|
9249
|
+
},
|
|
9250
|
+
{
|
|
9251
|
+
version: 7,
|
|
9252
|
+
sql: `
|
|
9253
|
+
ALTER TABLE memories ADD COLUMN confidence TEXT NOT NULL DEFAULT 'likely' CHECK (confidence IN ('doubtful', 'likely', 'sure'));
|
|
9254
|
+
CREATE TABLE memory_evidence (
|
|
9255
|
+
memory_id INTEGER NOT NULL REFERENCES memories (id) ON DELETE CASCADE,
|
|
9256
|
+
perception_id INTEGER NOT NULL REFERENCES perceptions (id) ON DELETE CASCADE,
|
|
9257
|
+
relation TEXT NOT NULL CHECK (relation IN ('supports', 'contradicts')),
|
|
9258
|
+
turn INTEGER NOT NULL,
|
|
9259
|
+
PRIMARY KEY (memory_id, perception_id)
|
|
9260
|
+
) STRICT;
|
|
9261
|
+
CREATE INDEX memory_evidence_perception ON memory_evidence (perception_id);
|
|
9262
|
+
ALTER TABLE routine_windows ADD COLUMN filters TEXT NOT NULL DEFAULT '[]';
|
|
8368
9263
|
`
|
|
8369
9264
|
}
|
|
8370
9265
|
];
|
|
@@ -9239,6 +10134,9 @@ var SqliteSessionStore = class {
|
|
|
9239
10134
|
runTriggers(options) {
|
|
9240
10135
|
return runTriggers(this, options);
|
|
9241
10136
|
}
|
|
10137
|
+
deliverNotes(notes, clock) {
|
|
10138
|
+
return deliverNotes(this, notes, clock);
|
|
10139
|
+
}
|
|
9242
10140
|
lapseNotes(turn, kept) {
|
|
9243
10141
|
return this.transaction(() => lapseNotes(this, turn, kept));
|
|
9244
10142
|
}
|
|
@@ -9739,8 +10637,9 @@ function lintWorld(target) {
|
|
|
9739
10637
|
//#region src/records/agenda.ts
|
|
9740
10638
|
/** An engagement still pending this long after it was due (or over) is overdue. */
|
|
9741
10639
|
const ENGAGEMENT_GRACE = 30 * MINUTE;
|
|
9742
|
-
function occurrenceOn(window, day, zone) {
|
|
10640
|
+
function occurrenceOn(window, day, zone, calendar) {
|
|
9743
10641
|
if (window.days.length > 0 && !window.days.includes(day.weekday)) return void 0;
|
|
10642
|
+
if (window.on.length > 0 && !dayMatches(window.on, day, calendar)) return void 0;
|
|
9744
10643
|
const startsAt = zonedInstant({
|
|
9745
10644
|
...day,
|
|
9746
10645
|
hour: Math.floor(window.start / 60),
|
|
@@ -9757,15 +10656,15 @@ function occurrenceOn(window, day, zone) {
|
|
|
9757
10656
|
};
|
|
9758
10657
|
}
|
|
9759
10658
|
/** The window that holds at an instant (the first of the list that does), with the stretch of this occurrence of it. */
|
|
9760
|
-
function occurrenceAt(windows, now, zone) {
|
|
10659
|
+
function occurrenceAt(windows, now, zone, calendar = NO_CALENDAR) {
|
|
9761
10660
|
const days = [addLocalDays(now, zone, 0), addLocalDays(now, zone, -1)];
|
|
9762
10661
|
for (const window of windows) for (const day of days) {
|
|
9763
|
-
const occurrence = occurrenceOn(window, day, zone);
|
|
10662
|
+
const occurrence = occurrenceOn(window, day, zone, calendar);
|
|
9764
10663
|
if (occurrence !== void 0 && occurrence.startsAt <= now && now < occurrence.endsAt) return occurrence;
|
|
9765
10664
|
}
|
|
9766
10665
|
}
|
|
9767
10666
|
/** The window that starts next after an instant, within `horizon`. */
|
|
9768
|
-
function nextOccurrence(windows, now, zone, horizon) {
|
|
10667
|
+
function nextOccurrence(windows, now, zone, horizon, calendar = NO_CALENDAR) {
|
|
9769
10668
|
let best;
|
|
9770
10669
|
for (const offset of [
|
|
9771
10670
|
0,
|
|
@@ -9774,7 +10673,7 @@ function nextOccurrence(windows, now, zone, horizon) {
|
|
|
9774
10673
|
]) {
|
|
9775
10674
|
const day = addLocalDays(now, zone, offset);
|
|
9776
10675
|
for (const window of windows) {
|
|
9777
|
-
const occurrence = occurrenceOn(window, day, zone);
|
|
10676
|
+
const occurrence = occurrenceOn(window, day, zone, calendar);
|
|
9778
10677
|
if (occurrence === void 0 || occurrence.startsAt <= now || occurrence.startsAt > now + horizon) continue;
|
|
9779
10678
|
if (best === void 0 || occurrence.startsAt < best.startsAt) best = occurrence;
|
|
9780
10679
|
}
|
|
@@ -9807,8 +10706,9 @@ const DEFAULTS = {
|
|
|
9807
10706
|
function agendaAt(source, characterId, now, zone, options = {}) {
|
|
9808
10707
|
const horizon = options.horizon ?? DEFAULTS.horizon;
|
|
9809
10708
|
const windows = source.routines.list(characterId);
|
|
9810
|
-
const
|
|
9811
|
-
const
|
|
10709
|
+
const calendar = source.metadata === void 0 ? NO_CALENDAR : readCalendar(source.metadata);
|
|
10710
|
+
const current = occurrenceAt(windows, now, zone, calendar);
|
|
10711
|
+
const next = nextOccurrence(windows, now, zone, horizon, calendar);
|
|
9812
10712
|
const engagements = source.engagements.list({
|
|
9813
10713
|
status: "pending",
|
|
9814
10714
|
participant: characterId,
|
|
@@ -11324,6 +12224,7 @@ var RoleplayState = class extends Service {
|
|
|
11324
12224
|
edit: (edit, options) => store.edit(edit, options),
|
|
11325
12225
|
runTriggers: (options) => store.runTriggers(options),
|
|
11326
12226
|
lapseNotes: (turn, kept) => store.lapseNotes(turn, kept),
|
|
12227
|
+
deliverNotes: (notes, clock) => store.deliverNotes(notes, clock),
|
|
11327
12228
|
records: (scope) => store.records(scope)
|
|
11328
12229
|
};
|
|
11329
12230
|
this.sessions.set(key, {
|
|
@@ -11560,6 +12461,6 @@ function createCharacterTools(link, characterId) {
|
|
|
11560
12461
|
];
|
|
11561
12462
|
}
|
|
11562
12463
|
//#endregion
|
|
11563
|
-
export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_KINDS, MIGRATION_BACKUP_FOLDER, MINUTE, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SECOND, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, clarityAt, clockTime, conditionHolds, createCharacterTools, createSession, createSessionStore, createTools, dayPart, deleteSession, denseScore, describeDuration, describeHolder, describeInstant, describeValue, finishRecall, fuseScores, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, judgedStatements, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, migrationBackupPath, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, occurrenceAt, openSession, openSessionStore, overlap, parseInstant, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, retention, runTriggers, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, triggerReferenceProblems, valueIssue, zonedInstant };
|
|
12464
|
+
export { ACCURACIES, CHARACTER_CONTROLS, CHARACTER_KINDS, CHARACTER_ROLES, CHARACTER_TIERS, CLOCK_MODES, COMPARISONS, CONSOLIDATION_KINDS, CONTAINER_STATES, Config, ConflictError, CorruptDataError, DAY, DAY_PARTS, DEFAULT_CONFIDENCE, DEFAULT_MEMORY_POLICY, DEFAULT_SLOTS, EMOTION_VARIABLES, ENGAGEMENT_GRACE, ENGAGEMENT_STATUSES, EVIDENCE_RELATIONS, EXIT_STATES, HOUR, IMPORTANCE_MAX, IMPORTANCE_MIN, INTENT_HORIZONS, INTENT_STATUSES, INTENT_TYPES, ITEM_ACTIONS, InvalidEditError, InvalidMoveError, InvalidValueError, KNOWLEDGE_KINDS, LANGUAGES, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_LOCATION_FEATURES, MAX_ROUTINE_WINDOWS, MAX_SHOWN, MAX_TRIGGERS, MAX_VARIABLES, MEMBERSHIP_STATUSES, MEMORY_CONFIDENCES, MEMORY_KINDS, MIGRATION_BACKUP_FOLDER, MINUTE, MONTH_NAMES, NO_CALENDAR, NotFoundError, PERCEPTION_MODES, PLACEABLE_KINDS, PLOT_NUDGE_MODES, PLOT_STATUSES, RECALL_ITEM_KINDS, RoleplayState, LATEST_VERSION as SCHEMA_VERSION, SEASONS, SECOND, SELF, SESSION_DATABASE_FILE, SLOT_ID, SUMMARY_LEVELS, SchemaError, StateError, TEMPLATE_KINDS, THREAD_STATUSES, TIME_CHANGE_KINDS, TRIGGER_NOTE_KINDS, TRIGGER_STATUSES, UnwritableError, VARIABLE_KEY, VARIABLE_KINDS, VARIABLE_SUBJECTS, VARIABLE_VISIBILITIES, VISUAL_SUBJECT_KINDS, WEEKDAY_NAMES, addLocalDays, agendaAt, announcements, apply, applyItemPlan, applyRecordsEdit, audienceIds, catchUpPlan, changeItem, changeVariable, characterKey, clarityAt, clockTime, conditionHolds, createCharacterTools, createSession, createSessionStore, createTools, dayMatches, dayPart, dayTokenProblems, deleteSession, deliverNotes, denseScore, describeDuration, describeHolder, describeInstant, describeValue, finishRecall, fuseScores, holidaysOn, inAudience, inWindow, isSessionId, isTimeZone, itemKey, jitter, judgedStatements, languageOf, lapseNotes, lintWorld, listSessions, localIso, localTime, locationKey, mentioned, migrationBackupPath, moveCharacter, name, nextOccurrence, nextTimeOfDay, nodeSqliteDriver, occurrenceAt, openSession, openSessionStore, overlap, parseDayToken, parseInstant, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, placeholdersOf, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readCalendar, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, renderWords, retention, runTriggers, seasonOf, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, translated, translation, translator, triggerReferenceProblems, valueIssue, writeCalendar, zonedInstant };
|
|
11564
12465
|
|
|
11565
12466
|
//# sourceMappingURL=index.js.map
|