@dsh-agentic-roleplay/state 0.3.0 → 0.3.2

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/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { DatabaseSync } from "node:sqlite";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { Service } from "@deepseek-ai/cordis";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { randomUUID } from "node:crypto";
6
+ import { randomInt, randomUUID } from "node:crypto";
7
7
  import { copyFileSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
8
8
  import { defineTool } from "@deepseek-ai/dsh-tools";
9
9
  //#region ../dsh-roleplay-model/src/character.ts
@@ -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"];
@@ -358,6 +367,31 @@ const TRIGGER_NOTE_KINDS = ["beat", "cue"];
358
367
  /** At most this many triggers in a story. */
359
368
  const MAX_TRIGGERS = 200;
360
369
  //#endregion
370
+ //#region ../dsh-roleplay-model/src/dice.ts
371
+ /** What a band of a check makes of an attempt; the same three results a round's outcome has. */
372
+ const DICE_RESULTS = [
373
+ "succeeded",
374
+ "partial",
375
+ "failed"
376
+ ];
377
+ /** How a roll is read: by its total, or by how many of the dice came up high enough. */
378
+ const DICE_READINGS = ["total", "successes"];
379
+ /**
380
+ * What the value is compared with: `absolute` reads the value itself, `over` the amount by which it beats the target
381
+ * (a difficulty to meet or exceed), `under` the amount by which it stays below the target (a skill to roll under).
382
+ */
383
+ const DICE_COMPARISONS = [
384
+ "absolute",
385
+ "over",
386
+ "under"
387
+ ];
388
+ /** Rolls and checks are limited so that a story cannot grow without bound. */
389
+ const MAX_DICE_CHECKS = 40;
390
+ const MAX_DICE_BANDS = 20;
391
+ const MAX_DICE_PER_ROLL = 100;
392
+ const MAX_DICE_SIDES = 1e3;
393
+ const MAX_DICE_TERMS = 8;
394
+ //#endregion
361
395
  //#region ../dsh-roleplay-model/src/visual.ts
362
396
  const VISUAL_SUBJECT_KINDS = [
363
397
  "character",
@@ -403,6 +437,227 @@ const PLOT_NUDGE_MODES = [
403
437
  "breathing_room"
404
438
  ];
405
439
  //#endregion
440
+ //#region ../dsh-roleplay-model/src/messages-zh.ts
441
+ /** 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. */
442
+ const ZH = {
443
+ ", ": ",",
444
+ "; ": ";",
445
+ ": ": ":",
446
+ ".": "。",
447
+ " and ": "和",
448
+ "doubtful": "存疑",
449
+ "likely": "较有把握",
450
+ "sure": "确信",
451
+ "open": "畅通",
452
+ "locked": "已上锁",
453
+ "blocked": "被堵住",
454
+ "night": "夜里",
455
+ "morning": "上午",
456
+ "afternoon": "下午",
457
+ "evening": "傍晚",
458
+ "spring": "春季",
459
+ "summer": "夏季",
460
+ "autumn": "秋季",
461
+ "winter": "冬季",
462
+ "today is {holidays}": "今天是{holidays}",
463
+ "Summary": "概述",
464
+ "Personality": "性格",
465
+ "Background": "背景",
466
+ "Appearance": "外貌",
467
+ "How you speak": "说话方式",
468
+ "You are {name}.": "你是{name}。",
469
+ "You are also called {names}.": "你也被称作{names}。",
470
+ "age: {value}": "年龄:{value}",
471
+ "gender: {value}": "性别:{value}",
472
+ "pronouns: {value}": "代词:{value}",
473
+ "- {key} ({name}): {use}": "- {key}({name}):{use}",
474
+ "Rolls {dice}.": "掷 {dice}。",
475
+ "Give a target: the difficulty, the number to meet or beat; set it by how hard the attempt is.": "需给出 target:难度,即要达到或超过的数字;按这次尝试的难易来定。",
476
+ "Give a target: the number to stay under; set it by how hard the attempt is.": "需给出 target:要低于的数字;按这次尝试的难易来定。",
477
+ "It adds the character's {variable}.": "它会加上该角色的 {variable}。",
478
+ "Counts the dice that show {hit} or more.": "统计显示 {hit} 或更高的骰子数。",
479
+ "the player's roll": "玩家的掷骰",
480
+ "The player rolled {dice} ({reason}) and got {total}: dice {faces}. This is a fact, decided by the dice: take it into account when you judge the attempt and decide what happens.": "玩家掷了 {dice}({reason}),得到 {total}:骰面 {faces}。这是由骰子决定的事实:在你判断这次尝试、决定发生什么时把它考虑进去。",
481
+ "The checks of this story:": "本故事的检定:",
482
+ "No checks are defined: use roll_dice only.": "没有定义检定:只用 roll_dice。",
483
+ "Situation:": "当前情形:",
484
+ "Now: {time}.": "现在是:{time}。",
485
+ "Where you are:": "你所在之处:",
486
+ "Present:": "在场的人:",
487
+ " (you)": "(你)",
488
+ "What you know is coming:": "你知道即将发生的事:",
489
+ "Just now you notice:": "此刻你注意到:",
490
+ "How you are (what you can tell of yourself):": "你的状态(你自己能感觉到的):",
491
+ "What you can tell of the others here:": "你能看出在场其他人的情况:",
492
+ "What you have on you:": "你身上带着:",
493
+ "Things here: ": "这里有:",
494
+ "Ways out: ": "出路:",
495
+ "Lying about: ": "散落在周围的:",
496
+ "hidden": "隐蔽",
497
+ " holding {things}": ",里面有{things}",
498
+ "; and {count} more": ";另有{count}项",
499
+ "at {feature}": "在{feature}",
500
+ "; and {count} more you can recall if needed": ";另有{count}项,需要时你可以回想起来",
501
+ "What you know exists (this does not tell you where it is now or what state it is in):": "你知道存在的事物(这并不说明它现在在哪里、处于什么状态):",
502
+ "People: ": "人物:",
503
+ "Factions: ": "组织:",
504
+ "Places: ": "地点:",
505
+ "Routes: ": "路径:",
506
+ "Things: ": "物品:",
507
+ "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):": "你的日程与你想达成的事(只是计划,不是命令:依当下情形和你的性格,可以照做、推迟或放弃):",
508
+ "Your day, now: {activity}{place}, until {until}{note}.": "你此刻的日程:{activity}{place},持续到{until}{note}。",
509
+ "Your day, next: {activity}{place}, from {from}.": "你接下来的日程:{activity}{place},{from}开始。",
510
+ " at {place}": ",在{place}",
511
+ " (not something you would drop lightly)": "(你不会轻易放下)",
512
+ ", with {names}": ",与{names}一起",
513
+ "You were to: {title}{place}{with}, at {at}, which has passed and is still open.": "你本应:{title}{place}{with},时间是{at},已经过了,事情仍未了结。",
514
+ "You are due to: {title}{place}{with}, at {at}.": "你约定要:{title}{place}{with},时间是{at}。",
515
+ "What you are after:": "你想达成的事:",
516
+ ", by {deadline}, which has passed": ",限期{deadline},已过",
517
+ ", by {deadline}": ",限期{deadline}",
518
+ " Next: {plan}.": " 下一步:{plan}。",
519
+ "How you stand with people who bear on this moment (your own view, not objective fact):": "你与和此刻相关的人的关系(是你自己的看法,不是客观事实):",
520
+ " (here)": "(在场)",
521
+ " (not here)": "(不在场)",
522
+ "knows them well": "很了解对方",
523
+ "barely knows them": "几乎不了解对方",
524
+ "trusts them": "信任对方",
525
+ "distrusts them": "不信任对方",
526
+ "is fond of them": "喜欢对方",
527
+ "dislikes them": "不喜欢对方",
528
+ "things are strained between them": "彼此关系紧张",
529
+ "Your affiliations (their goals are pressures and commitments, not commands):": "你的所属组织(其目标是压力和承诺,不是命令):",
530
+ "member": "成员",
531
+ " Goals: {goals}.": " 目标:{goals}。",
532
+ "- {name}: {position}; {status}; commitment {commitment}.": "- {name}:{position};{status};投入程度 {commitment}。",
533
+ " Your view: {attitude}.": " 你的看法:{attitude}。",
534
+ "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):": "你记得的、与此相关的事(只限你自己亲身经历的;不要声称记得更多,模糊的地方要保持不确定):",
535
+ "What you remember that bears on this: nothing comes to mind.": "你记得的、与此相关的事:想不起任何事。",
536
+ "some time ago": "一段时间以前",
537
+ "just now": "刚才",
538
+ "1 day ago": "1天前",
539
+ "{amount} days ago": "{amount}天前",
540
+ "1 hour ago": "1小时前",
541
+ "{amount} hours ago": "{amount}小时前",
542
+ "1 minute ago": "1分钟前",
543
+ "{amount} minutes ago": "{amount}分钟前",
544
+ "the details are fuzzy": "细节已经模糊",
545
+ "you barely remember": "你几乎记不清了",
546
+ "you are sure of it": "你对此确信无疑",
547
+ "you doubt it": "你对此存疑",
548
+ "something you have seen since speaks against it": "此后你所见所闻有与之相悖的地方",
549
+ "it comes to mind with the rest": "它和其余的事一起浮现在脑海",
550
+ "Earlier in this scene, before this moment:": "这一幕此刻之前发生的事:",
551
+ "What just happened, and what you are reacting to:": "刚才发生的事,以及你在回应的是什么:",
552
+ "- {name} intended: {intent} ({result}) {summary}": "- {name}打算:{intent}({result}){summary}",
553
+ "- {name} said \"{utterance}\" and intended: {intent} ({result}) {summary}": "- {name}说了“{utterance}”,并打算:{intent}({result}){summary}",
554
+ "Your earlier intent did not work out: {intent}\nYou are trying again, knowing the above. Decide again what you do now.": "你先前的打算没有成功:{intent}\n你带着上面这些再试一次。重新决定你现在做什么。",
555
+ "Character: {name}{summary}": "角色:{name}{summary}",
556
+ "What they believe now:": "他们现在的看法:",
557
+ "(nothing yet)": "(暂时没有)",
558
+ "What they have taken in since:": "他们此后经历的事:",
559
+ "Sort their memory.": "请整理他们的记忆。",
560
+ "not acceptable: {problems}": "不可接受:{problems}",
561
+ "{kind}, importance {importance}, {confidence}{about}{dispute}": "{kind},重要性 {importance},{confidence}{about}{dispute}",
562
+ ", about {names}": ",关于{names}",
563
+ ", something taken in since speaks against it": ",此后经历的事与之相悖",
564
+ "add: at most {max} new beliefs in one pass; merge or choose": "add:一次最多 {max} 条新看法;请合并或取舍",
565
+ "at most {max} changes in one pass": "一次最多 {max} 处修改",
566
+ "{where}: there is no thing it took in numbered {id}": "{where}:没有编号为 {id} 的经历",
567
+ "{where}: confidence is one of {options}": "{where}:confidence 须为 {options} 之一",
568
+ "{where}: unknown kind \"{kind}\"": "{where}:未知的 kind “{kind}”",
569
+ "{where}: say what it believes": "{where}:请写出它相信什么",
570
+ "{where}: at most {max} characters; say it shorter": "{where}:最多 {max} 个字符;请写得更短",
571
+ "{where}: importance runs from {min} to {max}": "{where}:importance 取值为 {min} 到 {max}",
572
+ "{where}: there is no belief numbered {id}": "{where}:没有编号为 {id} 的看法",
573
+ "{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}:一个转瞬即逝的瞬间还不是看法;请以至少两个不同回合的经历为依据,或以一件扭转了故事的事件为依据",
574
+ "{where}: to be sure of something it must rest on things from at least two different turns": "{where}:要对某事确信,必须以至少两个不同回合的经历为依据",
575
+ "{where}: \"{name}\" is not a character; name them as they are named": "{where}:“{name}”不是角色;请使用他们的名字",
576
+ "{where}: belief {id} is already changed in this pass": "{where}:看法 {id} 在这一轮中已经修改过",
577
+ "{where}: say what it took in since that bears on it": "{where}:请写出此后与之相关的经历",
578
+ "{where}: thing {id} came before the belief was formed; cite what came after": "{where}:经历 {id} 发生在看法形成之前;请引用之后发生的事",
579
+ "{where}: say what changes": "{where}:请写出有何改变",
580
+ "{where}: the text must be there and at most {max} characters; leave it out to keep the belief as it is": "{where}:文字不能为空,且最多 {max} 个字符;想保持信念原样就不要写这一项",
581
+ "{where}: the confidence, the importance and the numbers are fields of their own, not part of the text": "{where}:把握程度、重要性和编号是各自独立的字段,不属于文字",
582
+ "{where}: name what speaks against the belief in contradicted_by": "{where}:在 contradicted_by 里写出与该信念相悖的内容的编号",
583
+ "{where}: a belief that something contradicts must be doubted: lower its confidence": "{where}:被事实相悖的看法必须存疑:请降低其 confidence",
584
+ "{where}: to doubt a belief, name what contradicts it in contradicted_by": "{where}:要对看法存疑,请在 contradicted_by 中写明与之相悖的经历",
585
+ "{where}: merge into a belief that stays": "{where}:请合并到保留的看法中",
586
+ "Time: {time}.": "时间:{time}。",
587
+ "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 可以跳过一段时间。",
588
+ "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}的真实时间,其间故事仍在继续:角色们各忙各的事,除非记录另有说明,不要假定任何人还在玩家离开时的位置或状态。",
589
+ "The story's time follows the real clock: it cannot be skipped, and each round takes as long as it really took.": "故事时间跟随真实时钟:无法跳过,每一轮耗时就是它实际花费的时间。",
590
+ "By their routines since you last looked: {moves}.": "按各自的日程,自你上次查看以来:{moves}。",
591
+ "{name} went to {place} ({activity})": "{name}去了{place}({activity})",
592
+ "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}。是否照日程行事由角色自己决定;当下的情形也可能拖住他们。",
593
+ ", at {place}": ",在{place}",
594
+ "\"{title}\" ({with}{place}) was due {when} and is still open": "“{title}”({with}{place})原定{when},仍未了结",
595
+ "\"{title}\" ({with}{place}) is due {when}": "“{title}”({with}{place})定于{when}",
596
+ "Commitments due soon or overdue: {entries}.": "即将到期或已逾期的约定:{entries}。",
597
+ "named in the move": "被这一步行动点到",
598
+ "in the scene": "在场",
599
+ "own goal: {goal}": "自身目标:{goal}",
600
+ "faction pressure: {goal}": "组织压力:{goal}",
601
+ "high tension toward the player": "对玩家高度紧张",
602
+ "distrusts the player": "不信任玩家",
603
+ "cares about the player": "在意玩家",
604
+ "dislikes the player": "不喜欢玩家",
605
+ "Likely characters to activate, in priority order (advice only; activate only whoever the moment actually reaches):": "可能需要启动的角色,按优先级排列(仅供参考;只启动当下真正牵涉到的人):",
606
+ "socially implicated": "社交上被牵涉",
607
+ "The player's character is in {place}": "玩家角色在{place}",
608
+ "Characters here:": "在这里的角色:",
609
+ "Characters elsewhere (they perceive nothing here unless the moment reaches them):": "在别处的角色(除非当下波及他们,否则感知不到这里的事):",
610
+ "{member} - in {place} ({key})": "{member} - 在{place}({key})",
611
+ "Characters with no place (not part of this scene; they cannot be asked to act):": "没有所在地点的角色(不属于这一幕;不能让他们行动):",
612
+ "the player's character, \"{key}\"": "玩家角色,“{key}”",
613
+ "acts through \"{key}\"": "通过“{key}”行动",
614
+ "Currently: {state}": "目前:{state}",
615
+ "Scene before turn {turn}:": "第 {turn} 回合之前的场景:",
616
+ "What characters have on them:": "角色们身上带着:",
617
+ "The world acted since the last turn, by itself:": "自上一回合以来,世界自行发生的事:",
618
+ "Known beforehand, still to come (who knows of it; it will happen by itself when its time comes):": "事先已知、尚未发生的事(谁知道;时候到了会自行发生):",
619
+ "[known to {who}]": "[知情者:{who}]",
620
+ "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 中说明已处理(每条会保留几个回合,之后放弃):",
621
+ "What the story keeps track of (change it with variable_changes in an outcome when what happened calls for it):": "故事追踪的内容(发生的事需要时,在结果中用 variable_changes 修改):",
622
+ "Still open in the story:": "故事中仍未了结的事:",
623
+ "Earlier events that bear on what the player just did (from the records of the story):": "与玩家刚才的行动相关的早先事件(来自故事记录):",
624
+ "turn {turn}": "第 {turn} 回合",
625
+ "You were to {title}, at {when}": "你本应{title},时间是{when}",
626
+ "You were to {title} at {place}, at {when}": "你本应在{place}:{title},时间是{when}",
627
+ " ({note})": "({note})",
628
+ "It is {now} now and the time has passed.": "现在是{now},时间已经过了。",
629
+ "It is {now} now.": "现在是{now}。",
630
+ "It was with: {who}.": "同你一起的有:{who}。",
631
+ "You are not with the player's character, and nothing of this happens in front of anyone: decide what you do about it.": "你不和玩家的角色在一起,这件事也不会发生在任何人面前:决定你要怎么处理。",
632
+ "The time you had for \"{goal}\" has passed ({when}; it is {now} now)": "你为“{goal}”预留的时间已过({when};现在是{now})",
633
+ "You are not with the player's character and nothing of this happens in front of anyone: decide what you do about it.": "你不和玩家的角色在一起,这件事也不会发生在任何人面前:决定你要怎么处理。",
634
+ "{names} noticed: {text}": "{names}注意到:{text}",
635
+ "{name} went to {place}.": "{name}去了{place}。",
636
+ "## MCP resource servers\n\nUse {tools} with the server argument naming an MCP server.": "## MCP 资源服务器\n\n使用 {tools},server 参数填 MCP 服务器的名称。",
637
+ "## MCP resource servers\n\nUse {tools} with one of these names as the server argument: {servers}.": "## MCP 资源服务器\n\n使用 {tools},server 参数填下列名称之一:{servers}。"
638
+ };
639
+ //#endregion
640
+ //#region ../dsh-roleplay-model/src/messages.ts
641
+ /** The languages the texts a model reads are written in. English is the source: a text is its own key. */
642
+ const LANGUAGES = ["en", "zh"];
643
+ /** The language a setting such as `zh-CN` or `en` stands for; English when it is none that is known. */
644
+ function languageOf(code) {
645
+ return (code ?? "").trim().toLowerCase().startsWith("zh") ? "zh" : "en";
646
+ }
647
+ const fill = (template, values) => values === void 0 ? template : template.replace(/\{(\w+)\}/g, (whole, name) => name in values ? String(values[name]) : whole);
648
+ const CATALOGUES = { zh: ZH };
649
+ /**
650
+ * 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
651
+ * the others; a text a language lacks is told in English, and a test keeps that from happening.
652
+ */
653
+ function translator(language) {
654
+ const catalogue = language === "en" ? void 0 : CATALOGUES[language];
655
+ return (text, values) => fill(catalogue?.[text] ?? text, values);
656
+ }
657
+ /** The texts with a translation, for the test that every text has one. */
658
+ const translated = (language) => Object.keys(CATALOGUES[language]);
659
+ const translation = (language, text) => CATALOGUES[language][text];
660
+ //#endregion
406
661
  //#region src/clock.ts
407
662
  const systemClock = () => /* @__PURE__ */ new Date();
408
663
  //#endregion
@@ -562,6 +817,7 @@ const toWindow = (row) => ({
562
817
  id: row.id,
563
818
  characterId: row.character_id,
564
819
  days: parseNumberList(row.days, "routine_windows.days"),
820
+ on: parseStringList(row.filters, "routine_windows.filters"),
565
821
  start: row.start_minute,
566
822
  end: row.end_minute,
567
823
  activity: row.activity,
@@ -595,10 +851,11 @@ var SqliteRoutineRepository = class {
595
851
  if (this.db.get("SELECT 1 FROM characters WHERE id = ?", [characterId]) === void 0) throw new NotFoundError(`character ${characterId} does not exist`);
596
852
  this.db.run("DELETE FROM routine_windows WHERE character_id = ?", [characterId]);
597
853
  windows.forEach((window, index) => {
598
- this.db.run("INSERT INTO routine_windows (character_id, ordinal, days, start_minute, end_minute, activity, location_id, feature_key, position, interruptible) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
854
+ this.db.run("INSERT INTO routine_windows (character_id, ordinal, days, filters, start_minute, end_minute, activity, location_id, feature_key, position, interruptible) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
599
855
  characterId,
600
856
  index,
601
857
  toJson([...new Set(window.days)].sort()),
858
+ toJson([...new Set(window.on ?? [])]),
602
859
  window.start,
603
860
  window.end,
604
861
  window.activity.trim(),
@@ -707,6 +964,226 @@ var SqliteEngagementRepository = class {
707
964
  }
708
965
  };
709
966
  //#endregion
967
+ //#region src/records/calendar.ts
968
+ /**
969
+ * 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,
970
+ * 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`,
971
+ * `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
972
+ * day when it names no day to apply on or one of them is today, and none of the days to leave out is.
973
+ */
974
+ const MONTH_NAMES = [
975
+ "jan",
976
+ "feb",
977
+ "mar",
978
+ "apr",
979
+ "may",
980
+ "jun",
981
+ "jul",
982
+ "aug",
983
+ "sep",
984
+ "oct",
985
+ "nov",
986
+ "dec"
987
+ ];
988
+ const SEASONS = [
989
+ "spring",
990
+ "summer",
991
+ "autumn",
992
+ "winter"
993
+ ];
994
+ const NO_CALENDAR = {
995
+ hemisphere: "north",
996
+ holidays: []
997
+ };
998
+ const MAX_TOKEN = 60;
999
+ const daysIn = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
1000
+ const DATE = /^(?:(\d{4})-)?(\d{2})-(\d{2})$/;
1001
+ function dateOf(text) {
1002
+ const found = DATE.exec(text);
1003
+ if (found === null) return void 0;
1004
+ const [, year, month, day] = found;
1005
+ const m = Number(month);
1006
+ const d = Number(day);
1007
+ if (m < 1 || m > 12 || d < 1 || d > daysIn(year === void 0 ? 2024 : Number(year), m)) return void 0;
1008
+ return {
1009
+ ...year === void 0 ? {} : { year: Number(year) },
1010
+ month: m,
1011
+ day: d
1012
+ };
1013
+ }
1014
+ /** What a token says, or why it says nothing. */
1015
+ function parseDayToken(raw) {
1016
+ const negated = raw.startsWith("!");
1017
+ const text = (negated ? raw.slice(1) : raw).trim().toLowerCase();
1018
+ const fail = (problem) => ({ problem: `"${raw}": ${problem}` });
1019
+ if (text === "" || text.length > MAX_TOKEN) return fail("a day is told in a few words");
1020
+ const weekday = WEEKDAY_NAMES.indexOf(text);
1021
+ if (weekday >= 0) return {
1022
+ token: {
1023
+ kind: "weekday",
1024
+ weekday
1025
+ },
1026
+ negated
1027
+ };
1028
+ const month = MONTH_NAMES.indexOf(text);
1029
+ if (month >= 0) return {
1030
+ token: {
1031
+ kind: "month",
1032
+ month: month + 1
1033
+ },
1034
+ negated
1035
+ };
1036
+ const one = dateOf(text);
1037
+ if (one !== void 0) return {
1038
+ token: {
1039
+ kind: "date",
1040
+ ...one
1041
+ },
1042
+ negated
1043
+ };
1044
+ if (text.includes("..")) {
1045
+ const [from, to, ...rest] = text.split("..");
1046
+ const start = from === void 0 ? void 0 : dateOf(from);
1047
+ const end = to === void 0 ? void 0 : dateOf(to);
1048
+ 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");
1049
+ return {
1050
+ token: {
1051
+ kind: "span",
1052
+ from: start,
1053
+ to: end
1054
+ },
1055
+ negated
1056
+ };
1057
+ }
1058
+ const [head, a, b] = text.split(":");
1059
+ if (head === "dom" && a !== void 0 && b === void 0) {
1060
+ if (a === "last") return {
1061
+ token: {
1062
+ kind: "dom",
1063
+ day: "last"
1064
+ },
1065
+ negated
1066
+ };
1067
+ const day = Number(a);
1068
+ return Number.isInteger(day) && day >= 1 && day <= 31 ? {
1069
+ token: {
1070
+ kind: "dom",
1071
+ day
1072
+ },
1073
+ negated
1074
+ } : fail("the day of the month is 1 to 31, or `dom:last`");
1075
+ }
1076
+ if (head === "nth" && a !== void 0 && b !== void 0) {
1077
+ const day = WEEKDAY_NAMES.indexOf(a);
1078
+ const n = b === "last" ? "last" : Number(b);
1079
+ return day >= 0 && (n === "last" || Number.isInteger(n) && n >= 1 && n <= 5) ? {
1080
+ token: {
1081
+ kind: "nth",
1082
+ weekday: day,
1083
+ n
1084
+ },
1085
+ negated
1086
+ } : fail("`nth:mon:1` is the first Monday of a month, `nth:fri:last` the last Friday");
1087
+ }
1088
+ if (head === "season" && a !== void 0 && b === void 0) {
1089
+ const season = SEASONS.find((candidate) => candidate === a);
1090
+ return season === void 0 ? fail(`a season is one of ${SEASONS.join(", ")}`) : {
1091
+ token: {
1092
+ kind: "season",
1093
+ season
1094
+ },
1095
+ negated
1096
+ };
1097
+ }
1098
+ if (head === "holiday" && a !== void 0 && b === void 0 && a !== "") return {
1099
+ token: {
1100
+ kind: "holiday",
1101
+ id: a
1102
+ },
1103
+ negated
1104
+ };
1105
+ 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>`");
1106
+ }
1107
+ /** 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. */
1108
+ function dayTokenProblems(tokens, path, holidays) {
1109
+ return tokens.flatMap((raw) => {
1110
+ const parsed = parseDayToken(raw);
1111
+ if ("problem" in parsed) return [`${path}: ${parsed.problem}`];
1112
+ if (parsed.token.kind === "holiday") {
1113
+ if (holidays === void 0) return [`${path}: "${raw}": a holiday cannot be named here`];
1114
+ if (holidays !== "any" && !holidays.has(parsed.token.id)) return [`${path}: "${raw}": the world names no holiday "${parsed.token.id}"`];
1115
+ }
1116
+ return [];
1117
+ });
1118
+ }
1119
+ /** 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). */
1120
+ function seasonOf(day, hemisphere = "north") {
1121
+ const north = day.month === 12 || day.month <= 2 ? "winter" : day.month <= 5 ? "spring" : day.month <= 8 ? "summer" : "autumn";
1122
+ if (hemisphere === "north") return north;
1123
+ return north === "winter" ? "summer" : north === "summer" ? "winter" : north === "spring" ? "autumn" : "spring";
1124
+ }
1125
+ const key = (month, day) => month * 100 + day;
1126
+ function within$1(day, from, to) {
1127
+ if (from.year !== void 0 && to.year !== void 0) {
1128
+ const at = day.year * 1e4 + key(day.month, day.day);
1129
+ return at >= from.year * 1e4 + key(from.month, from.day) && at <= to.year * 1e4 + key(to.month, to.day);
1130
+ }
1131
+ const at = key(day.month, day.day);
1132
+ const start = key(from.month, from.day);
1133
+ const end = key(to.month, to.day);
1134
+ return start <= end ? at >= start && at <= end : at >= start || at <= end;
1135
+ }
1136
+ function matches(token, day, calendar) {
1137
+ switch (token.kind) {
1138
+ case "weekday": return day.weekday === token.weekday;
1139
+ case "month": return day.month === token.month;
1140
+ case "date": return day.month === token.month && day.day === token.day && (token.year === void 0 || day.year === token.year);
1141
+ case "span": return within$1(day, token.from, token.to);
1142
+ case "dom": return token.day === "last" ? day.day === daysIn(day.year, day.month) : day.day === token.day;
1143
+ 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);
1144
+ case "season": return seasonOf(day, calendar.hemisphere) === token.season;
1145
+ case "holiday": return holidaysOn(calendar, day).some((holiday) => holiday.id === token.id);
1146
+ }
1147
+ }
1148
+ /** 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. */
1149
+ function dayMatches(tokens, day, calendar = NO_CALENDAR) {
1150
+ const parsed = tokens.flatMap((raw) => {
1151
+ const found = parseDayToken(raw);
1152
+ return "problem" in found ? [] : [found];
1153
+ });
1154
+ const wanted = parsed.filter((entry) => !entry.negated);
1155
+ const unwanted = parsed.filter((entry) => entry.negated);
1156
+ return (wanted.length === 0 || wanted.some((entry) => matches(entry.token, day, calendar))) && !unwanted.some((entry) => matches(entry.token, day, calendar));
1157
+ }
1158
+ /** 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. */
1159
+ function holidaysOn(calendar, day) {
1160
+ return calendar.holidays.filter((holiday) => holiday.on.length > 0 && dayMatches(holiday.on, day, {
1161
+ ...calendar,
1162
+ holidays: []
1163
+ }));
1164
+ }
1165
+ const CALENDAR_KEY = "calendar";
1166
+ /** The calendar a story was started with, from its metadata; none when the world has none. */
1167
+ function readCalendar(metadata) {
1168
+ const stored = metadata.getValue(CALENDAR_KEY);
1169
+ if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return NO_CALENDAR;
1170
+ const { hemisphere, holidays } = stored;
1171
+ return {
1172
+ hemisphere: hemisphere === "south" ? "south" : "north",
1173
+ holidays: Array.isArray(holidays) ? holidays.flatMap((entry) => {
1174
+ const { id, name, on } = entry ?? {};
1175
+ return typeof id === "string" && typeof name === "string" && Array.isArray(on) ? [{
1176
+ id,
1177
+ name,
1178
+ on: on.filter((token) => typeof token === "string")
1179
+ }] : [];
1180
+ }) : []
1181
+ };
1182
+ }
1183
+ const writeCalendar = (metadata, calendar) => {
1184
+ metadata.setValue(CALENDAR_KEY, calendar);
1185
+ };
1186
+ //#endregion
710
1187
  //#region src/records/time.ts
711
1188
  /** Instants are UTC milliseconds; a time zone only decides how they read. */
712
1189
  const SECOND = 1e3;
@@ -834,9 +1311,19 @@ function parseInstant(text, zone) {
834
1311
  }
835
1312
  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)`);
836
1313
  }
837
- /** `Tuesday 4 October 1912, 21:40`. */
838
- function describeInstant(instant, zone) {
1314
+ const WEEKDAYS_ZH = [
1315
+ "星期日",
1316
+ "星期一",
1317
+ "星期二",
1318
+ "星期三",
1319
+ "星期四",
1320
+ "星期五",
1321
+ "星期六"
1322
+ ];
1323
+ /** `Tuesday 4 October 1912, 21:40`, or `1912年10月4日 星期二 21:40`. */
1324
+ function describeInstant(instant, zone, language = "en") {
839
1325
  const local = localTime(instant, zone);
1326
+ if (language === "zh") return `${local.year}年${local.month}月${local.day}日 ${WEEKDAYS_ZH[local.weekday]} ${pad$1(local.hour)}:${pad$1(local.minute)}`;
840
1327
  return `${WEEKDAYS[local.weekday]} ${local.day} ${MONTHS[local.month - 1]} ${local.year}, ${pad$1(local.hour)}:${pad$1(local.minute)}`;
841
1328
  }
842
1329
  /** `1912-10-04T21:40:00` as the wall clock of a zone reads, the form a file or a person can write back. */
@@ -856,9 +1343,25 @@ function dayPart(minuteOfDay) {
856
1343
  const hour = Math.floor(minuteOfDay / 60);
857
1344
  return hour < 5 || hour >= 22 ? "night" : hour < 12 ? "morning" : hour < 17 ? "afternoon" : "evening";
858
1345
  }
859
- /** In words and rounded to what matters: `45 seconds`, `3 hours 12 minutes`, `2 days 3 hours`. */
860
- function describeDuration(milliseconds) {
1346
+ /** In words and rounded to what matters: `45 seconds`, `3 hours 12 minutes`, `2 days 3 hours`; or `45秒`, `3小时12分钟`, `2天3小时`. */
1347
+ function describeDuration(milliseconds, language = "en") {
861
1348
  const total = Math.max(0, Math.round(milliseconds / SECOND));
1349
+ if (language === "zh") {
1350
+ if (total < 60) return `${total}秒`;
1351
+ const parts = [];
1352
+ let left = total;
1353
+ for (const [name, size] of [
1354
+ ["天", 86400],
1355
+ ["小时", 3600],
1356
+ ["分钟", 60]
1357
+ ]) {
1358
+ const count = Math.floor(left / size);
1359
+ left -= count * size;
1360
+ if (count > 0) parts.push(`${count}${name}`);
1361
+ if (parts.length === 2) break;
1362
+ }
1363
+ return parts.join("");
1364
+ }
862
1365
  if (total < 60) return `${total} second${total === 1 ? "" : "s"}`;
863
1366
  const units = [
864
1367
  ["day", 86400],
@@ -1114,6 +1617,8 @@ function prepareAgenda(target, edit, context) {
1114
1617
  if (found < 0) problems.push(`${here}.days: "${day}" is not a day (${WEEKDAY_NAMES.join(", ")})`);
1115
1618
  return found < 0 ? [] : [found];
1116
1619
  });
1620
+ const holidays = new Set(readCalendar(target.metadata).holidays.map((holiday) => holiday.id));
1621
+ problems.push(...dayTokenProblems(list$1(window.on), `${here}.on`, holidays));
1117
1622
  if (window.location !== void 0) {
1118
1623
  placeKnown(window.location, `${here}.location`);
1119
1624
  const place = context.places.get(window.location);
@@ -1122,6 +1627,7 @@ function prepareAgenda(target, edit, context) {
1122
1627
  if (start === void 0 || end === void 0) return [];
1123
1628
  return [{
1124
1629
  days,
1630
+ on: [...list$1(window.on)],
1125
1631
  start,
1126
1632
  end,
1127
1633
  activity: window.activity.trim(),
@@ -2143,8 +2649,11 @@ function parseCondition(value, path, problems, count, depth = 0) {
2143
2649
  to: text(value["window"], "to") ?? ""
2144
2650
  } : void 0;
2145
2651
  const days = Array.isArray(value["days"]) ? value["days"].filter((day) => typeof day === "string") : void 0;
2146
- if (from === void 0 && to === void 0 && window === void 0 && days === void 0) {
2147
- problems.push(`${path}: a time condition needs from, to, window or days`);
2652
+ const on = Array.isArray(value["on"]) ? value["on"].filter((day) => typeof day === "string") : void 0;
2653
+ if (value["on"] !== void 0 && on?.length !== value["on"]?.length) problems.push(`${path}.on: a list of days (months, dates, seasons, holidays)`);
2654
+ if (on !== void 0) problems.push(...dayTokenProblems(on, `${path}.on`, "any"));
2655
+ if (from === void 0 && to === void 0 && window === void 0 && days === void 0 && on === void 0) {
2656
+ problems.push(`${path}: a time condition needs from, to, window, days or on`);
2148
2657
  return;
2149
2658
  }
2150
2659
  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`);
@@ -2155,7 +2664,8 @@ function parseCondition(value, path, problems, count, depth = 0) {
2155
2664
  ...from === void 0 ? {} : { from },
2156
2665
  ...to === void 0 ? {} : { to },
2157
2666
  ...window === void 0 ? {} : { window },
2158
- ...days === void 0 ? {} : { days }
2667
+ ...days === void 0 ? {} : { days },
2668
+ ...on === void 0 ? {} : { on }
2159
2669
  };
2160
2670
  }
2161
2671
  case "place": {
@@ -2407,7 +2917,7 @@ function conditionProblems(target, condition, path, known, self = false) {
2407
2917
  const character = (key) => self && key === "@self" || target.characters.list().some((entry) => characterKey(entry) === key) ? [] : [`${path}: there is no character "${key}"`];
2408
2918
  const place = (key) => target.locations.list().some((entry) => locationKey(entry) === key) ? [] : [`${path}: there is no place "${key}"`];
2409
2919
  switch (condition.type) {
2410
- case "time": return [];
2920
+ case "time": return dayTokenProblems(condition.on ?? [], `${path}.on`, new Set(readCalendar(target.metadata).holidays.map((holiday) => holiday.id)));
2411
2921
  case "place": return [...character(condition.character), ...place(condition.location)];
2412
2922
  case "variable": {
2413
2923
  const def = target.variables.def(condition.variable);
@@ -2503,14 +3013,20 @@ function holds(target, condition, clock) {
2503
3013
  if (condition.from !== void 0 && now < parseInstant(condition.from, zone)) return false;
2504
3014
  if (condition.to !== void 0 && now >= parseInstant(condition.to, zone)) return false;
2505
3015
  const local = localTime(now, zone);
3016
+ const calendar = readCalendar(target.metadata);
3017
+ /** The day a time belongs to: the morning part of a window past midnight belongs to the day before. */
3018
+ const dayAt = (shift) => addLocalDays(now, zone, shift);
3019
+ const onDay = (shift) => {
3020
+ const day = dayAt(shift);
3021
+ 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));
3022
+ };
2506
3023
  if (condition.window !== void 0) {
2507
3024
  const start = parseTimeOfDay(condition.window.from) ?? 0;
2508
3025
  const end = parseTimeOfDay(condition.window.to) ?? 0;
2509
3026
  if (!inWindow(local.minuteOfDay, start, end)) return false;
2510
- const weekday = start > end && local.minuteOfDay < end ? (local.weekday + 6) % 7 : local.weekday;
2511
- return condition.days === void 0 || condition.days.length === 0 || condition.days.includes(WEEKDAY_NAMES[weekday] ?? "");
3027
+ return onDay(start > end && local.minuteOfDay < end ? -1 : 0);
2512
3028
  }
2513
- return condition.days === void 0 || condition.days.length === 0 || condition.days.includes(WEEKDAY_NAMES[local.weekday] ?? "");
3029
+ return onDay(0);
2514
3030
  }
2515
3031
  case "place": {
2516
3032
  const who = target.characters.list().find((entry) => characterKey(entry) === condition.character);
@@ -2632,6 +3148,15 @@ function parseRecordsEdit(value) {
2632
3148
  }
2633
3149
  return found;
2634
3150
  };
3151
+ const numbersOf = (entry, key) => {
3152
+ const found = entry[key];
3153
+ if (found === void 0 || found === null) return void 0;
3154
+ if (!Array.isArray(found) || found.some((item) => typeof item !== "number")) {
3155
+ problems.push(`${key} must be a list of numbers`);
3156
+ return;
3157
+ }
3158
+ return found;
3159
+ };
2635
3160
  const text = (entry, key) => typeof entry[key] === "string" ? entry[key] : void 0;
2636
3161
  /** 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. */
2637
3162
  const named = (entry, key) => {
@@ -2924,13 +3449,19 @@ function parseRecordsEdit(value) {
2924
3449
  kind: text(entry, "kind") ?? "",
2925
3450
  text: text(entry, "text") ?? "",
2926
3451
  about: strings(entry, "about"),
2927
- importance: num(entry, "importance") ?? 0
3452
+ importance: num(entry, "importance") ?? 0,
3453
+ confidence: text(entry, "confidence"),
3454
+ evidenceFor: numbersOf(entry, "evidence_for"),
3455
+ evidenceAgainst: numbersOf(entry, "evidence_against")
2928
3456
  })),
2929
3457
  beliefEdits: array("belief_edits").map((entry) => defined({
2930
3458
  id: num(entry, "id") ?? 0,
2931
3459
  text: text(entry, "text"),
2932
3460
  about: strings(entry, "about"),
2933
- importance: num(entry, "importance")
3461
+ importance: num(entry, "importance"),
3462
+ confidence: text(entry, "confidence"),
3463
+ evidenceFor: numbersOf(entry, "evidence_for"),
3464
+ evidenceAgainst: numbersOf(entry, "evidence_against")
2934
3465
  })),
2935
3466
  beliefRetractions: numbers("belief_retractions"),
2936
3467
  threadsOpened: Array.isArray(value["threads_opened"]) ? value["threads_opened"].filter((entry) => typeof entry === "string") : [],
@@ -3015,6 +3546,7 @@ function parseRecordsEdit(value) {
3015
3546
  character: text(entry, "character") ?? "",
3016
3547
  windows: Array.isArray(entry["windows"]) ? entry["windows"].filter(isObject$1).map((window) => defined({
3017
3548
  days: strings(window, "days"),
3549
+ on: strings(window, "on"),
3018
3550
  from: text(window, "from") ?? "",
3019
3551
  to: text(window, "to") ?? "",
3020
3552
  activity: text(window, "activity") ?? "",
@@ -3036,6 +3568,21 @@ const scalarValue = {
3036
3568
  ],
3037
3569
  description: "A number, true or false, or one of the words of a choice."
3038
3570
  };
3571
+ const CONFIDENCE = {
3572
+ type: "string",
3573
+ enum: [...MEMORY_CONFIDENCES],
3574
+ 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."
3575
+ };
3576
+ const EVIDENCE_FOR = {
3577
+ type: "array",
3578
+ items: { type: "integer" },
3579
+ description: "The numbers of perceptions of the character (from read_records) that this rests on."
3580
+ };
3581
+ const EVIDENCE_AGAINST = {
3582
+ type: "array",
3583
+ items: { type: "integer" },
3584
+ description: "The numbers of perceptions of the character that speak against this; lower the confidence then."
3585
+ };
3039
3586
  const IMPORTANCE = {
3040
3587
  type: "integer",
3041
3588
  enum: [
@@ -3196,7 +3743,10 @@ function recordsEditSchema(options = {}) {
3196
3743
  },
3197
3744
  text: { type: "string" },
3198
3745
  about: keys,
3199
- importance: IMPORTANCE
3746
+ importance: IMPORTANCE,
3747
+ confidence: CONFIDENCE,
3748
+ evidence_for: EVIDENCE_FOR,
3749
+ evidence_against: EVIDENCE_AGAINST
3200
3750
  },
3201
3751
  required: [
3202
3752
  "character",
@@ -3893,6 +4443,11 @@ function recordsEditSchema(options = {}) {
3893
4443
  },
3894
4444
  description: "Leave out for every day."
3895
4445
  },
4446
+ on: {
4447
+ type: "array",
4448
+ items: { type: "string" },
4449
+ 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."
4450
+ },
3896
4451
  from: {
3897
4452
  type: "string",
3898
4453
  description: "HH:MM, 24-hour."
@@ -4258,7 +4813,10 @@ function recordsEditSchema(options = {}) {
4258
4813
  id: { type: "integer" },
4259
4814
  text: { type: "string" },
4260
4815
  about: keys,
4261
- importance: IMPORTANCE
4816
+ importance: IMPORTANCE,
4817
+ confidence: CONFIDENCE,
4818
+ evidence_for: EVIDENCE_FOR,
4819
+ evidence_against: EVIDENCE_AGAINST
4262
4820
  },
4263
4821
  required: ["id"],
4264
4822
  additionalProperties: false
@@ -4604,26 +5162,49 @@ function applyChanges(target, edit) {
4604
5162
  perception: one
4605
5163
  }];
4606
5164
  });
5165
+ const confidence = (value, where) => {
5166
+ if (value !== void 0 && !MEMORY_CONFIDENCES.includes(value)) problems.push(`${where}: confidence is one of ${MEMORY_CONFIDENCES.join(", ")}`);
5167
+ };
5168
+ /** Perceptions a belief of one character is said to rest on, or to be contradicted by: they must be the character's own. */
5169
+ const evidence = (value, characterId, where) => (value ?? []).flatMap((id) => {
5170
+ const seen = target.events.getPerception(id);
5171
+ if (seen === void 0) {
5172
+ problems.push(`${where}: there is no perception ${id}`);
5173
+ return [];
5174
+ }
5175
+ if (characterId !== void 0 && seen.characterId !== characterId) {
5176
+ problems.push(`${where}: perception ${id} is not one the character took in`);
5177
+ return [];
5178
+ }
5179
+ return [id];
5180
+ });
4607
5181
  const beliefs = list(edit.beliefs).flatMap((belief, index) => {
4608
5182
  const where = `beliefs[${index}]`;
4609
5183
  const character = idOf(belief.character, where);
4610
5184
  if (!MEMORY_KINDS.includes(belief.kind)) problems.push(`${where}: unknown kind "${String(belief.kind)}"`);
4611
5185
  if (!present(belief.text)) problems.push(`${where}: say what the character believes or knows`);
4612
5186
  importance(belief.importance, where);
5187
+ confidence(belief.confidence, where);
4613
5188
  return character === void 0 ? [] : [{
4614
5189
  belief,
4615
5190
  character,
4616
- about: ids(belief.about, `${where}.about`)
5191
+ about: ids(belief.about, `${where}.about`),
5192
+ evidenceFor: evidence(belief.evidenceFor, character, `${where}.evidence_for`),
5193
+ evidenceAgainst: evidence(belief.evidenceAgainst, character, `${where}.evidence_against`)
4617
5194
  }];
4618
5195
  });
4619
5196
  const beliefEdits = list(edit.beliefEdits).map((change, index) => {
4620
5197
  const where = `belief_edits[${index}]`;
4621
- if (target.memories.get(change.id) === void 0) problems.push(`${where}: there is no belief ${change.id}`);
5198
+ const current = target.memories.get(change.id);
5199
+ if (current === void 0) problems.push(`${where}: there is no belief ${change.id}`);
4622
5200
  if (change.text !== void 0 && !present(change.text)) problems.push(`${where}: say what the character believes or knows`);
4623
5201
  importance(change.importance, where);
5202
+ confidence(change.confidence, where);
4624
5203
  return {
4625
5204
  change,
4626
- about: change.about === void 0 ? void 0 : ids(change.about, `${where}.about`)
5205
+ about: change.about === void 0 ? void 0 : ids(change.about, `${where}.about`),
5206
+ evidenceFor: evidence(change.evidenceFor, current?.characterId, `${where}.evidence_for`),
5207
+ evidenceAgainst: evidence(change.evidenceAgainst, current?.characterId, `${where}.evidence_against`)
4627
5208
  };
4628
5209
  });
4629
5210
  for (const id of list(edit.beliefRetractions)) if (target.memories.get(id) === void 0) problems.push(`belief_retractions: there is no belief ${id}`);
@@ -4898,23 +5479,30 @@ function applyChanges(target, edit) {
4898
5479
  learnEventEntities(target.events.get(eventId), perception.characterId);
4899
5480
  result.writes += 1;
4900
5481
  }
4901
- for (const { belief, character, about } of beliefs) {
4902
- result.beliefIds.push(target.memories.add({
5482
+ for (const { belief, character, about, evidenceFor, evidenceAgainst } of beliefs) {
5483
+ const added = target.memories.add({
4903
5484
  characterId: character,
4904
5485
  kind: belief.kind,
4905
5486
  text: belief.text.trim(),
4906
5487
  about,
4907
5488
  importance: belief.importance,
5489
+ ...belief.confidence === void 0 ? {} : { confidence: belief.confidence },
4908
5490
  turn: edit.turn
4909
- }).id);
5491
+ });
5492
+ result.beliefIds.push(added.id);
5493
+ for (const id of evidenceFor) target.memories.addEvidence(added.id, id, "supports", edit.turn);
5494
+ for (const id of evidenceAgainst) target.memories.addEvidence(added.id, id, "contradicts", edit.turn);
4910
5495
  result.writes += 1;
4911
5496
  }
4912
- for (const { change, about } of beliefEdits) {
5497
+ for (const { change, about, evidenceFor, evidenceAgainst } of beliefEdits) {
4913
5498
  target.memories.update(change.id, {
4914
5499
  ...change.text === void 0 ? {} : { text: change.text.trim() },
4915
5500
  ...about === void 0 ? {} : { about },
4916
- ...change.importance === void 0 ? {} : { importance: change.importance }
5501
+ ...change.importance === void 0 ? {} : { importance: change.importance },
5502
+ ...change.confidence === void 0 ? {} : { confidence: change.confidence }
4917
5503
  });
5504
+ for (const id of evidenceFor) target.memories.addEvidence(change.id, id, "supports", edit.turn);
5505
+ for (const id of evidenceAgainst) target.memories.addEvidence(change.id, id, "contradicts", edit.turn);
4918
5506
  result.writes += 1;
4919
5507
  }
4920
5508
  for (const id of list(edit.beliefRetractions)) {
@@ -5480,7 +6068,8 @@ const DEFAULT_MEMORY_POLICY = {
5480
6068
  recall: 12,
5481
6069
  overRetrieve: 4,
5482
6070
  associate: 2,
5483
- diversity: .3
6071
+ diversity: .3,
6072
+ foldedFade: .5
5484
6073
  };
5485
6074
  /** Story days for the memory of an unimportant event to halve; each step of importance doubles it. */
5486
6075
  const HALF_LIFE_DAYS = 6;
@@ -5569,7 +6158,7 @@ function describeEvent({ perception, event }, names, at) {
5569
6158
  vague: about === "" ? "something happened, but you cannot say what" : `something to do with ${about}`
5570
6159
  };
5571
6160
  }
5572
- function describeBelief(memory, at) {
6161
+ function describeBelief(memory, at, disputed) {
5573
6162
  return {
5574
6163
  turn: memory.turn,
5575
6164
  ...at === void 0 ? {} : { at },
@@ -5577,6 +6166,8 @@ function describeBelief(memory, at) {
5577
6166
  kind: "memory",
5578
6167
  id: memory.id
5579
6168
  },
6169
+ confidence: memory.confidence,
6170
+ ...disputed ? { disputed: true } : {},
5580
6171
  clear: memory.text,
5581
6172
  faded: first(memory.text),
5582
6173
  vague: first(memory.text)
@@ -5611,7 +6202,9 @@ const DENSE_RANGE = .5;
5611
6202
  const denseScore = (similarity) => Math.min(1, Math.max(0, (similarity - DENSE_FLOOR) / DENSE_RANGE));
5612
6203
  /** Being where something happened calls it to mind, about as much as sharing two words with it. */
5613
6204
  const PLACE_CUE = 2;
5614
- function gather(source, { characterId, characters, situation, locationId, about }) {
6205
+ function gather(source, { characterId, characters, situation, locationId, about, policy }) {
6206
+ const evidence = source.memories.evidenceIndex?.(characterId);
6207
+ const folding = Math.min(1, Math.max(.05, policy?.foldedFade ?? 1));
5615
6208
  const names = new Map(characters.map((character) => [character.id, character.name]));
5616
6209
  const peers = mentioned(situation, characters);
5617
6210
  peers.delete(characterId);
@@ -5651,7 +6244,8 @@ function gather(source, { characterId, characters, situation, locationId, about
5651
6244
  ageDays: ageDays(at),
5652
6245
  recalls: seen.perception.recalls,
5653
6246
  seed: seen.perception.id,
5654
- durability: 1
6247
+ durability: evidence?.folded.has(seen.perception.id) === true ? folding : 1,
6248
+ ...evidence?.folded.has(seen.perception.id) === true ? { covered: evidence.folded.get(seen.perception.id) } : {}
5655
6249
  };
5656
6250
  }), ...source.memories.list(characterId).filter((belief) => concerns(belief.about)).map((belief) => {
5657
6251
  const at = atTurn(belief.turn);
@@ -5666,7 +6260,7 @@ function gather(source, { characterId, characters, situation, locationId, about
5666
6260
  people: belief.about.filter((id) => id !== characterId),
5667
6261
  tags: []
5668
6262
  },
5669
- recalled: describeBelief(belief, at),
6263
+ recalled: describeBelief(belief, at, evidence?.disputed.has(belief.id) === true),
5670
6264
  cues: cue,
5671
6265
  relevance: cue.people + cue.words,
5672
6266
  importance: belief.importance,
@@ -5826,6 +6420,28 @@ function diversified(ranked, count, diversity) {
5826
6420
  }
5827
6421
  return chosen;
5828
6422
  }
6423
+ /** How sure the character is of a belief, and whether something since speaks against it. */
6424
+ function beliefOf(candidate) {
6425
+ const { confidence, disputed } = candidate.recalled;
6426
+ return {
6427
+ ...confidence === void 0 ? {} : { confidence },
6428
+ ...disputed === void 0 ? {} : { disputed }
6429
+ };
6430
+ }
6431
+ /**
6432
+ * 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,
6433
+ * and the place goes to the next best.
6434
+ */
6435
+ function chooseUncovered(standing, count, diversity) {
6436
+ let pool = standing;
6437
+ for (;;) {
6438
+ const chosen = diversified(pool, count, diversity);
6439
+ const told = new Set(chosen.filter((entry) => entry.candidate.recalled.source.kind === "memory").map((entry) => entry.candidate.recalled.source.id));
6440
+ const repeated = chosen.filter((entry) => entry.candidate.covered?.some((id) => told.has(id)) === true);
6441
+ if (repeated.length === 0) return chosen;
6442
+ pool = pool.filter((entry) => !repeated.includes(entry));
6443
+ }
6444
+ }
5829
6445
  /**
5830
6446
  * The last stage: choose the `policy.recall` best of the pool, spread by `policy.diversity`. With a reranker's judgement each
5831
6447
  * candidate's relevance is mostly that (a candidate it left out counts as 0), and the rest is what
@@ -5854,13 +6470,14 @@ function finishRecall(pool, rerank) {
5854
6470
  why
5855
6471
  }];
5856
6472
  }).sort((a, b) => b.score - a.score || b.candidate.recalled.turn - a.candidate.recalled.turn);
5857
- const chosen = diversified(standing, policy.recall, policy.diversity ?? 0);
6473
+ const chosen = chooseUncovered(standing, policy.recall, policy.diversity ?? 0);
5858
6474
  const result = chosen.map(({ candidate, clarity, text, why }) => ({
5859
6475
  clarity,
5860
6476
  text,
5861
6477
  turn: candidate.recalled.turn,
5862
6478
  ...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
5863
6479
  source: candidate.recalled.source,
6480
+ ...beliefOf(candidate),
5864
6481
  ...why === void 0 ? {} : { why }
5865
6482
  }));
5866
6483
  const anchors = chosen.filter((entry) => entry.relevance >= CUE_RELEVANCE);
@@ -5882,6 +6499,7 @@ function finishRecall(pool, rerank) {
5882
6499
  turn: candidate.recalled.turn,
5883
6500
  ...candidate.recalled.at === void 0 ? {} : { at: candidate.recalled.at },
5884
6501
  source: candidate.recalled.source,
6502
+ ...beliefOf(candidate),
5885
6503
  linked: true,
5886
6504
  ...pool.query.explain === true ? { why: {
5887
6505
  cues: candidate.cues,
@@ -6031,15 +6649,21 @@ function readRecords(target, scope = {}) {
6031
6649
  gist: entry.gist
6032
6650
  }))
6033
6651
  })),
6034
- beliefs: characters.flatMap((character) => target.memories.list(character.id).filter((belief) => inScope(belief.turn)).map((belief) => ({
6035
- id: belief.id,
6036
- character: characterKey(character),
6037
- kind: belief.kind,
6038
- text: belief.text,
6039
- about: belief.about.map(keyOf),
6040
- importance: belief.importance,
6041
- turn: belief.turn
6042
- }))),
6652
+ beliefs: characters.flatMap((character) => target.memories.list(character.id).filter((belief) => inScope(belief.turn)).map((belief) => {
6653
+ const evidence = target.memories.evidenceOf(belief.id);
6654
+ return {
6655
+ id: belief.id,
6656
+ character: characterKey(character),
6657
+ kind: belief.kind,
6658
+ text: belief.text,
6659
+ about: belief.about.map(keyOf),
6660
+ importance: belief.importance,
6661
+ confidence: belief.confidence,
6662
+ evidence_for: evidence.filter((entry) => entry.relation === "supports").map((entry) => entry.perceptionId),
6663
+ evidence_against: evidence.filter((entry) => entry.relation === "contradicts").map((entry) => entry.perceptionId),
6664
+ turn: belief.turn
6665
+ };
6666
+ })),
6043
6667
  open_threads: target.threads.list("open").map((thread) => ({
6044
6668
  id: thread.id,
6045
6669
  text: thread.text,
@@ -6400,6 +7024,7 @@ const toMemory = (row) => ({
6400
7024
  text: row.text,
6401
7025
  about: parseNumberList(row.about, "memories.about"),
6402
7026
  importance: row.importance,
7027
+ confidence: row.confidence,
6403
7028
  turn: row.turn,
6404
7029
  recalls: row.recalls,
6405
7030
  ...row.last_recalled_turn === null ? {} : { lastRecalledTurn: row.last_recalled_turn },
@@ -6416,12 +7041,13 @@ var SqliteMemoryRepository = class {
6416
7041
  }
6417
7042
  add(memory) {
6418
7043
  return guard(() => {
6419
- const { lastInsertRowid } = this.db.run("INSERT INTO memories (character_id, kind, text, about, importance, turn, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [
7044
+ const { lastInsertRowid } = this.db.run("INSERT INTO memories (character_id, kind, text, about, importance, confidence, turn, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
6420
7045
  memory.characterId,
6421
7046
  memory.kind,
6422
7047
  memory.text,
6423
7048
  toJson(memory.about),
6424
7049
  memory.importance,
7050
+ memory.confidence ?? "likely",
6425
7051
  memory.turn,
6426
7052
  this.clock().toISOString()
6427
7053
  ]);
@@ -6445,10 +7071,11 @@ var SqliteMemoryRepository = class {
6445
7071
  update(id, patch) {
6446
7072
  return guard(() => {
6447
7073
  const current = this.require(id);
6448
- this.db.run("UPDATE memories SET text = ?, about = ?, importance = ? WHERE id = ?", [
7074
+ this.db.run("UPDATE memories SET text = ?, about = ?, importance = ?, confidence = ? WHERE id = ?", [
6449
7075
  patch.text ?? current.text,
6450
7076
  toJson(patch.about ?? current.about),
6451
7077
  patch.importance ?? current.importance,
7078
+ patch.confidence ?? current.confidence,
6452
7079
  id
6453
7080
  ]);
6454
7081
  const changed = this.require(id);
@@ -6470,6 +7097,36 @@ var SqliteMemoryRepository = class {
6470
7097
  this.index.remove("memory", id);
6471
7098
  return this.db.run("DELETE FROM memories WHERE id = ?", [id]).changes > 0;
6472
7099
  }
7100
+ addEvidence(memoryId, perceptionId, relation, turn) {
7101
+ this.require(memoryId);
7102
+ 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", [
7103
+ memoryId,
7104
+ perceptionId,
7105
+ relation,
7106
+ turn
7107
+ ]));
7108
+ }
7109
+ evidenceOf(memoryId) {
7110
+ return this.db.all("SELECT * FROM memory_evidence WHERE memory_id = ? ORDER BY perception_id", [memoryId]).map((row) => ({
7111
+ memoryId: row.memory_id,
7112
+ perceptionId: row.perception_id,
7113
+ relation: row.relation,
7114
+ turn: row.turn
7115
+ }));
7116
+ }
7117
+ evidenceIndex(characterId) {
7118
+ 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]);
7119
+ const folded = /* @__PURE__ */ new Map();
7120
+ const disputed = /* @__PURE__ */ new Set();
7121
+ for (const row of rows) {
7122
+ folded.set(row.perception_id, [...folded.get(row.perception_id) ?? [], row.memory_id]);
7123
+ if (row.relation === "contradicts") disputed.add(row.memory_id);
7124
+ }
7125
+ return {
7126
+ folded,
7127
+ disputed
7128
+ };
7129
+ }
6473
7130
  require(id) {
6474
7131
  const memory = this.get(id);
6475
7132
  if (memory === void 0) throw new NotFoundError(`memory ${id} does not exist`);
@@ -6920,6 +7577,108 @@ var SqliteTriggerRepository = class {
6920
7577
  }
6921
7578
  };
6922
7579
  //#endregion
7580
+ //#region src/sqlite/dice-repository.ts
7581
+ const MAX_ROLLS_LISTED = 200;
7582
+ function parseDefinition(text) {
7583
+ try {
7584
+ return JSON.parse(text);
7585
+ } catch (error) {
7586
+ throw new CorruptDataError("a dice check's definition is not valid JSON", { cause: error });
7587
+ }
7588
+ }
7589
+ var SqliteDiceRepository = class {
7590
+ db;
7591
+ clock;
7592
+ constructor(db, clock) {
7593
+ this.db = db;
7594
+ this.clock = clock;
7595
+ }
7596
+ define(definition) {
7597
+ if ((this.db.get("SELECT count(*) AS count FROM dice_checks")?.count ?? 0) >= 40) throw new InvalidValueError(`a story has at most 40 dice checks`);
7598
+ const { key, name, use, ...rest } = definition;
7599
+ return guard(() => {
7600
+ const { lastInsertRowid } = this.db.run("INSERT INTO dice_checks (key, name, use_text, definition) VALUES (?, ?, ?, ?)", [
7601
+ key,
7602
+ name,
7603
+ use,
7604
+ toJson(rest)
7605
+ ]);
7606
+ return this.require(Number(lastInsertRowid));
7607
+ });
7608
+ }
7609
+ checks() {
7610
+ return this.db.all("SELECT * FROM dice_checks ORDER BY id").map((row) => this.toCheck(row));
7611
+ }
7612
+ check(key) {
7613
+ const row = this.db.get("SELECT * FROM dice_checks WHERE key = ?", [key]);
7614
+ return row === void 0 ? void 0 : this.toCheck(row);
7615
+ }
7616
+ removeCheck(key) {
7617
+ if (this.db.run("DELETE FROM dice_checks WHERE key = ?", [key]).changes === 0) throw new NotFoundError(`there is no dice check "${key}"`);
7618
+ }
7619
+ record(roll) {
7620
+ const at = this.clock().toISOString();
7621
+ const { lastInsertRowid } = this.db.run("INSERT INTO dice_rolls (turn, check_key, expression, faces, kept, modifier, target, total, value, readout, reason, character, by, at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
7622
+ roll.turn,
7623
+ roll.check ?? null,
7624
+ roll.expression,
7625
+ toJson(roll.faces),
7626
+ toJson(roll.kept),
7627
+ roll.modifier,
7628
+ roll.target ?? null,
7629
+ roll.total,
7630
+ roll.value,
7631
+ roll.readout === void 0 ? null : toJson(roll.readout),
7632
+ roll.reason,
7633
+ roll.character ?? null,
7634
+ roll.by,
7635
+ at
7636
+ ]);
7637
+ return {
7638
+ ...roll,
7639
+ id: Number(lastInsertRowid),
7640
+ at
7641
+ };
7642
+ }
7643
+ rolls(filter = {}) {
7644
+ const limit = Math.min(filter.limit ?? MAX_ROLLS_LISTED, MAX_ROLLS_LISTED);
7645
+ return (filter.turn === void 0 ? this.db.all("SELECT * FROM (SELECT * FROM dice_rolls ORDER BY id DESC LIMIT ?) ORDER BY id", [limit]) : this.db.all("SELECT * FROM dice_rolls WHERE turn = ? ORDER BY id LIMIT ?", [filter.turn, limit])).map((row) => this.toRoll(row));
7646
+ }
7647
+ require(id) {
7648
+ const row = this.db.get("SELECT * FROM dice_checks WHERE id = ?", [id]);
7649
+ if (row === void 0) throw new NotFoundError(`there is no dice check ${id}`);
7650
+ return this.toCheck(row);
7651
+ }
7652
+ toCheck(row) {
7653
+ return {
7654
+ id: row.id,
7655
+ key: row.key,
7656
+ name: row.name,
7657
+ use: row.use_text,
7658
+ ...parseDefinition(row.definition)
7659
+ };
7660
+ }
7661
+ toRoll(row) {
7662
+ return {
7663
+ id: row.id,
7664
+ turn: row.turn,
7665
+ expression: row.expression,
7666
+ faces: parseNumberList(row.faces, "dice_rolls.faces"),
7667
+ kept: parseNumberList(row.kept, "dice_rolls.kept"),
7668
+ modifier: row.modifier,
7669
+ total: row.total,
7670
+ value: row.value,
7671
+ reason: row.reason,
7672
+ by: row.by,
7673
+ at: row.at,
7674
+ ...row.check_key === null ? {} : { check: row.check_key },
7675
+ ...row.target === null ? {} : { target: row.target },
7676
+ ...row.readout === null ? {} : { readout: JSON.parse(row.readout) },
7677
+ ...row.character === null ? {} : { character: row.character }
7678
+ };
7679
+ }
7680
+ };
7681
+ //#endregion
6923
7682
  //#region src/sqlite/clock-repository.ts
6924
7683
  const toChange = (row) => ({
6925
7684
  id: row.id,
@@ -8063,7 +8822,7 @@ var SqliteMetadataRepository = class {
8063
8822
  //#region src/sqlite/schema.ts
8064
8823
  /**
8065
8824
  * Migrations are applied in order and never edited once released (0.1.0 released
8066
- * versions 1 to 6; tests/sqlite/migrate.spec.ts pins them): a change to the schema
8825
+ * versions 1 to 6; tests/sqlite/migrate.spec.ts pins the released ones): a change to the schema
8067
8826
  * is a new entry. Enumerations are spelled out so that a later change to the model
8068
8827
  * cannot alter what an old migration did.
8069
8828
  */
@@ -8627,6 +9386,51 @@ const MIGRATIONS = [
8627
9386
  INSERT INTO entity_knowledge SELECT * FROM old_entity_knowledge;
8628
9387
  DROP TABLE old_entity_knowledge;
8629
9388
  CREATE INDEX entity_knowledge_subject ON entity_knowledge (subject_kind, subject_id);
9389
+ `
9390
+ },
9391
+ {
9392
+ version: 7,
9393
+ sql: `
9394
+ ALTER TABLE memories ADD COLUMN confidence TEXT NOT NULL DEFAULT 'likely' CHECK (confidence IN ('doubtful', 'likely', 'sure'));
9395
+ CREATE TABLE memory_evidence (
9396
+ memory_id INTEGER NOT NULL REFERENCES memories (id) ON DELETE CASCADE,
9397
+ perception_id INTEGER NOT NULL REFERENCES perceptions (id) ON DELETE CASCADE,
9398
+ relation TEXT NOT NULL CHECK (relation IN ('supports', 'contradicts')),
9399
+ turn INTEGER NOT NULL,
9400
+ PRIMARY KEY (memory_id, perception_id)
9401
+ ) STRICT;
9402
+ CREATE INDEX memory_evidence_perception ON memory_evidence (perception_id);
9403
+ ALTER TABLE routine_windows ADD COLUMN filters TEXT NOT NULL DEFAULT '[]';
9404
+ `
9405
+ },
9406
+ {
9407
+ version: 8,
9408
+ sql: `
9409
+ CREATE TABLE dice_checks (
9410
+ id INTEGER PRIMARY KEY,
9411
+ key TEXT NOT NULL UNIQUE,
9412
+ name TEXT NOT NULL,
9413
+ use_text TEXT NOT NULL,
9414
+ definition TEXT NOT NULL
9415
+ ) STRICT;
9416
+ CREATE TABLE dice_rolls (
9417
+ id INTEGER PRIMARY KEY,
9418
+ turn INTEGER NOT NULL,
9419
+ check_key TEXT,
9420
+ expression TEXT NOT NULL,
9421
+ faces TEXT NOT NULL,
9422
+ kept TEXT NOT NULL,
9423
+ modifier INTEGER NOT NULL,
9424
+ target INTEGER,
9425
+ total INTEGER NOT NULL,
9426
+ value INTEGER NOT NULL,
9427
+ readout TEXT,
9428
+ reason TEXT NOT NULL,
9429
+ character TEXT,
9430
+ by TEXT NOT NULL CHECK (by IN ('agent', 'player')),
9431
+ at TEXT NOT NULL
9432
+ ) STRICT;
9433
+ CREATE INDEX dice_rolls_turn ON dice_rolls (turn, id);
8630
9434
  `
8631
9435
  }
8632
9436
  ];
@@ -9375,6 +10179,7 @@ var SqliteSessionStore = class {
9375
10179
  routines;
9376
10180
  engagements;
9377
10181
  triggers;
10182
+ dice;
9378
10183
  recallIndex;
9379
10184
  eventRepository;
9380
10185
  constructor(db, clock, vectors) {
@@ -9405,6 +10210,7 @@ var SqliteSessionStore = class {
9405
10210
  this.routines = new SqliteRoutineRepository(db);
9406
10211
  this.engagements = new SqliteEngagementRepository(db);
9407
10212
  this.triggers = new SqliteTriggerRepository(db);
10213
+ this.dice = new SqliteDiceRepository(db, clock);
9408
10214
  this.eventRepository = new SqliteEventRepository(db, clock, this.recallIndex, (id) => this.locations.get(id)?.name, (id) => this.items.get(id)?.name);
9409
10215
  this.events = this.eventRepository;
9410
10216
  this.memories = new SqliteMemoryRepository(db, clock, this.recallIndex);
@@ -9811,6 +10617,375 @@ function identityOf(database, storeOptions) {
9811
10617
  }
9812
10618
  }
9813
10619
  //#endregion
10620
+ //#region src/records/dice.ts
10621
+ const systemRoller = (sides) => randomInt(1, sides + 1);
10622
+ const TERM = /^(?:(\d*)d(\d+|%)(?:(kh|kl|dh|dl)(\d*))?|(\d+))$/;
10623
+ /** Reads dice notation: `2d6+1`, `d20`, `4d6kh3`, `d%`, `1d8+1d4-1`. @throws {InvalidValueError} with what is wrong. */
10624
+ function parseNotation(input) {
10625
+ const source = input.toLowerCase().replace(/\s+/g, "");
10626
+ if (source === "") throw new InvalidValueError("dice notation is empty: write it like 2d6+1");
10627
+ const parts = source.split(/(?=[+-])/);
10628
+ if (parts.length > 8) throw new InvalidValueError(`dice notation has more than 8 terms`);
10629
+ const terms = parts.map((part, index) => {
10630
+ const sign = part.startsWith("-") ? -1 : 1;
10631
+ const body = part.replace(/^[+-]/, "");
10632
+ if (index > 0 && !/^[+-]/.test(part)) throw new InvalidValueError(`"${input}" is not dice notation: write it like 2d6+1`);
10633
+ const match = TERM.exec(body);
10634
+ if (match === null) throw new InvalidValueError(`"${part}" is not dice notation: write it like 2d6+1, 4d6kh3 or d100`);
10635
+ if (match[5] !== void 0) return {
10636
+ sign,
10637
+ constant: Number(match[5])
10638
+ };
10639
+ const count = match[1] === "" || match[1] === void 0 ? 1 : Number(match[1]);
10640
+ const sides = match[2] === "%" ? 100 : Number(match[2]);
10641
+ if (count < 1 || count > 100) throw new InvalidValueError(`"${part}": roll from 1 to 100 dice at a time`);
10642
+ if (sides < 2 || sides > 1e3) throw new InvalidValueError(`"${part}": a die has from 2 to ${MAX_DICE_SIDES} sides`);
10643
+ const mode = match[3];
10644
+ if (mode === void 0) return {
10645
+ sign,
10646
+ count,
10647
+ sides
10648
+ };
10649
+ const n = match[4] === "" || match[4] === void 0 ? 1 : Number(match[4]);
10650
+ if (n < 1 || n >= count) throw new InvalidValueError(`"${part}": ${mode.startsWith("k") ? "keep" : "drop"} at least 1 and fewer than the ${count} dice`);
10651
+ return {
10652
+ sign,
10653
+ count,
10654
+ sides,
10655
+ keep: {
10656
+ mode,
10657
+ n
10658
+ }
10659
+ };
10660
+ });
10661
+ if (!terms.some((term) => "sides" in term)) throw new InvalidValueError(`"${input}" has no dice: write it like 2d6+1`);
10662
+ return {
10663
+ text: source,
10664
+ terms
10665
+ };
10666
+ }
10667
+ /** The lowest and highest total the dice and constants can give. */
10668
+ function notationRange(notation) {
10669
+ let min = 0;
10670
+ let max = 0;
10671
+ for (const term of notation.terms) {
10672
+ if ("constant" in term) {
10673
+ min += term.sign * term.constant;
10674
+ max += term.sign * term.constant;
10675
+ continue;
10676
+ }
10677
+ const kept = term.keep === void 0 ? term.count : term.keep.mode.startsWith("k") ? term.keep.n : term.count - term.keep.n;
10678
+ const low = kept;
10679
+ const high = kept * term.sides;
10680
+ min += term.sign === 1 ? low : -high;
10681
+ max += term.sign === 1 ? high : -low;
10682
+ }
10683
+ return {
10684
+ min,
10685
+ max
10686
+ };
10687
+ }
10688
+ function keepOf(faces, keep) {
10689
+ const ordered = faces.map((face, index) => ({
10690
+ face,
10691
+ index
10692
+ })).sort((a, b) => a.face - b.face || a.index - b.index);
10693
+ const count = keep.mode.startsWith("k") ? keep.n : faces.length - keep.n;
10694
+ return (keep.mode === "kh" || keep.mode === "dl" ? ordered.slice(ordered.length - count) : ordered.slice(0, count)).sort((a, b) => a.index - b.index).map((entry) => entry.face);
10695
+ }
10696
+ function rollNotation(notation, roll = systemRoller) {
10697
+ const faces = [];
10698
+ const kept = [];
10699
+ let constant = 0;
10700
+ let total = 0;
10701
+ for (const term of notation.terms) {
10702
+ if ("constant" in term) {
10703
+ constant += term.sign * term.constant;
10704
+ total += term.sign * term.constant;
10705
+ continue;
10706
+ }
10707
+ const thrown = Array.from({ length: term.count }, () => roll(term.sides));
10708
+ const counted = term.keep === void 0 ? thrown : keepOf(thrown, term.keep);
10709
+ faces.push(...thrown);
10710
+ kept.push(...counted);
10711
+ total += term.sign * counted.reduce((sum, face) => sum + face, 0);
10712
+ }
10713
+ return {
10714
+ faces,
10715
+ kept,
10716
+ constant,
10717
+ total
10718
+ };
10719
+ }
10720
+ const within = (band, value) => (band.min === void 0 || value >= band.min) && (band.max === void 0 || value <= band.max);
10721
+ /** What the bands of a check make of a value. @throws {InvalidValueError} when no band holds it. */
10722
+ function readBands(bands, value) {
10723
+ const band = bands.find((candidate) => within(candidate, value));
10724
+ if (band === void 0) throw new InvalidValueError(`no band of this check holds ${value}: the check's bands leave a gap`);
10725
+ return {
10726
+ label: band.label,
10727
+ result: band.result,
10728
+ critical: band.critical === true
10729
+ };
10730
+ }
10731
+ /**
10732
+ * Rolls a check: the dice, the character's modifier and any bonus added, then the value read against the bands. With the reading
10733
+ * `successes` the total is the number of dice that hit. A check read against a target needs one.
10734
+ * @throws {InvalidValueError} for a target a check needs and is not given, or bands that leave the value unread.
10735
+ */
10736
+ function rollCheck(check, input, roll = systemRoller) {
10737
+ const rolled = rollNotation(parseNotation(check.dice), roll);
10738
+ const comparison = check.compare ?? "absolute";
10739
+ if (comparison !== "absolute" && input.target === void 0) throw new InvalidValueError(`the check "${check.key}" is read against a target (${comparison === "over" ? "the number to meet or beat" : "the number to stay under"}): give one`);
10740
+ const added = (input.modifier ?? 0) + (input.bonus ?? 0);
10741
+ const total = check.reading === "successes" ? rolled.kept.filter((face) => face >= (check.hit ?? 0)).length + (input.bonus ?? 0) : rolled.total + added;
10742
+ const value = comparison === "over" ? total - (input.target ?? 0) : comparison === "under" ? (input.target ?? 0) - total : total;
10743
+ return {
10744
+ faces: rolled.faces,
10745
+ kept: rolled.kept,
10746
+ modifier: check.reading === "successes" ? input.bonus ?? 0 : rolled.constant + added,
10747
+ ...input.target === void 0 ? {} : { target: input.target },
10748
+ total,
10749
+ value,
10750
+ readout: readBands(check.bands, value)
10751
+ };
10752
+ }
10753
+ /** What is wrong with a check as a world defines it; nothing when it is sound. */
10754
+ function checkProblems(check) {
10755
+ const problems = [];
10756
+ if (!/^[a-z][a-z0-9_-]{0,39}$/.test(check.key)) problems.push(`the key "${check.key}" must be lower-case letters, digits, - or _, starting with a letter`);
10757
+ if (check.name.trim() === "") problems.push("it needs a name");
10758
+ if (check.use.trim() === "") problems.push("it needs a \"use\": when the game master rolls it");
10759
+ if (check.compare !== void 0 && !DICE_COMPARISONS.includes(check.compare)) problems.push(`"compare" must be one of: ${DICE_COMPARISONS.join(", ")}`);
10760
+ if (check.reading !== void 0 && !DICE_READINGS.includes(check.reading)) problems.push(`"reading" must be one of: ${DICE_READINGS.join(", ")}`);
10761
+ let notation;
10762
+ try {
10763
+ notation = parseNotation(check.dice);
10764
+ } catch (error) {
10765
+ problems.push(`"dice": ${error instanceof Error ? error.message : String(error)}`);
10766
+ }
10767
+ if (check.bands.length === 0) problems.push("it needs at least one band");
10768
+ if (check.bands.length > 20) problems.push(`it has more than 20 bands`);
10769
+ check.bands.forEach((band, index) => {
10770
+ if (band.label.trim() === "") problems.push(`band ${index + 1} needs a label`);
10771
+ if (!DICE_RESULTS.includes(band.result)) problems.push(`band ${index + 1}: "result" must be one of: ${DICE_RESULTS.join(", ")}`);
10772
+ if (band.min !== void 0 && band.max !== void 0 && band.min > band.max) problems.push(`band ${index + 1}: min is above max`);
10773
+ });
10774
+ if (check.reading === "successes") {
10775
+ const sides = notation?.terms.flatMap((term) => "sides" in term ? [term.sides] : []) ?? [];
10776
+ if (check.hit === void 0 || !Number.isInteger(check.hit) || check.hit < 1 || sides.length > 0 && check.hit > Math.max(...sides)) problems.push("the reading \"successes\" needs \"hit\": the lowest face that counts");
10777
+ }
10778
+ if (notation !== void 0 && problems.length === 0 && (check.compare ?? "absolute") === "absolute" && check.modifier === void 0) {
10779
+ const { min, max } = check.reading === "successes" ? {
10780
+ min: 0,
10781
+ max: notation.terms.reduce((sum, term) => sum + ("count" in term ? term.count : 0), 0)
10782
+ } : notationRange(notation);
10783
+ for (let value = min; value <= max; value += 1) if (!check.bands.some((band) => within(band, value))) {
10784
+ problems.push(`no band holds ${value}, which the dice can give`);
10785
+ break;
10786
+ }
10787
+ }
10788
+ return problems;
10789
+ }
10790
+ function rollPlain(expression, roll = systemRoller) {
10791
+ const notation = parseNotation(expression);
10792
+ const rolled = rollNotation(notation, roll);
10793
+ return {
10794
+ expression: notation.text,
10795
+ faces: rolled.faces,
10796
+ kept: rolled.kept,
10797
+ modifier: rolled.constant,
10798
+ total: rolled.total
10799
+ };
10800
+ }
10801
+ //#endregion
10802
+ //#region src/tools/dice.ts
10803
+ const ROLL_DICE_TOOL = "roll_dice";
10804
+ const ROLL_CHECK_TOOL = "roll_check";
10805
+ const LIST_CHECKS_TOOL = "list_checks";
10806
+ const sign = (value) => value >= 0 ? `+${value}` : String(value);
10807
+ /** How a roll reads to the agent that asked for it: what fell, what it adds up to, and what the world's check makes of it. */
10808
+ function describeRoll(roll, check) {
10809
+ const dice = `[${roll.faces.join(", ")}]${roll.kept.length < roll.faces.length ? ` keeping [${roll.kept.join(", ")}]` : ""}`;
10810
+ const lines = [`Roll ${roll.id}: ${roll.expression}${check === void 0 ? "" : ` for the check "${check.name}"`}${roll.character === void 0 ? "" : ` (${roll.character})`} - ${dice}${roll.modifier === 0 ? "" : ` ${sign(roll.modifier)}`} = ${roll.total}${check?.reading === "successes" ? " successes" : ""}.`];
10811
+ if (roll.target !== void 0) lines.push(`Against ${roll.target}: ${roll.value >= 0 ? "+" : ""}${roll.value}.`);
10812
+ if (roll.readout !== void 0) lines.push(`That is: ${roll.readout.label}${roll.readout.critical ? " (critical)" : ""}; the result is ${roll.readout.result}. This is decided by the dice: do not change it, narrate it.${check === void 0 || roll.character === void 0 ? "" : ` In resolve_round give ${roll.character}'s outcome this result and cite the roll: roll ${roll.id}.`}`);
10813
+ return lines.join(" ");
10814
+ }
10815
+ const rolled = (_args, value) => [{
10816
+ type: "text",
10817
+ text: value.text
10818
+ }];
10819
+ /**
10820
+ * Tools that roll dice for an agent, so that numbers are not made up. `roll_check` rolls a check the story defines and reads
10821
+ * the result with the check's own bands; `roll_dice` rolls plain notation for what has no check. The roller is the system's
10822
+ * source of randomness unless one is given.
10823
+ */
10824
+ function createDiceTools(resolve, options = {}) {
10825
+ const roller = options.roller ?? systemRoller;
10826
+ const linkOf = (agent) => {
10827
+ const found = agent === void 0 ? void 0 : resolve(agent.session.id);
10828
+ if (found === void 0) throw new NotFoundError("this agent is not working in a roleplay session, so there are no dice to roll");
10829
+ return found;
10830
+ };
10831
+ return [
10832
+ defineTool({
10833
+ name: LIST_CHECKS_TOOL,
10834
+ description: "List the checks this story defines for settling uncertain attempts: when each is used, the dice it rolls and how its result is read.",
10835
+ parameters: {},
10836
+ output: {
10837
+ schema: { type: "json" },
10838
+ render: (_args, value) => [{
10839
+ type: "text",
10840
+ text: JSON.stringify(value, null, 2)
10841
+ }]
10842
+ },
10843
+ isConcurrencySafe: () => true,
10844
+ execute(_args, exec) {
10845
+ const { session } = linkOf(exec.agent);
10846
+ return Promise.resolve(session.dice.checks().map((check) => ({
10847
+ key: check.key,
10848
+ name: check.name,
10849
+ use: check.use,
10850
+ dice: check.dice,
10851
+ compare: check.compare ?? "absolute",
10852
+ ...check.modifier === void 0 ? {} : { modifier: `the character's ${check.modifier}` },
10853
+ bands: check.bands.map((band) => ({
10854
+ ...band.min === void 0 ? {} : { from: band.min },
10855
+ ...band.max === void 0 ? {} : { to: band.max },
10856
+ label: band.label,
10857
+ result: band.result
10858
+ }))
10859
+ })));
10860
+ }
10861
+ }),
10862
+ defineTool({
10863
+ name: ROLL_CHECK_TOOL,
10864
+ description: "Roll one of the story's checks for an attempt whose outcome is uncertain and matters: the dice are rolled for you and the result is read with the check's own bands, so you never choose or make up a number. Use the check whose \"use\" fits (list_checks shows them). Roll once per attempt, before you decide what happens, and let the result stand: a failed roll is a failure, whatever the story would like.",
10865
+ parameters: {
10866
+ check: {
10867
+ type: "string",
10868
+ required: true,
10869
+ description: "The key of the check, as list_checks gives it."
10870
+ },
10871
+ reason: {
10872
+ type: "string",
10873
+ required: true,
10874
+ description: "What is attempted, by whom, in a sentence."
10875
+ },
10876
+ character: {
10877
+ type: "string",
10878
+ required: true,
10879
+ description: "The key of the character the attempt is for (the player's character included): the roll is theirs, and their outcome in resolve_round takes its result from it."
10880
+ },
10881
+ target: {
10882
+ type: "integer",
10883
+ description: "When the check is read against a target (list_checks says compare over or under): the difficulty, the number the total must meet or beat (or stay under). Set it by how hard the attempt is, using what the check's \"use\" says about difficulty; never 0 to make it easy."
10884
+ },
10885
+ bonus: {
10886
+ type: "integer",
10887
+ description: "What the circumstances add to or take from the roll, from -10 to 10; leave out for none."
10888
+ }
10889
+ },
10890
+ output: {
10891
+ schema: { type: "json" },
10892
+ render: rolled
10893
+ },
10894
+ isConcurrencySafe: () => false,
10895
+ execute(args, exec) {
10896
+ const { session, turn } = linkOf(exec.agent);
10897
+ const check = session.dice.check(args.check);
10898
+ if (check === void 0) throw new NotFoundError(`there is no check "${args.check}"; list_checks shows the story's checks`);
10899
+ if (session.characters.list().every((entry) => characterKey(entry) !== args.character)) throw new NotFoundError(`there is no character "${args.character}"; list_characters shows the keys`);
10900
+ let modifier;
10901
+ if (check.modifier !== void 0) {
10902
+ const who = session.characters.list().find((entry) => characterKey(entry) === args.character);
10903
+ if (who === void 0) throw new NotFoundError(`there is no character "${args.character}"; list_characters shows the keys`);
10904
+ const value = session.variables.value(check.modifier, "character", who.id);
10905
+ if (typeof value !== "number") throw new NotFoundError(`the character's ${check.modifier} is not a number, so the check cannot add it`);
10906
+ modifier = value;
10907
+ }
10908
+ const bonus = Math.max(-10, Math.min(10, args.bonus ?? 0));
10909
+ const made = rollCheck(check, {
10910
+ ...modifier === void 0 ? {} : { modifier },
10911
+ bonus,
10912
+ ...args.target === void 0 ? {} : { target: args.target }
10913
+ }, roller);
10914
+ const roll = session.dice.record({
10915
+ turn,
10916
+ check: check.key,
10917
+ expression: check.dice,
10918
+ faces: made.faces,
10919
+ kept: made.kept,
10920
+ modifier: made.modifier,
10921
+ ...made.target === void 0 ? {} : { target: made.target },
10922
+ total: made.total,
10923
+ value: made.value,
10924
+ readout: made.readout,
10925
+ reason: args.reason,
10926
+ character: args.character,
10927
+ by: options.by ?? "agent"
10928
+ });
10929
+ return Promise.resolve({
10930
+ roll: roll.id,
10931
+ check: check.key,
10932
+ total: roll.total,
10933
+ result: made.readout.result,
10934
+ label: made.readout.label,
10935
+ critical: made.readout.critical,
10936
+ text: describeRoll(roll, check)
10937
+ });
10938
+ }
10939
+ }),
10940
+ defineTool({
10941
+ name: ROLL_DICE_TOOL,
10942
+ description: "Roll dice for something the story has no check for (damage, a random pick, who goes first, a rumour table): the dice are rolled for you, so you never make up a number. Write the notation like 1d6, 2d4+1, 4d6kh3 (keep the highest 3) or d100. What the number means is for you to decide from the rules you were given; when a check fits the attempt, use roll_check instead.",
10943
+ parameters: {
10944
+ expression: {
10945
+ type: "string",
10946
+ required: true,
10947
+ description: "Dice notation, for example 2d6+1."
10948
+ },
10949
+ reason: {
10950
+ type: "string",
10951
+ required: true,
10952
+ description: "What the roll is for, in a sentence."
10953
+ },
10954
+ character: {
10955
+ type: "string",
10956
+ description: "The key of the character it is for, when it is for one."
10957
+ }
10958
+ },
10959
+ output: {
10960
+ schema: { type: "json" },
10961
+ render: rolled
10962
+ },
10963
+ isConcurrencySafe: () => false,
10964
+ execute(args, exec) {
10965
+ const { session, turn } = linkOf(exec.agent);
10966
+ const made = rollPlain(args.expression, roller);
10967
+ const roll = session.dice.record({
10968
+ turn,
10969
+ expression: made.expression,
10970
+ faces: made.faces,
10971
+ kept: made.kept,
10972
+ modifier: made.modifier,
10973
+ total: made.total,
10974
+ value: made.total,
10975
+ reason: args.reason,
10976
+ ...args.character === void 0 ? {} : { character: args.character },
10977
+ by: options.by ?? "agent"
10978
+ });
10979
+ return Promise.resolve({
10980
+ roll: roll.id,
10981
+ total: roll.total,
10982
+ text: describeRoll(roll)
10983
+ });
10984
+ }
10985
+ })
10986
+ ];
10987
+ }
10988
+ //#endregion
9814
10989
  //#region src/records/lint.ts
9815
10990
  /**
9816
10991
  * What is inconsistent or unfinished in the world's graph of places and characters. Things made up in
@@ -10004,8 +11179,9 @@ function lintWorld(target) {
10004
11179
  //#region src/records/agenda.ts
10005
11180
  /** An engagement still pending this long after it was due (or over) is overdue. */
10006
11181
  const ENGAGEMENT_GRACE = 30 * MINUTE;
10007
- function occurrenceOn(window, day, zone) {
11182
+ function occurrenceOn(window, day, zone, calendar) {
10008
11183
  if (window.days.length > 0 && !window.days.includes(day.weekday)) return void 0;
11184
+ if (window.on.length > 0 && !dayMatches(window.on, day, calendar)) return void 0;
10009
11185
  const startsAt = zonedInstant({
10010
11186
  ...day,
10011
11187
  hour: Math.floor(window.start / 60),
@@ -10022,15 +11198,15 @@ function occurrenceOn(window, day, zone) {
10022
11198
  };
10023
11199
  }
10024
11200
  /** The window that holds at an instant (the first of the list that does), with the stretch of this occurrence of it. */
10025
- function occurrenceAt(windows, now, zone) {
11201
+ function occurrenceAt(windows, now, zone, calendar = NO_CALENDAR) {
10026
11202
  const days = [addLocalDays(now, zone, 0), addLocalDays(now, zone, -1)];
10027
11203
  for (const window of windows) for (const day of days) {
10028
- const occurrence = occurrenceOn(window, day, zone);
11204
+ const occurrence = occurrenceOn(window, day, zone, calendar);
10029
11205
  if (occurrence !== void 0 && occurrence.startsAt <= now && now < occurrence.endsAt) return occurrence;
10030
11206
  }
10031
11207
  }
10032
11208
  /** The window that starts next after an instant, within `horizon`. */
10033
- function nextOccurrence(windows, now, zone, horizon) {
11209
+ function nextOccurrence(windows, now, zone, horizon, calendar = NO_CALENDAR) {
10034
11210
  let best;
10035
11211
  for (const offset of [
10036
11212
  0,
@@ -10039,7 +11215,7 @@ function nextOccurrence(windows, now, zone, horizon) {
10039
11215
  ]) {
10040
11216
  const day = addLocalDays(now, zone, offset);
10041
11217
  for (const window of windows) {
10042
- const occurrence = occurrenceOn(window, day, zone);
11218
+ const occurrence = occurrenceOn(window, day, zone, calendar);
10043
11219
  if (occurrence === void 0 || occurrence.startsAt <= now || occurrence.startsAt > now + horizon) continue;
10044
11220
  if (best === void 0 || occurrence.startsAt < best.startsAt) best = occurrence;
10045
11221
  }
@@ -10072,8 +11248,9 @@ const DEFAULTS = {
10072
11248
  function agendaAt(source, characterId, now, zone, options = {}) {
10073
11249
  const horizon = options.horizon ?? DEFAULTS.horizon;
10074
11250
  const windows = source.routines.list(characterId);
10075
- const current = occurrenceAt(windows, now, zone);
10076
- const next = nextOccurrence(windows, now, zone, horizon);
11251
+ const calendar = source.metadata === void 0 ? NO_CALENDAR : readCalendar(source.metadata);
11252
+ const current = occurrenceAt(windows, now, zone, calendar);
11253
+ const next = nextOccurrence(windows, now, zone, horizon, calendar);
10077
11254
  const engagements = source.engagements.list({
10078
11255
  status: "pending",
10079
11256
  participant: characterId,
@@ -11396,7 +12573,8 @@ function createTools(resolve, options) {
11396
12573
  //#region src/plugin.ts
11397
12574
  const Config = z.object({
11398
12575
  journalMode: z.union(["delete", "wal"]).default("delete"),
11399
- writeTools: z.boolean().default(false)
12576
+ writeTools: z.boolean().default(false),
12577
+ diceTools: z.boolean().default(false)
11400
12578
  });
11401
12579
  /**
11402
12580
  * Roleplay session state for other plugins. It creates and opens sessions
@@ -11422,6 +12600,7 @@ var RoleplayState = class extends Service {
11422
12600
  };
11423
12601
  ctx.inject(["tools"], (scope) => {
11424
12602
  for (const tool of createTools(resolve, { write: config.writeTools ?? false })) scope.tools.register(tool);
12603
+ if (config.diceTools === true) for (const tool of createDiceTools(resolve)) scope.tools.register(tool);
11425
12604
  });
11426
12605
  }
11427
12606
  /**
@@ -11557,6 +12736,7 @@ var RoleplayState = class extends Service {
11557
12736
  routines: store.routines,
11558
12737
  engagements: store.engagements,
11559
12738
  triggers: store.triggers,
12739
+ dice: store.dice,
11560
12740
  planMove: (characterId, move) => store.planMove(characterId, move),
11561
12741
  planKnownMove: (characterId, move) => store.planKnownMove(characterId, move),
11562
12742
  move: (characterId, move) => store.move(characterId, move),
@@ -11826,6 +13006,6 @@ function createCharacterTools(link, characterId) {
11826
13006
  ];
11827
13007
  }
11828
13008
  //#endregion
11829
- 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, 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, dayPart, deleteSession, deliverNotes, 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, placeholdersOf, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, renderWords, retention, runTriggers, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemTimeZone, tokens, triggerReferenceProblems, valueIssue, zonedInstant };
13009
+ 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, DICE_COMPARISONS, DICE_READINGS, DICE_RESULTS, 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, LIST_CHECKS_TOOL, LORE_KINDS, LORE_SUBJECT_KINDS, MAX_DICE_BANDS, MAX_DICE_CHECKS, MAX_DICE_PER_ROLL, MAX_DICE_SIDES, MAX_DICE_TERMS, 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, ROLL_CHECK_TOOL, ROLL_DICE_TOOL, 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, checkProblems, clarityAt, clockTime, conditionHolds, createCharacterTools, createDiceTools, createSession, createSessionStore, createTools, dayMatches, dayPart, dayTokenProblems, deleteSession, deliverNotes, denseScore, describeDuration, describeHolder, describeInstant, describeRoll, 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, notationRange, occurrenceAt, openSession, openSessionStore, overlap, parseDayToken, parseInstant, parseNotation, parseRecordsEdit, parseTimeOfDay, parseTriggerDefinition, placeholdersOf, planItemChange, planKnownMove, planMove, planVariableChange, poolCandidates, rankGoals, readBands, readCalendar, readRecords, readTriggerDefinition, recall, recallCandidates, recallPool, recordsEditSchema, renderWords, retention, rollCheck, rollNotation, rollPlain, runTriggers, seasonOf, seededRoll, sessionDatabase, sessionFolder, shownVariables, subjectOf, systemClock, systemRoller, systemTimeZone, tokens, translated, translation, translator, triggerReferenceProblems, valueIssue, writeCalendar, zonedInstant };
11830
13010
 
11831
13011
  //# sourceMappingURL=index.js.map