pi-roundtable 0.22.5 → 0.23.0
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/CHANGELOG.md +55 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +350 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { OutfitId } from "./catalog.ts";
|
|
2
|
+
import type { Part, Tones } from "./colors.ts";
|
|
3
|
+
|
|
4
|
+
const WOOD = "#8b5a33";
|
|
5
|
+
const STEEL = "#c7cfdd";
|
|
6
|
+
const STEELD = "#9aa5ba";
|
|
7
|
+
const LEATHER = "#a6713f";
|
|
8
|
+
|
|
9
|
+
const T = "M31 94C29 79 35 71 50 71C65 71 71 79 69 94Z";
|
|
10
|
+
export const hand = (v: Tones, x: number, y: number, r = 4.8): string =>
|
|
11
|
+
`<circle cx="${x}" cy="${y}" r="${r}" fill="${v.k}"/>`;
|
|
12
|
+
const torso = (fill: string): string => `<path d="${T}" fill="${fill}"/>`;
|
|
13
|
+
const arms = (fill: string): string =>
|
|
14
|
+
`<ellipse cx="28.5" cy="80" rx="5.2" ry="8" transform="rotate(14 28.5 80)" fill="${fill}"/><ellipse cx="71.5" cy="80" rx="5.2" ry="8" transform="rotate(-14 71.5 80)" fill="${fill}"/>`;
|
|
15
|
+
export const star5 = (x: number, y: number, fill: string): string =>
|
|
16
|
+
`<path d="M${x} ${y - 4}l1.5 3.2 3.5.4-2.6 2.4.7 3.5-3.1-1.8-3.1 1.8.7-3.5-2.6-2.4 3.5-.4z" fill="${fill}"/>`;
|
|
17
|
+
|
|
18
|
+
export const OUTFITS: Record<OutfitId, Part> = {
|
|
19
|
+
robe: (v) =>
|
|
20
|
+
arms(v.p) +
|
|
21
|
+
torso(v.p) +
|
|
22
|
+
`<path d="M30 89H70L69.6 94H30.4Z" fill="${v.t}"/>` +
|
|
23
|
+
star5(50, 77, v.t) +
|
|
24
|
+
hand(v, 27, 88, 4.4),
|
|
25
|
+
scholar: (v) =>
|
|
26
|
+
arms(v.pd) +
|
|
27
|
+
torso(v.pd) +
|
|
28
|
+
`<path d="M39 71.5L50 85L61 71.5Z" fill="${v.s}"/><path d="M50 85V94" stroke="${v.t}" stroke-width="1.6"/><path d="M33 88H67" stroke="${v.p}" stroke-width="2.4" opacity=".7"/>`,
|
|
29
|
+
armor: (v) =>
|
|
30
|
+
arms(STEEL) +
|
|
31
|
+
torso(STEEL) +
|
|
32
|
+
`<path d="M44 71H56V94H44Z" fill="${v.p}"/><path d="M44 80H56" stroke="${v.t}" stroke-width="2"/><circle cx="31" cy="75.5" r="6.4" fill="${STEELD}"/><circle cx="69" cy="75.5" r="6.4" fill="${STEELD}"/><circle cx="29.5" cy="73.5" r="2" fill="#fff" opacity=".6"/><circle cx="67.5" cy="73.5" r="2" fill="#fff" opacity=".6"/>`,
|
|
33
|
+
plate: (v) =>
|
|
34
|
+
arms("#e6eaf3") +
|
|
35
|
+
torso("#e6eaf3") +
|
|
36
|
+
`<path d="M44 71H56L58 94H42Z" fill="${v.s}" stroke="${v.t}" stroke-width="1.6"/><path d="M50 74V92M45 81H55" stroke="${v.p}" stroke-width="3"/><circle cx="31" cy="75.5" r="6.4" fill="${v.t}"/><circle cx="69" cy="75.5" r="6.4" fill="${v.t}"/><circle cx="29.5" cy="73.5" r="2" fill="#fff" opacity=".6"/><circle cx="67.5" cy="73.5" r="2" fill="#fff" opacity=".6"/>`,
|
|
37
|
+
apron: (v) =>
|
|
38
|
+
arms(v.s) +
|
|
39
|
+
torso(v.s) +
|
|
40
|
+
`<path d="M38 71H62L66 94H34Z" fill="${LEATHER}"/><path d="M38 71L42 75M62 71L58 75" stroke="#7a4f27" stroke-width="2"/><rect x="43.5" y="82" width="13" height="8" rx="2" fill="#8d5c30"/><rect x="24" y="85" width="9" height="3" rx="1.5" fill="#8d5c30"/>`,
|
|
41
|
+
chef: (v) =>
|
|
42
|
+
arms("#fff") +
|
|
43
|
+
torso("#fff") +
|
|
44
|
+
`<path d="M50 72V94" stroke="#cfd5e4" stroke-width="1.4"/><circle cx="45" cy="79" r="1.7" fill="${v.t}"/><circle cx="45" cy="87" r="1.7" fill="${v.t}"/><circle cx="55" cy="79" r="1.7" fill="${v.t}"/><circle cx="55" cy="87" r="1.7" fill="${v.t}"/><path d="M41 71.5Q50 77 59 71.5L57 75Q50 79 43 75Z" fill="${v.p}"/>`,
|
|
45
|
+
cloak: (v) =>
|
|
46
|
+
`<path d="M26 79Q50 66 74 79L72 94H28Z" fill="${v.pd}"/>` +
|
|
47
|
+
arms(v.p) +
|
|
48
|
+
torso(v.p) +
|
|
49
|
+
`<path d="M32 87H68" stroke="${WOOD}" stroke-width="3.2"/><rect x="46" y="84.8" width="8" height="4.4" rx="1.4" fill="${v.t}"/><circle cx="50" cy="72.5" r="2.6" fill="${v.t}"/>`,
|
|
50
|
+
tunic: (v) =>
|
|
51
|
+
arms(v.p) +
|
|
52
|
+
torso(v.p) +
|
|
53
|
+
`<path d="M31 83L69 76V82L31 89Z" fill="${v.s}"/><circle cx="44" cy="90" r="1.8" fill="${v.t}"/><circle cx="56" cy="88" r="1.8" fill="${v.t}"/><path d="M37 71.5q3 5 6.3 0t6.4 0t6.3 0t6.3 0V75H37Z" fill="${v.s}"/>`,
|
|
54
|
+
coat: (v) =>
|
|
55
|
+
arms(v.pd) +
|
|
56
|
+
torso(v.pd) +
|
|
57
|
+
`<path d="M50 72V94" stroke="${v.ink}" stroke-width="1" opacity=".35"/><path d="M40 71.5L47 84L50 72ZM60 71.5L53 84L50 72Z" fill="${v.s}"/><circle cx="46" cy="87" r="1.8" fill="${v.t}"/><circle cx="54" cy="87" r="1.8" fill="${v.t}"/><circle cx="46" cy="80" r="1.8" fill="${v.t}" opacity="0"/>`,
|
|
58
|
+
overalls: (v) =>
|
|
59
|
+
arms(v.s) +
|
|
60
|
+
torso(v.s) +
|
|
61
|
+
`<path d="M36 80H64V94H36Z" fill="${v.p}"/><path d="M39 80L40 71.4M61 80L60 71.4" stroke="${v.p}" stroke-width="4"/><rect x="44.5" y="83" width="11" height="6" rx="1.5" fill="${v.pd}"/><circle cx="40" cy="79" r="1.6" fill="${v.t}"/><circle cx="60" cy="79" r="1.6" fill="${v.t}"/>`,
|
|
62
|
+
vest: (v) =>
|
|
63
|
+
arms(v.s) +
|
|
64
|
+
torso(v.s) +
|
|
65
|
+
`<path d="M37 71.5L45 94H36ZM63 71.5L55 94H64Z" fill="${v.p}"/><path d="M40 83Q50 90 60 83" fill="none" stroke="${v.t}" stroke-width="1.6"/><circle cx="50" cy="86" r="2" fill="${v.t}"/>`,
|
|
66
|
+
habit: (v) =>
|
|
67
|
+
arms(v.p) +
|
|
68
|
+
`<path d="M29 94C27 78 35 71 50 71C65 71 73 78 71 94Z" fill="${v.p}"/><path d="M40 71.5L50 82L60 71.5" fill="none" stroke="${v.s}" stroke-width="3.2" stroke-linejoin="round"/><path d="M29 87H71" stroke="${v.s}" stroke-width="3"/><circle cx="50" cy="87" r="2" fill="${v.t}"/>`,
|
|
69
|
+
surplice: (v) =>
|
|
70
|
+
arms(v.s) +
|
|
71
|
+
torso(v.s) +
|
|
72
|
+
`<path d="M46 74H54V79H59V85H54V93H46V85H41V79H46Z" fill="${v.p}"/><path d="M31 90H69" stroke="${v.sd}" stroke-width="2"/>`,
|
|
73
|
+
motley: (v) =>
|
|
74
|
+
arms(v.p) +
|
|
75
|
+
torso(v.p) +
|
|
76
|
+
`<path d="M50 71C65 71 71 79 69 94H50Z" fill="${v.s}"/><path d="M71.5 72A8 8 0 0 1 71.5 88Z" fill="${v.s}" opacity="0"/><path d="M41 82L50 90L59 82L50 74Z" fill="${v.t}" opacity=".9"/><path d="M36 71.5q3 5 6.5 0t7.5 0t7.5 0t6.5 0V75H36Z" fill="${v.t}"/>`,
|
|
77
|
+
sailor: (v) =>
|
|
78
|
+
arms(v.s) +
|
|
79
|
+
torso(v.s) +
|
|
80
|
+
`<path d="M31 78H69M31 84H69M31 90H69" stroke="${v.p}" stroke-width="2.6"/><path d="M38 71.5L50 82L62 71.5L56 71Q50 76 44 71Z" fill="${v.pd}"/>`,
|
|
81
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { PropId } from "./catalog.ts";
|
|
2
|
+
import type { Part } from "./colors.ts";
|
|
3
|
+
import { hand, star5 } from "./parts-outfits.ts";
|
|
4
|
+
|
|
5
|
+
const WOOD = "#8b5a33";
|
|
6
|
+
const LEATHER = "#a6713f";
|
|
7
|
+
|
|
8
|
+
/** The part's markup as it is; the pivot its animation turns about is in `PROP_PIVOTS`. */
|
|
9
|
+
const held = (inner: string): string => inner;
|
|
10
|
+
|
|
11
|
+
/** Where each prop turns when it sweeps, in the 100-unit drawing (applied by characters.css). */
|
|
12
|
+
export const PROP_PIVOTS: Readonly<Record<PropId, string>> = {
|
|
13
|
+
staff: "73px 82px",
|
|
14
|
+
shield: "27px 82px",
|
|
15
|
+
scroll: "76px 76px",
|
|
16
|
+
hammer: "79px 84px",
|
|
17
|
+
bow: "78px 70px",
|
|
18
|
+
lute: "75px 70px",
|
|
19
|
+
flask: "73px 66px",
|
|
20
|
+
quill: "72px 82px",
|
|
21
|
+
tome: "78px 76px",
|
|
22
|
+
bell: "76px 62px",
|
|
23
|
+
keys: "78px 62px",
|
|
24
|
+
map: "77px 58px",
|
|
25
|
+
telescope: "76px 74px",
|
|
26
|
+
wrench: "78px 86px",
|
|
27
|
+
spade: "76px 60px",
|
|
28
|
+
ladle: "78px 78px",
|
|
29
|
+
lantern: "76px 62px",
|
|
30
|
+
seal: "76px 66px",
|
|
31
|
+
dagger: "78px 86px",
|
|
32
|
+
beads: "78px 62px",
|
|
33
|
+
bird: "78px 76px",
|
|
34
|
+
anchor: "76px 76px",
|
|
35
|
+
marotte: "76px 62px",
|
|
36
|
+
sword: "76px 82px",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const PROPS: Record<PropId, Part> = {
|
|
40
|
+
staff: (v) =>
|
|
41
|
+
held(
|
|
42
|
+
`<circle class="glow" cx="73" cy="15" r="13" fill="${v.gl}"/><path d="M73 15V92" stroke="${WOOD}" stroke-width="4.2" stroke-linecap="round"/><circle cx="73" cy="14" r="6.4" fill="${v.gl}" stroke="${v.t}" stroke-width="2"/><circle cx="71" cy="12" r="1.8" fill="#fff" opacity=".85"/>${hand(v, 73, 82)}`,
|
|
43
|
+
),
|
|
44
|
+
shield: (v) =>
|
|
45
|
+
held(
|
|
46
|
+
`<path class="glow" d="M17 70H37V80Q37 89 27 93.5Q17 89 17 80Z" fill="none" stroke="${v.gl}" stroke-width="6"/><path d="M17 70H37V80Q37 89 27 93.5Q17 89 17 80Z" fill="${v.s}" stroke="${v.t}" stroke-width="2"/><path d="M17 70H27V93.5Q17 89 17 80Z" fill="${v.p}"/>` +
|
|
47
|
+
star5(27, 79, v.t),
|
|
48
|
+
),
|
|
49
|
+
scroll: (v) =>
|
|
50
|
+
held(
|
|
51
|
+
`<g transform="rotate(-16 76 74)"><path class="glow" d="M76 38l2.4 5.4 5.7.6-4.3 3.9 1.2 5.7-5-2.9-5 2.9 1.2-5.7-4.3-3.9 5.7-.6z" fill="${v.t}"/><rect x="69.5" y="56" width="13" height="30" rx="2" fill="#fbefd0" stroke="#c9a867" stroke-width="1.2"/><ellipse cx="76" cy="56" rx="7.4" ry="2.8" fill="#f1d99b" stroke="#c9a867" stroke-width="1.2"/><ellipse cx="76" cy="86" rx="7.4" ry="2.8" fill="#f1d99b" stroke="#c9a867" stroke-width="1.2"/><path d="M72.5 63H79.5M72.5 68H79.5M72.5 73H77" stroke="${v.p}" stroke-width="1.6" stroke-linecap="round"/></g>${hand(v, 75.5, 74, 4.6)}`,
|
|
52
|
+
),
|
|
53
|
+
hammer: (v) =>
|
|
54
|
+
held(
|
|
55
|
+
`<path class="glow" d="M89 49l1.8 4 4.2.4-3.2 2.9.9 4.2-3.7-2.2-3.7 2.2.9-4.2-3.2-2.9 4.2-.4z" fill="${v.t}"/><path d="M78 95L82 70" stroke="${WOOD}" stroke-width="4.4" stroke-linecap="round"/><g transform="rotate(8 81 66)"><rect x="68" y="59" width="26" height="14" rx="3.4" fill="#aab3c5" stroke="#7d879b" stroke-width="1.4"/><rect x="68" y="59" width="7" height="14" rx="3" fill="#8b95a9"/><path d="M73 61.4H90" stroke="#fff" stroke-width="1.6" stroke-linecap="round" opacity=".6"/></g>${hand(v, 79.4, 85)}`,
|
|
56
|
+
),
|
|
57
|
+
bow: (v) =>
|
|
58
|
+
held(
|
|
59
|
+
`<circle class="glow" cx="94" cy="70" r="8" fill="${v.gl}"/><path d="M74 40Q98 68 74 96" fill="none" stroke="${WOOD}" stroke-width="4.2" stroke-linecap="round"/><path d="M74 40V96" stroke="#9aa5ba" stroke-width="1" opacity=".9"/><path d="M66 70H94" stroke="${WOOD}" stroke-width="1.8" stroke-linecap="round"/><path d="M93 66.5L99 70L93 73.5Z" fill="${v.t}"/><path d="M66 70l-3.4-3M66 70l-3.4 3" stroke="${v.p}" stroke-width="1.8" stroke-linecap="round"/>${hand(v, 78, 70)}`,
|
|
60
|
+
),
|
|
61
|
+
lute: (v) =>
|
|
62
|
+
held(
|
|
63
|
+
`<g transform="rotate(18 76 80)"><path d="M74.2 70V44" stroke="${WOOD}" stroke-width="3.8"/><rect x="72.6" y="38" width="6.4" height="8" rx="1.6" fill="#6d4426"/><ellipse cx="76" cy="82" rx="11.4" ry="12.6" fill="#cd8b4a" stroke="#9a5f2c" stroke-width="1.6"/><circle cx="76" cy="82" r="3.6" fill="${v.ink}"/><path d="M74.8 46V88M77.2 46V88" stroke="#fff" stroke-width=".8" opacity=".85"/></g><g class="glow"><circle cx="89" cy="54" r="2.8" fill="${v.gl}"/><path d="M91.5 54V41q4.2 1 4.4 6" fill="none" stroke="${v.gl}" stroke-width="2" stroke-linecap="round"/></g>${hand(v, 75, 68, 4.6)}`,
|
|
64
|
+
),
|
|
65
|
+
flask: (v) =>
|
|
66
|
+
held(
|
|
67
|
+
`<circle class="glow" cx="73.5" cy="78" r="17" fill="${v.gl}"/><rect x="70" y="56" width="7" height="12" fill="#eaf7fb" stroke="#9fc9d8" stroke-width="1.4"/><rect x="69" y="51.5" width="9" height="5.6" rx="1.6" fill="#b7773f"/><circle cx="73.5" cy="78" r="12.4" fill="#eaf7fb" stroke="#9fc9d8" stroke-width="1.6"/><path d="M61.4 77Q73.5 72.6 85.6 77A12.2 12.2 0 0 1 61.4 77Z" fill="${v.gl}"/><circle cx="70" cy="80" r="1.8" fill="#fff" opacity=".8"/><circle cx="76.5" cy="83.5" r="1.2" fill="#fff" opacity=".8"/>${hand(v, 73.5, 64)}`,
|
|
68
|
+
),
|
|
69
|
+
quill: (v) =>
|
|
70
|
+
held(
|
|
71
|
+
`<circle class="glow" cx="92" cy="30" r="8" fill="${v.t}"/><path d="M70 88C70 62 80 40 92 30C95 50 86 72 70 88Z" fill="${v.s}" stroke="${v.td}" stroke-width="1.3"/><path d="M70 88Q80 58 92 30" fill="none" stroke="${v.td}" stroke-width="1.5"/><path d="M80 52Q86 54 89 48M77 62Q84 63 88 57" fill="none" stroke="${v.sd}" stroke-width="1.3"/><path d="M70 88l-3 5" stroke="${v.ink}" stroke-width="2" stroke-linecap="round"/>${hand(v, 72.4, 80)}`,
|
|
72
|
+
),
|
|
73
|
+
tome: (v) =>
|
|
74
|
+
held(
|
|
75
|
+
`<circle class="glow" cx="80" cy="60" r="9" fill="${v.t}"/><g transform="rotate(-12 78 76)"><rect x="65" y="63" width="25" height="24" rx="2.4" fill="${v.pd}" stroke="${v.ink}" stroke-width="1" stroke-opacity=".3"/><rect x="67.4" y="65.4" width="20" height="19" rx="1.4" fill="none" stroke="${v.t}" stroke-width="1.2"/><path d="M90 66V84" stroke="#f8efd6" stroke-width="3"/><rect x="72" y="71" width="9" height="7" rx="1.4" fill="${v.t}"/></g>${hand(v, 66, 80, 4.4)}`,
|
|
76
|
+
),
|
|
77
|
+
bell: (v) =>
|
|
78
|
+
held(
|
|
79
|
+
`<g class="glow"><path d="M90 66q4 6 0 12M94 62q7 10 0 20" fill="none" stroke="${v.gl}" stroke-width="2" stroke-linecap="round"/></g><path d="M66 85C70 83 69 68 76 66C83 68 82 83 86 85Z" fill="${v.t}" stroke="${v.td}" stroke-width="1.4" stroke-linejoin="round"/><circle cx="76" cy="87.6" r="2.4" fill="${v.td}"/><rect x="73.4" y="60" width="5.2" height="6.4" rx="1.6" fill="${WOOD}"/>${hand(v, 76, 60, 4.4)}`,
|
|
80
|
+
),
|
|
81
|
+
keys: (v) =>
|
|
82
|
+
held(
|
|
83
|
+
`<circle class="glow" cx="79" cy="80" r="14" fill="${v.t}"/><circle cx="78" cy="62" r="5.6" fill="none" stroke="${v.t}" stroke-width="2.6"/><path d="M82 66L90 88M90 88L93 87M87 82L90 81" fill="none" stroke="${v.t}" stroke-width="2.6" stroke-linecap="round"/><path d="M74 66L70 84M70 84L73 85M71 78L74 78" fill="none" stroke="${v.td}" stroke-width="2.6" stroke-linecap="round"/>${hand(v, 78, 62, 4.4)}`,
|
|
84
|
+
),
|
|
85
|
+
map: (v) =>
|
|
86
|
+
held(
|
|
87
|
+
`<circle class="glow" cx="81" cy="76" r="8" fill="${v.p}"/><rect x="66" y="58" width="24" height="29" rx="2" fill="#f3e2b5" stroke="#c9a867" stroke-width="1.3"/><path d="M70 66Q78 62 82 70T86 80M70 82L74 76" fill="none" stroke="#8ec7d8" stroke-width="2" stroke-linecap="round" stroke-dasharray="3 2.4"/><path d="M79 74l4 4M83 74l-4 4" stroke="${v.p}" stroke-width="2.2" stroke-linecap="round"/>${hand(v, 78, 58, 4.4)}`,
|
|
88
|
+
),
|
|
89
|
+
telescope: (v) =>
|
|
90
|
+
held(
|
|
91
|
+
`<circle class="glow" cx="94" cy="48" r="8" fill="${v.gl}"/><g transform="rotate(-38 78 70)"><rect x="55" y="64.5" width="14" height="11" rx="2" fill="${v.pd}"/><rect x="68" y="65.6" width="14" height="8.8" fill="${v.p}"/><rect x="81" y="66.6" width="12" height="6.8" rx="1.4" fill="${v.pd}"/><rect x="68" y="64.5" width="2.4" height="11" fill="${v.t}"/><rect x="81" y="65.6" width="2.4" height="8.8" fill="${v.t}"/><rect x="91" y="63.6" width="3.6" height="12.8" rx="1.4" fill="${v.t}"/></g>${hand(v, 77, 72, 4.6)}`,
|
|
92
|
+
),
|
|
93
|
+
wrench: (v) =>
|
|
94
|
+
held(
|
|
95
|
+
`<path class="glow" d="M90 52l1.6 3.6 3.8.4-2.8 2.6.8 3.8-3.4-2-3.4 2 .8-3.8-2.8-2.6 3.8-.4z" fill="${v.t}"/><path d="M76 94L83 66" stroke="#aab3c5" stroke-width="5" stroke-linecap="round"/><path d="M76 94L83 66" stroke="#fff" stroke-width="1.2" stroke-linecap="round" opacity=".5"/><path d="M77.6 58C77 52 84 50 86 54L84 57L87 59L89 57C92 62 86 68 81 66Z" fill="#aab3c5" stroke="#7d879b" stroke-width="1.2" stroke-linejoin="round"/>${hand(v, 77.4, 86)}`,
|
|
96
|
+
),
|
|
97
|
+
spade: (v) =>
|
|
98
|
+
held(
|
|
99
|
+
`<g class="glow"><ellipse cx="88" cy="62" rx="4.4" ry="2.4" transform="rotate(-30 88 62)" fill="#4fae6c"/><ellipse cx="91" cy="70" rx="4" ry="2.2" transform="rotate(20 91 70)" fill="#4fae6c"/></g><path d="M76 92V56" stroke="${WOOD}" stroke-width="4" stroke-linecap="round"/><path d="M70 56H82" stroke="${WOOD}" stroke-width="3.4" stroke-linecap="round"/><path d="M69.5 78H82.5V88Q76 97 69.5 88Z" fill="#aab3c5" stroke="#7d879b" stroke-width="1.3" stroke-linejoin="round"/>${hand(v, 76, 59)}`,
|
|
100
|
+
),
|
|
101
|
+
ladle: (v) =>
|
|
102
|
+
held(
|
|
103
|
+
`<g class="glow"><path d="M88 48q-3-4 0-8M93 50q-3-4 0-8" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/></g><path d="M74 94L82 62" stroke="${WOOD}" stroke-width="4" stroke-linecap="round"/><path d="M73 58H93A10 8 0 0 1 73 58Z" fill="#aab3c5" stroke="#7d879b" stroke-width="1.3" stroke-linejoin="round" transform="rotate(-14 83 58)"/><ellipse cx="83" cy="57" rx="8" ry="2" fill="#c97b3a" transform="rotate(-14 83 57)"/>${hand(v, 76.4, 82)}`,
|
|
104
|
+
),
|
|
105
|
+
lantern: (v) =>
|
|
106
|
+
held(
|
|
107
|
+
`<circle class="glow" cx="77" cy="78" r="20" fill="#ffd84a"/><path d="M69 66Q77 52 85 66" fill="none" stroke="${v.td}" stroke-width="2.2"/><rect x="69" y="66" width="16" height="4" rx="1.6" fill="${v.t}"/><rect x="70.4" y="70" width="13.2" height="16" fill="#fff3c2" stroke="${v.td}" stroke-width="1.4"/><path d="M77 73Q81 78 77 83Q73 78 77 73Z" fill="#ffa63a"/><rect x="69" y="86" width="16" height="4" rx="1.6" fill="${v.t}"/>${hand(v, 77, 60, 4.4)}`,
|
|
108
|
+
),
|
|
109
|
+
seal: (v) =>
|
|
110
|
+
held(
|
|
111
|
+
`<circle class="glow" cx="79" cy="78" r="9" fill="${v.p}"/><g transform="rotate(-10 78 74)"><rect x="66" y="64" width="26" height="18" rx="2" fill="#fff7e6" stroke="#d3bd8e" stroke-width="1.2"/><path d="M66 65L79 75L92 65" fill="none" stroke="#d3bd8e" stroke-width="1.2"/><circle cx="79" cy="75" r="4.6" fill="${v.p}" stroke="${v.pd}" stroke-width="1"/>${star5(79, 75, v.t).replace('d="M', 'transform="scale(.6) translate(52 50)" d="M')}</g>${hand(v, 70, 82, 4.4)}`,
|
|
112
|
+
),
|
|
113
|
+
dagger: (v) =>
|
|
114
|
+
held(
|
|
115
|
+
`<path class="glow" d="M78 50V80" stroke="#fff" stroke-width="5" stroke-linecap="round"/><path d="M78 52L82.4 78H73.6Z" fill="#dfe5f0" stroke="#9aa5ba" stroke-width="1.2" stroke-linejoin="round"/><path d="M78 56V76" stroke="#fff" stroke-width="1.2" opacity=".7"/><rect x="71" y="78" width="14" height="3.4" rx="1.6" fill="${v.t}"/><rect x="76.4" y="81" width="3.2" height="8" fill="${WOOD}"/>${hand(v, 78, 86)}`,
|
|
116
|
+
),
|
|
117
|
+
beads: (v) =>
|
|
118
|
+
held(
|
|
119
|
+
`<g class="glow"><circle cx="78" cy="78" r="12" fill="${v.t}"/></g><g fill="${v.t}" stroke="${v.td}" stroke-width="1"><circle cx="78" cy="66" r="2.4"/><circle cx="84.4" cy="68.4" r="2.4"/><circle cx="87" cy="75" r="2.4"/><circle cx="85.6" cy="82" r="2.4"/><circle cx="80.4" cy="86.6" r="2.4"/><circle cx="73.6" cy="86.6" r="2.4"/><circle cx="68.4" cy="82" r="2.4"/><circle cx="67" cy="75" r="2.4"/><circle cx="71.6" cy="68.4" r="2.4"/></g>${hand(v, 78, 62, 4.4)}`,
|
|
120
|
+
),
|
|
121
|
+
bird: (v) =>
|
|
122
|
+
held(
|
|
123
|
+
`<circle class="glow" cx="88" cy="55" r="8" fill="${v.t}"/><ellipse cx="80" cy="64" rx="9" ry="6.4" transform="rotate(-24 80 64)" fill="#8a6a4a"/><path d="M72 70L66 76L76 70Z" fill="#6f5338"/><path d="M76 66Q82 64 86 66Q82 70 78 70Z" fill="#f1e2c4"/><circle cx="87" cy="56" r="5" fill="#8a6a4a"/><path d="M91 55L96 57L91 59Z" fill="${v.t}"/><circle cx="88" cy="55" r="1.3" fill="${v.ink}"/><path d="M78 70V75" stroke="${v.t}" stroke-width="2"/><circle cx="78" cy="79" r="6.2" fill="${LEATHER}" stroke="#7a4f27" stroke-width="1.2"/>`,
|
|
124
|
+
),
|
|
125
|
+
anchor: (v) =>
|
|
126
|
+
held(
|
|
127
|
+
`<path class="glow" d="M76 54l2 4.4 4.8.5-3.6 3.2 1 4.7-4.2-2.4-4.2 2.4 1-4.7-3.6-3.2 4.8-.5z" fill="${v.t}" transform="translate(11 -4)"/><circle cx="76" cy="58" r="3.4" fill="none" stroke="#aab3c5" stroke-width="2.4"/><path d="M76 61.4V90M69.6 66H82.4M65 82Q76 98 87 82" fill="none" stroke="#aab3c5" stroke-width="3.4" stroke-linecap="round"/><path d="M65 82L63 86M87 82L89 86" stroke="#aab3c5" stroke-width="3.4" stroke-linecap="round"/>${hand(v, 76, 74)}`,
|
|
128
|
+
),
|
|
129
|
+
marotte: (v) =>
|
|
130
|
+
held(
|
|
131
|
+
`<g class="glow"><circle cx="66" cy="40" r="3" fill="${v.t}"/><circle cx="86" cy="40" r="3" fill="${v.t}"/></g><path d="M76 94V52" stroke="${WOOD}" stroke-width="4" stroke-linecap="round"/><circle cx="76" cy="46" r="7" fill="${v.k}" stroke="${v.kd}" stroke-width="1"/><path d="M70 42Q64 36 66 40Q68 42 71 44ZM82 42Q88 36 86 40Q84 42 81 44Z" fill="${v.p}"/><path d="M69 42C64 38 62 36 66 40ZM83 42C88 38 90 36 86 40Z" fill="none"/><circle cx="74" cy="46" r="1" fill="${v.ink}"/><circle cx="78" cy="46" r="1" fill="${v.ink}"/><path d="M73.4 49Q76 51.4 78.6 49" fill="none" stroke="${v.ink}" stroke-width="1" stroke-linecap="round"/>${hand(v, 76, 62, 4.4)}`,
|
|
132
|
+
),
|
|
133
|
+
sword: (v) =>
|
|
134
|
+
held(
|
|
135
|
+
`<path class="glow" d="M76 20V76" stroke="#fff" stroke-width="8" stroke-linecap="round"/><path d="M76 17L80.4 24V76H71.6V24Z" fill="#e1e7f2" stroke="#9aa5ba" stroke-width="1.2" stroke-linejoin="round"/><path d="M76 24V74" stroke="#fff" stroke-width="1.4" opacity=".8"/><rect x="67" y="76" width="18" height="4" rx="2" fill="${v.t}"/><rect x="74.4" y="80" width="3.6" height="8" fill="${WOOD}"/><circle cx="76.2" cy="90" r="2.4" fill="${v.t}"/>${hand(v, 76.2, 83)}`,
|
|
136
|
+
),
|
|
137
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCESSORY_CHOICES,
|
|
3
|
+
type AccessoryId,
|
|
4
|
+
BEARD_CHOICES,
|
|
5
|
+
type BeardId,
|
|
6
|
+
BUILDS,
|
|
7
|
+
CATALOG,
|
|
8
|
+
CLASS_IDS,
|
|
9
|
+
type ClassId,
|
|
10
|
+
HAIR_IDS,
|
|
11
|
+
HAT_IDS,
|
|
12
|
+
type HairId,
|
|
13
|
+
type HatId,
|
|
14
|
+
OUTFIT_IDS,
|
|
15
|
+
type OutfitId,
|
|
16
|
+
PROP_IDS,
|
|
17
|
+
type PropId,
|
|
18
|
+
SKIN_INDEXES,
|
|
19
|
+
} from "./catalog.ts";
|
|
20
|
+
|
|
21
|
+
/** A lowercase `#rrggbb` color. */
|
|
22
|
+
export type Hex = `#${string}`;
|
|
23
|
+
export type PaletteName =
|
|
24
|
+
| "azure"
|
|
25
|
+
| "gules"
|
|
26
|
+
| "vert"
|
|
27
|
+
| "purpure"
|
|
28
|
+
| "amber"
|
|
29
|
+
| "sable"
|
|
30
|
+
| "teal"
|
|
31
|
+
| "rose";
|
|
32
|
+
|
|
33
|
+
export interface CharacterColors {
|
|
34
|
+
/** The agent accent. */
|
|
35
|
+
primary: Hex;
|
|
36
|
+
secondary: Hex;
|
|
37
|
+
trim: Hex;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** An agent's look, fully resolved. */
|
|
41
|
+
export interface CharacterSpec {
|
|
42
|
+
v: 1;
|
|
43
|
+
class: ClassId;
|
|
44
|
+
outfit: OutfitId;
|
|
45
|
+
hat: HatId;
|
|
46
|
+
prop: PropId;
|
|
47
|
+
hair: HairId;
|
|
48
|
+
beard: BeardId | "none";
|
|
49
|
+
accessory: AccessoryId | "none";
|
|
50
|
+
build: "slim" | "regular" | "stout";
|
|
51
|
+
skin: 0 | 1 | 2 | 3 | 4 | 5;
|
|
52
|
+
hairColor: Hex;
|
|
53
|
+
colors: CharacterColors;
|
|
54
|
+
/** Set when `colors` equal a preset. */
|
|
55
|
+
palette?: PaletteName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type CharacterInput = Partial<
|
|
59
|
+
Omit<CharacterSpec, "v" | "colors" | "palette">
|
|
60
|
+
> & { palette?: PaletteName; colors?: Partial<CharacterColors> };
|
|
61
|
+
|
|
62
|
+
/** A character that cannot be made; `field` is the path of the offending field. */
|
|
63
|
+
export class CharacterError extends Error {
|
|
64
|
+
readonly field: string;
|
|
65
|
+
constructor(field: string, message: string) {
|
|
66
|
+
super(`${field}: ${message}`);
|
|
67
|
+
this.name = "CharacterError";
|
|
68
|
+
this.field = field;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const HEX = /^#[0-9a-f]{6}$/;
|
|
73
|
+
|
|
74
|
+
export function checkHex(value: unknown, field: string): Hex {
|
|
75
|
+
if (typeof value !== "string")
|
|
76
|
+
throw new CharacterError(field, "must be a #rrggbb color");
|
|
77
|
+
const lower = value.toLowerCase();
|
|
78
|
+
if (!HEX.test(lower))
|
|
79
|
+
throw new CharacterError(field, "must be a #rrggbb color");
|
|
80
|
+
return lower as Hex;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function checkOneOf<T extends string | number>(
|
|
84
|
+
list: readonly T[],
|
|
85
|
+
value: unknown,
|
|
86
|
+
field: string,
|
|
87
|
+
): T {
|
|
88
|
+
const found = list.find((item) => item === value);
|
|
89
|
+
if (found === undefined)
|
|
90
|
+
throw new CharacterError(
|
|
91
|
+
field,
|
|
92
|
+
`must be one of ${list.map(String).join(", ")}`,
|
|
93
|
+
);
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
98
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
99
|
+
return false;
|
|
100
|
+
const proto = Object.getPrototypeOf(value);
|
|
101
|
+
return proto === Object.prototype || proto === null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The palette whose three colors these are, if any. */
|
|
105
|
+
export function paletteOf(colors: CharacterColors): PaletteName | undefined {
|
|
106
|
+
for (const [name, preset] of Object.entries(CATALOG.palettes))
|
|
107
|
+
if (
|
|
108
|
+
preset.primary === colors.primary &&
|
|
109
|
+
preset.secondary === colors.secondary &&
|
|
110
|
+
preset.trim === colors.trim
|
|
111
|
+
)
|
|
112
|
+
return name as PaletteName;
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A fresh copy of a class's preset. */
|
|
117
|
+
export function presetSpec(id: ClassId): CharacterSpec {
|
|
118
|
+
const preset = CATALOG.classes[id].preset;
|
|
119
|
+
return {
|
|
120
|
+
v: 1,
|
|
121
|
+
class: id,
|
|
122
|
+
outfit: preset.outfit,
|
|
123
|
+
hat: preset.hat,
|
|
124
|
+
prop: preset.prop,
|
|
125
|
+
hair: preset.hair,
|
|
126
|
+
beard: preset.beard,
|
|
127
|
+
accessory: preset.accessory,
|
|
128
|
+
build: preset.build,
|
|
129
|
+
skin: preset.skin,
|
|
130
|
+
hairColor: preset.hairColor,
|
|
131
|
+
colors: { ...preset.colors },
|
|
132
|
+
...(preset.palette ? { palette: preset.palette } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const INPUT_KEYS = new Set([
|
|
137
|
+
"class",
|
|
138
|
+
"outfit",
|
|
139
|
+
"hat",
|
|
140
|
+
"prop",
|
|
141
|
+
"hair",
|
|
142
|
+
"beard",
|
|
143
|
+
"accessory",
|
|
144
|
+
"build",
|
|
145
|
+
"skin",
|
|
146
|
+
"hairColor",
|
|
147
|
+
"colors",
|
|
148
|
+
"palette",
|
|
149
|
+
]);
|
|
150
|
+
const COLOR_KEYS = ["primary", "secondary", "trim"] as const;
|
|
151
|
+
|
|
152
|
+
/** Set `colors` and `palette` on `spec` from a palette name and/or partial colors. */
|
|
153
|
+
function applyColors(
|
|
154
|
+
spec: CharacterSpec,
|
|
155
|
+
palette: unknown,
|
|
156
|
+
colors: unknown,
|
|
157
|
+
): void {
|
|
158
|
+
if (palette !== undefined) {
|
|
159
|
+
const name = checkOneOf(Object.keys(CATALOG.palettes), palette, "palette");
|
|
160
|
+
spec.colors = { ...CATALOG.palettes[name as PaletteName] };
|
|
161
|
+
}
|
|
162
|
+
if (colors !== undefined) {
|
|
163
|
+
if (!isRecord(colors))
|
|
164
|
+
throw new CharacterError("colors", "must be an object");
|
|
165
|
+
for (const key of Object.keys(colors))
|
|
166
|
+
if (!(COLOR_KEYS as readonly string[]).includes(key))
|
|
167
|
+
throw new CharacterError(`colors.${key}`, "is not a known color");
|
|
168
|
+
const next = { ...spec.colors };
|
|
169
|
+
for (const key of COLOR_KEYS)
|
|
170
|
+
if (colors[key] !== undefined)
|
|
171
|
+
next[key] = checkHex(colors[key], `colors.${key}`);
|
|
172
|
+
spec.colors = next;
|
|
173
|
+
}
|
|
174
|
+
const name = paletteOf(spec.colors);
|
|
175
|
+
if (name) spec.palette = name;
|
|
176
|
+
else delete spec.palette;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The look an input describes: `base` (or the input's class preset when there is no base or the
|
|
181
|
+
* input changes the class), then the input's fields over it.
|
|
182
|
+
*/
|
|
183
|
+
export function resolveCharacter(
|
|
184
|
+
input: CharacterInput,
|
|
185
|
+
base?: CharacterSpec,
|
|
186
|
+
): CharacterSpec {
|
|
187
|
+
if (!isRecord(input))
|
|
188
|
+
throw new CharacterError("character", "must be an object");
|
|
189
|
+
for (const key of Object.keys(input))
|
|
190
|
+
if (!INPUT_KEYS.has(key))
|
|
191
|
+
throw new CharacterError(key, "is not a known field");
|
|
192
|
+
const id =
|
|
193
|
+
input.class === undefined
|
|
194
|
+
? base?.class
|
|
195
|
+
: checkOneOf(CLASS_IDS, input.class, "class");
|
|
196
|
+
if (!id) throw new CharacterError("class", "is required");
|
|
197
|
+
const spec: CharacterSpec =
|
|
198
|
+
base && base.class === id
|
|
199
|
+
? { ...base, colors: { ...base.colors } }
|
|
200
|
+
: presetSpec(id);
|
|
201
|
+
if (input.outfit !== undefined)
|
|
202
|
+
spec.outfit = checkOneOf(OUTFIT_IDS, input.outfit, "outfit");
|
|
203
|
+
if (input.hat !== undefined) spec.hat = checkOneOf(HAT_IDS, input.hat, "hat");
|
|
204
|
+
if (input.prop !== undefined)
|
|
205
|
+
spec.prop = checkOneOf(PROP_IDS, input.prop, "prop");
|
|
206
|
+
if (input.hair !== undefined)
|
|
207
|
+
spec.hair = checkOneOf(HAIR_IDS, input.hair, "hair");
|
|
208
|
+
if (input.beard !== undefined)
|
|
209
|
+
spec.beard = checkOneOf(BEARD_CHOICES, input.beard, "beard");
|
|
210
|
+
if (input.accessory !== undefined)
|
|
211
|
+
spec.accessory = checkOneOf(
|
|
212
|
+
ACCESSORY_CHOICES,
|
|
213
|
+
input.accessory,
|
|
214
|
+
"accessory",
|
|
215
|
+
);
|
|
216
|
+
if (input.build !== undefined)
|
|
217
|
+
spec.build = checkOneOf(BUILDS, input.build, "build");
|
|
218
|
+
if (input.skin !== undefined)
|
|
219
|
+
spec.skin = checkOneOf(SKIN_INDEXES, input.skin, "skin");
|
|
220
|
+
if (input.hairColor !== undefined)
|
|
221
|
+
spec.hairColor = checkHex(input.hairColor, "hairColor");
|
|
222
|
+
applyColors(spec, input.palette, input.colors);
|
|
223
|
+
return spec;
|
|
224
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { BUILDS, CATALOG, CLASS_IDS, type ClassId } from "./catalog.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CharacterSpec,
|
|
4
|
+
type PaletteName,
|
|
5
|
+
paletteOf,
|
|
6
|
+
presetSpec,
|
|
7
|
+
} from "./spec.ts";
|
|
8
|
+
|
|
9
|
+
const NAME_WEIGHT = 3;
|
|
10
|
+
const PROMPT_WEIGHT = 1;
|
|
11
|
+
const PROMPT_CHARS = 600;
|
|
12
|
+
const ASCII = /^[ -~]+$/;
|
|
13
|
+
const MAX_SHORT = 3;
|
|
14
|
+
|
|
15
|
+
/** FNV-1a over the UTF-16 code units, an unsigned 32-bit integer. */
|
|
16
|
+
function fnv(text: string): number {
|
|
17
|
+
let hash = 0x811c9dc5;
|
|
18
|
+
for (let i = 0; i < text.length; i++) {
|
|
19
|
+
hash ^= text.charCodeAt(i);
|
|
20
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return hash >>> 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const escapeRegExp = (text: string): string =>
|
|
26
|
+
text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
27
|
+
|
|
28
|
+
type Matcher = (text: string) => boolean;
|
|
29
|
+
|
|
30
|
+
/** An English keyword matches from a word start (`review` finds `reviewer`); a short one is a whole word. */
|
|
31
|
+
function matcher(keyword: string): Matcher {
|
|
32
|
+
const lower = keyword.toLowerCase();
|
|
33
|
+
if (!ASCII.test(lower)) return (text) => text.includes(lower);
|
|
34
|
+
const pattern = new RegExp(
|
|
35
|
+
`(?<![a-z0-9])${escapeRegExp(lower)}${lower.length <= MAX_SHORT ? "(?![a-z0-9])" : ""}`,
|
|
36
|
+
);
|
|
37
|
+
return (text) => pattern.test(text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MATCHERS: Record<ClassId, Matcher[]> = Object.fromEntries(
|
|
41
|
+
CLASS_IDS.map((id) => [id, CATALOG.classes[id].keywords.map(matcher)]),
|
|
42
|
+
) as Record<ClassId, Matcher[]>;
|
|
43
|
+
|
|
44
|
+
function scoreOf(id: ClassId, fields: readonly [string, number][]): number {
|
|
45
|
+
let score = 0;
|
|
46
|
+
for (const check of MATCHERS[id])
|
|
47
|
+
for (const [text, weight] of fields) if (check(text)) score += weight;
|
|
48
|
+
return score;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The index into a list of `length` that a name picks, stable across runs. */
|
|
52
|
+
const pick = (hash: number, length: number): number => hash % length;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A look for an agent that has none, chosen from its role: the class whose keywords its display
|
|
56
|
+
* name, name and the start of its prompt hit most, with the name's hash settling a tie or no hit
|
|
57
|
+
* among the classes `taken` does not use, so two agents of one role still differ. Skin and build
|
|
58
|
+
* come from separate hashes of the name, and the palette is the one `taken` uses least.
|
|
59
|
+
*/
|
|
60
|
+
export function suggestCharacter(
|
|
61
|
+
agent: { name: string; displayName: string; prompt: string },
|
|
62
|
+
taken: readonly CharacterSpec[] = [],
|
|
63
|
+
): CharacterSpec {
|
|
64
|
+
const hash = fnv(agent.name);
|
|
65
|
+
const fields: [string, number][] = [
|
|
66
|
+
[agent.displayName.toLowerCase(), NAME_WEIGHT],
|
|
67
|
+
[agent.name.toLowerCase(), NAME_WEIGHT],
|
|
68
|
+
[agent.prompt.slice(0, PROMPT_CHARS).toLowerCase(), PROMPT_WEIGHT],
|
|
69
|
+
];
|
|
70
|
+
let best = 0;
|
|
71
|
+
let leaders: ClassId[] = [];
|
|
72
|
+
for (const id of CLASS_IDS) {
|
|
73
|
+
const score = scoreOf(id, fields);
|
|
74
|
+
if (score > best) {
|
|
75
|
+
best = score;
|
|
76
|
+
leaders = [id];
|
|
77
|
+
} else if (score === best && score > 0) leaders.push(id);
|
|
78
|
+
}
|
|
79
|
+
const pool = best > 0 ? leaders : [...CLASS_IDS];
|
|
80
|
+
const used = new Set(taken.map((spec) => spec.class));
|
|
81
|
+
const free = pool.filter((id) => !used.has(id));
|
|
82
|
+
const candidates = free.length > 0 ? free : pool;
|
|
83
|
+
const chosen = candidates[pick(hash, candidates.length)] as ClassId;
|
|
84
|
+
|
|
85
|
+
const spec = presetSpec(chosen);
|
|
86
|
+
spec.skin = (hash % 6) as CharacterSpec["skin"];
|
|
87
|
+
spec.build = BUILDS[fnv(`build:${agent.name}`) % 3] as CharacterSpec["build"];
|
|
88
|
+
|
|
89
|
+
const names = Object.keys(CATALOG.palettes) as PaletteName[];
|
|
90
|
+
const count = new Map<PaletteName, number>(names.map((name) => [name, 0]));
|
|
91
|
+
for (const other of taken) {
|
|
92
|
+
const name = other.palette ?? paletteOf(other.colors);
|
|
93
|
+
if (name) count.set(name, (count.get(name) ?? 0) + 1);
|
|
94
|
+
}
|
|
95
|
+
const least = Math.min(...count.values());
|
|
96
|
+
const open = names.filter((name) => count.get(name) === least);
|
|
97
|
+
const palette = open[pick(hash, open.length)] as PaletteName;
|
|
98
|
+
spec.colors = { ...CATALOG.palettes[palette] };
|
|
99
|
+
spec.palette = palette;
|
|
100
|
+
return spec;
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { resolveCharacter } from "../../characters/index.ts";
|
|
2
|
+
import { characterPng } from "../../core/agents/character-png.ts";
|
|
3
|
+
import { fail, ok, type Result } from "../report.ts";
|
|
4
|
+
|
|
5
|
+
/** Rasterizes one composition, as the agent server does for every agent's picture. */
|
|
6
|
+
const rasterize = async (): Promise<unknown> =>
|
|
7
|
+
characterPng(resolveCharacter({ class: "wizard" }), { size: 64 });
|
|
8
|
+
|
|
9
|
+
/** Whether `canvas` can rasterize the composer's SVG, which the agent server needs to draw its agents. */
|
|
10
|
+
export async function checkCharacters(
|
|
11
|
+
probe: () => Promise<unknown> = rasterize,
|
|
12
|
+
): Promise<Result> {
|
|
13
|
+
try {
|
|
14
|
+
await probe();
|
|
15
|
+
return ok("canvas rasterizes the agents' composed pictures");
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
18
|
+
return fail(
|
|
19
|
+
`canvas cannot rasterize SVG, so the agent server cannot draw the agents' pictures: ${reason}`,
|
|
20
|
+
"Install canvas with SVG support (librsvg), as the installation guide for your system describes, then run the check again.",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/cli/checks/images.ts
CHANGED
|
@@ -6,14 +6,16 @@ import { ok, type Result } from "../report.ts";
|
|
|
6
6
|
const GUIDE =
|
|
7
7
|
"node_modules/pi-roundtable/docs/plugins.md#providers-replace-a-part-the-core-runs-on";
|
|
8
8
|
|
|
9
|
-
/** Whether a plugin fills the `images` slot. Not having one is a choice, not a failure: agents
|
|
9
|
+
/** Whether a plugin fills the `images` slot. Not having one is a choice, not a failure: agents are composed characters, which the agent server no longer draws with an image provider. */
|
|
10
10
|
export async function checkImageProvider(project: Project): Promise<Result> {
|
|
11
11
|
const assembled = await project.assembled();
|
|
12
12
|
if (!assembled.ok)
|
|
13
13
|
return { status: "skipped", reason: "the configuration is not valid yet" };
|
|
14
14
|
return fillsImages(assembled.value.defined.plugins)
|
|
15
|
-
? ok(
|
|
15
|
+
? ok(
|
|
16
|
+
"a plugin fills the images slot, used for agents' generate_image; the agent server no longer draws their avatars with it",
|
|
17
|
+
)
|
|
16
18
|
: ok(
|
|
17
|
-
`none; agents
|
|
19
|
+
`none; agents are composed characters, and the agent server no longer draws with the images slot. To let agents make pictures with generate_image, fill the slot, as the plugin guide shows: ${GUIDE}`,
|
|
18
20
|
);
|
|
19
21
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BunFacts, checkBun } from "./checks/bun.ts";
|
|
2
|
+
import { checkCharacters } from "./checks/characters.ts";
|
|
2
3
|
import { checkConfiguration, checkPlugins } from "./checks/configuration.ts";
|
|
3
4
|
import { checkDatabase, type DatabasePort } from "./checks/database.ts";
|
|
4
5
|
import {
|
|
@@ -71,6 +72,7 @@ export function buildChecks(
|
|
|
71
72
|
offline: true,
|
|
72
73
|
run: () => checkImageProvider(project),
|
|
73
74
|
},
|
|
75
|
+
{ name: "characters", offline: true, run: () => checkCharacters() },
|
|
74
76
|
{
|
|
75
77
|
name: "PostgreSQL",
|
|
76
78
|
offline: false,
|