@polycode-projects/the-mechanical-code-talker 2.7.24 → 2.7.26

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.
Files changed (35) hide show
  1. package/data/sprites/adventurer-icon.toml +13 -0
  2. package/data/sprites/animal-icon.toml +15 -0
  3. package/data/sprites/butler-icon.toml +16 -0
  4. package/data/sprites/cabinet-icon.toml +13 -0
  5. package/data/sprites/container-icon.toml +12 -0
  6. package/data/sprites/cook-icon.toml +14 -0
  7. package/data/sprites/desk-icon.toml +14 -0
  8. package/data/sprites/dog-icon.toml +17 -0
  9. package/data/sprites/dog-with-colour-icon.toml +31 -0
  10. package/data/sprites/egg-icon.toml +9 -0
  11. package/data/sprites/fly-icon.toml +19 -0
  12. package/data/sprites/furniture-icon.toml +12 -0
  13. package/data/sprites/gardener-icon.toml +11 -0
  14. package/data/sprites/housekeeper-icon.toml +14 -0
  15. package/data/sprites/key-icon.toml +12 -0
  16. package/data/sprites/lamp-icon.toml +11 -0
  17. package/data/sprites/letter-icon.toml +11 -0
  18. package/data/sprites/person-icon.toml +10 -0
  19. package/data/sprites/poodle-icon.toml +22 -0
  20. package/data/sprites/portable-icon.toml +12 -0
  21. package/data/sprites/portrait-icon.toml +12 -0
  22. package/data/sprites/room-icon.toml +11 -0
  23. package/data/sprites/spider-icon.toml +21 -0
  24. package/package.json +4 -2
  25. package/src/adapters/corpus/sprite-large-template-files.mjs +62 -0
  26. package/src/adapters/corpus/sprite-template-files.mjs +52 -0
  27. package/src/domain/sprite-map.mjs +25 -10
  28. package/src/domain/sprite-materials.mjs +73 -0
  29. package/src/domain/sprite-size.mjs +34 -0
  30. package/src/domain/sprite-templates.mjs +222 -0
  31. package/src/domain/svg-instance-ids.mjs +40 -0
  32. package/src/services/adventure-viz.mjs +61 -5
  33. package/src/services/spider-fly-viz.mjs +20 -8
  34. package/src/surfaces/web/adventure-browser-entry.mjs +2 -1
  35. package/src/surfaces/web/spider-fly-browser-entry.mjs +2 -1
@@ -0,0 +1,13 @@
1
+ # adventurer — the player's own sprite: the person silhouette plus a
2
+ # strap-and-satchel, so the one individual the player controls always
3
+ # reads as visually distinct from the room's own cast of NPCs.
4
+ classes = ["adventurer"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <circle cx="12" cy="6.6" r="3.3" fill="currentColor"/>
8
+ <path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/>
9
+ <path d="M8.4 13.4 L17 17.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.85"/>
10
+ <rect x="14.6" y="15.8" width="5" height="4.6" rx="1" fill="currentColor" opacity="0.6"/>
11
+ <rect x="16.3" y="16.6" width="1.6" height="1.2" rx="0.3" fill="currentColor" opacity="0.9"/>
12
+ </svg>
13
+ """
@@ -0,0 +1,15 @@
1
+ # animal — the deliberately plainest shape in the set: an unadorned
2
+ # four-legged blob, so an unregistered species reads as "some animal"
3
+ # rather than suggesting one the taxonomy never named. The declared root
4
+ # fallback for the spider-and-fly world.
5
+ classes = ["animal"]
6
+ svg = """
7
+ <svg viewBox="0 0 24 24" aria-hidden="true">
8
+ <ellipse cx="12" cy="13" rx="7" ry="4.4" fill="currentColor"/>
9
+ <circle cx="12" cy="7" r="3.4" fill="currentColor"/>
10
+ <g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
11
+ <line x1="7" y1="17" x2="7" y2="21"/>
12
+ <line x1="17" y1="17" x2="17" y2="21"/>
13
+ </g>
14
+ </svg>
15
+ """
@@ -0,0 +1,16 @@
1
+ # butler — Ashcombe Hall's own butler: the person silhouette plus a stiff
2
+ # wing-collar flaring OUT past the shoulder line (changing the outer
3
+ # silhouette, not an internal opacity knockout — an internal light-on-dark
4
+ # accent was tried first and proved invisible once shrunk to 44px; a
5
+ # silhouette change survives shrinking the way gardener.toml's wide brim
6
+ # does) and a bow-tie.
7
+ classes = ["butler"]
8
+ svg = """
9
+ <svg viewBox="0 0 24 24" aria-hidden="true">
10
+ <circle cx="12" cy="6.6" r="3.3" fill="currentColor"/>
11
+ <path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/>
12
+ <path d="M7.6 11.6 L11.3 13.2 L8.6 15.4 Z" fill="currentColor"/>
13
+ <path d="M16.4 11.6 L12.7 13.2 L15.4 15.4 Z" fill="currentColor"/>
14
+ <path d="M10.6 14.2 L12 16.1 L13.4 14.2 L12 18.6 Z" fill="currentColor" opacity="0.95"/>
15
+ </svg>
16
+ """
@@ -0,0 +1,13 @@
1
+ # cabinet — Ashcombe Hall's own locked cabinet (a container): a two-door
2
+ # wardrobe silhouette, the gap between the doors reading as the seam, with
3
+ # two knobs and a cornice band.
4
+ classes = ["cabinet"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="4.5" y="2.5" width="7.2" height="19" rx="1" fill="currentColor"/>
8
+ <rect x="12.3" y="2.5" width="7.2" height="19" rx="1" fill="currentColor"/>
9
+ <rect x="4.5" y="2.5" width="15" height="1.4" fill="currentColor" opacity="0.4"/>
10
+ <circle cx="10.6" cy="12" r="0.9" fill="currentColor" opacity="0.35"/>
11
+ <circle cx="13.4" cy="12" r="0.9" fill="currentColor" opacity="0.35"/>
12
+ </svg>
13
+ """
@@ -0,0 +1,12 @@
1
+ # container — the generic fallback for any container with no sprite of
2
+ # its own (see cabinet/portrait for Ashcombe Hall's own two): a solid
3
+ # chest silhouette with a carved lid-seam and a latch highlight.
4
+ classes = ["container"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <path d="M3 10 L5 5 H19 L21 10 Z" fill="currentColor"/>
8
+ <rect x="3" y="10" width="18" height="10" rx="1.2" fill="currentColor"/>
9
+ <rect x="3" y="10" width="18" height="1.6" fill="currentColor" opacity="0.35"/>
10
+ <rect x="10.6" y="9.2" width="2.8" height="2.6" rx="0.5" fill="currentColor" opacity="0.85"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,14 @@
1
+ # cook — Ashcombe Hall's own cook: the person silhouette topped with a
2
+ # chef's toque puffing out WIDER than the head (the same "wider than the
3
+ # head reads as a separate garment at 44px" move gardener.toml's brim and
4
+ # housekeeper.toml's cap use — a same-width tall hat read as just an
5
+ # elongated head once shrunk, tried and rejected first).
6
+ classes = ["cook"]
7
+ svg = """
8
+ <svg viewBox="0 0 24 24" aria-hidden="true">
9
+ <ellipse cx="12" cy="3.4" rx="5.4" ry="3" fill="currentColor"/>
10
+ <rect x="8.6" y="5.6" width="6.8" height="2.2" rx="0.6" fill="currentColor"/>
11
+ <circle cx="12" cy="10" r="3.1" fill="currentColor"/>
12
+ <path d="M6 21 C6 15.6 8.7 13.2 12 13.2 C15.3 13.2 18 15.6 18 21 Z" fill="currentColor"/>
13
+ </svg>
14
+ """
@@ -0,0 +1,14 @@
1
+ # desk — Ashcombe Hall's own study desk: a tabletop bar over two drawer
2
+ # blocks with pull-knobs, on tapered legs.
3
+ classes = ["desk"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <rect x="3" y="7" width="18" height="2.6" rx="0.6" fill="currentColor"/>
7
+ <rect x="4.4" y="9.6" width="6.6" height="5.6" rx="0.6" fill="currentColor" opacity="0.85"/>
8
+ <rect x="13" y="9.6" width="6.6" height="5.6" rx="0.6" fill="currentColor" opacity="0.85"/>
9
+ <circle cx="9.6" cy="12.4" r="0.55" fill="currentColor" opacity="0.4"/>
10
+ <circle cx="16.4" cy="12.4" r="0.55" fill="currentColor" opacity="0.4"/>
11
+ <path d="M4.6 15.2 L3.8 20.5 L5.6 20.5 L6.2 15.2 Z" fill="currentColor"/>
12
+ <path d="M17.8 15.2 L17.2 20.5 L19 20.5 L18.2 15.2 Z" fill="currentColor"/>
13
+ </svg>
14
+ """
@@ -0,0 +1,17 @@
1
+ # dog — the plain-class sprite a taxonomy hop away from poodle/sheepdog.
2
+ # Same body language as poodle's, without the pompom cut, so the family
3
+ # reads as one dog rather than two unrelated animals.
4
+ classes = ["dog"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <ellipse cx="13" cy="15" rx="6.4" ry="4.3" fill="currentColor"/>
8
+ <circle cx="7" cy="9" r="3.6" fill="currentColor"/>
9
+ <ellipse cx="6.1" cy="8" rx="1" ry="1.3" fill="currentColor" opacity="0.3"/>
10
+ <path d="M4.5 6.5 L2 3 L6 5 Z" fill="currentColor"/>
11
+ <path d="M19 13 C22 12 22.5 15 20 16 Z" fill="currentColor"/>
12
+ <g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
13
+ <line x1="9" y1="19" x2="9" y2="22"/>
14
+ <line x1="17" y1="19" x2="17" y2="22"/>
15
+ </g>
16
+ </svg>
17
+ """
@@ -0,0 +1,31 @@
1
+ # dog-with-colour — the parameterized-property worked example: "a dog that
2
+ # is black". Same silhouette as dog.toml, but every filled shape takes its
3
+ # color from the instance's OWN observed
4
+ # mgx:hasProperty value rather than the page's currentColor accent, with a
5
+ # thin currentColor stroke kept on each shape so a light value (white)
6
+ # still reads against a light card background. A property value with no
7
+ # entry in [parameters.colour.values] is not a match for this template at
8
+ # all (see sprite-templates.mjs's own header for the naming/specificity
9
+ # convention) — it falls through to the plain dog.toml sprite, never a
10
+ # guessed color.
11
+ classes = ["dog"]
12
+ svg = """
13
+ <svg viewBox="0 0 24 24" aria-hidden="true">
14
+ <ellipse cx="13" cy="15" rx="6.4" ry="4.3" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/>
15
+ <circle cx="7" cy="9" r="3.6" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/>
16
+ <path d="M4.5 6.5 L2 3 L6 5 Z" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/>
17
+ <path d="M19 13 C22 12 22.5 15 20 16 Z" fill="{{FILL}}" stroke="currentColor" stroke-width="0.9"/>
18
+ <g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
19
+ <line x1="9" y1="19" x2="9" y2="22"/>
20
+ <line x1="17" y1="19" x2="17" y2="22"/>
21
+ </g>
22
+ </svg>
23
+ """
24
+
25
+ [parameters.colour]
26
+ property = "mgx:hasProperty"
27
+ placeholder = "{{FILL}}"
28
+
29
+ [parameters.colour.values]
30
+ black = "#22201d"
31
+ white = "#f5f2ea"
@@ -0,0 +1,9 @@
1
+ # egg — the spider-and-fly board's laid egg. The plain shell silhouette
2
+ # plus one small carved highlight.
3
+ classes = ["egg"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <path d="M12 2 C7 2 4 10 4 15.5 C4 20 7.6 22 12 22 C16.4 22 20 20 20 15.5 C20 10 17 2 12 2 Z" fill="currentColor"/>
7
+ <ellipse cx="9.6" cy="9.5" rx="1.7" ry="2.4" fill="currentColor" opacity="0.28"/>
8
+ </svg>
9
+ """
@@ -0,0 +1,19 @@
1
+ # fly — the spider-and-fly board's other agent. A round head, a tapered
2
+ # body with a carved highlight, translucent wings, and two thin trailing
3
+ # legs for a touch of character at rest.
4
+ classes = ["fly"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <ellipse cx="12" cy="13.6" rx="3.4" ry="5.2" fill="currentColor"/>
8
+ <circle cx="12" cy="7" r="2.7" fill="currentColor"/>
9
+ <ellipse cx="11.1" cy="12" rx="1.1" ry="1.7" fill="currentColor" opacity="0.3"/>
10
+ <g fill="currentColor" opacity="0.55">
11
+ <path d="M9 10 C3 6 2 4 3 2 C6 2 9 6 10.5 10 Z"/>
12
+ <path d="M15 10 C21 6 22 4 21 2 C18 2 15 6 13.5 10 Z"/>
13
+ </g>
14
+ <g stroke="currentColor" stroke-width="1" stroke-linecap="round">
15
+ <path d="M9.6 17 L7.4 19.4"/>
16
+ <path d="M14.4 17 L16.6 19.4"/>
17
+ </g>
18
+ </svg>
19
+ """
@@ -0,0 +1,12 @@
1
+ # furniture — the generic fallback for any furniture piece with no sprite
2
+ # of its own: a solid console-table silhouette (tapered legs, a carved
3
+ # highlight along the top edge), replacing the old hollow-shelf outline.
4
+ classes = ["furniture"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3.5" y="6.5" width="17" height="3.2" rx="0.8" fill="currentColor"/>
8
+ <rect x="3.5" y="6.5" width="17" height="1" fill="currentColor" opacity="0.35"/>
9
+ <path d="M5.6 9.7 L4.6 20 L6.7 20 L7.2 9.7 Z" fill="currentColor"/>
10
+ <path d="M18.4 9.7 L16.8 20 L18.9 20 L19.4 9.7 Z" fill="currentColor"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,11 @@
1
+ # gardener — Ashcombe Hall's own gardener: the person silhouette under a
2
+ # wide sunhat brim.
3
+ classes = ["gardener"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <ellipse cx="12" cy="5.6" rx="7.4" ry="2" fill="currentColor"/>
7
+ <path d="M8.8 5.2 C8.8 3.1 10.2 1.6 12 1.6 C13.8 1.6 15.2 3.1 15.2 5.2 C15.2 5.6 15.1 6 15 6.3 L9 6.3 C8.9 6 8.8 5.6 8.8 5.2 Z" fill="currentColor" opacity="0.85"/>
8
+ <circle cx="12" cy="8.6" r="3.1" fill="currentColor"/>
9
+ <path d="M6 21 C6 15.5 8.7 13.1 12 13.1 C15.3 13.1 18 15.5 18 21 Z" fill="currentColor"/>
10
+ </svg>
11
+ """
@@ -0,0 +1,14 @@
1
+ # housekeeper — Ashcombe Hall's own housekeeper: the person silhouette
2
+ # under a wide mob-cap brim (deliberately WIDER than the head, the same
3
+ # "wide silhouette reads at 44px" move gardener.toml's sunhat uses) and a
4
+ # tied apron line.
5
+ classes = ["housekeeper"]
6
+ svg = """
7
+ <svg viewBox="0 0 24 24" aria-hidden="true">
8
+ <ellipse cx="12" cy="4.6" rx="5.6" ry="2.1" fill="currentColor"/>
9
+ <path d="M8.6 5 C8.6 3 10 1.7 12 1.7 C14 1.7 15.4 3 15.4 5 L15.4 5.6 L8.6 5.6 Z" fill="currentColor" opacity="0.9"/>
10
+ <circle cx="12" cy="7.6" r="3.1" fill="currentColor"/>
11
+ <path d="M6 21 C6 15.5 8.7 13.1 12 13.1 C15.3 13.1 18 15.5 18 21 Z" fill="currentColor"/>
12
+ <path d="M9 16 C10 15.4 14 15.4 15 16" fill="none" stroke="currentColor" stroke-width="1" opacity="0.45" stroke-linecap="round"/>
13
+ </svg>
14
+ """
@@ -0,0 +1,12 @@
1
+ # key — Ashcombe Hall's own key (hidden in the portrait, unlocks the
2
+ # cabinet): a classic round-bow skeleton key, bold strokes so it stays
3
+ # legible at a 44px render.
4
+ classes = ["key"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <circle cx="7" cy="9" r="4.4" fill="none" stroke="currentColor" stroke-width="2.4"/>
8
+ <path d="M10.4 12 L19.5 21.1" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
9
+ <path d="M16.4 17.1 L18.6 14.9" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
10
+ <path d="M18.6 19.3 L20.6 17.3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,11 @@
1
+ # lamp — Ashcombe Hall's own study lamp: a Victorian hurricane-lamp
2
+ # silhouette — base, glass globe, a neck, and a small carved flame.
3
+ classes = ["lamp"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <path d="M9 21 H15 L14 18 H10 Z" fill="currentColor"/>
7
+ <path d="M8.4 18 C7 15.6 7 12.6 8.6 10.4 H15.4 C17 12.6 17 15.6 15.6 18 Z" fill="currentColor"/>
8
+ <rect x="10.6" y="6.4" width="2.8" height="4.4" fill="currentColor" opacity="0.85"/>
9
+ <path d="M12 3.2 C13.4 4.6 13.8 6 12 7.4 C10.2 6 10.6 4.6 12 3.2 Z" fill="currentColor" opacity="0.55"/>
10
+ </svg>
11
+ """
@@ -0,0 +1,11 @@
1
+ # letter — Ashcombe Hall's own objective (hidden in the cabinet): an
2
+ # envelope with a folded flap (a lighter overlay, the same carved-highlight
3
+ # trick as the lid on container.toml) and a small wax seal.
4
+ classes = ["letter"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="6" width="18" height="13" rx="1.2" fill="currentColor"/>
8
+ <path d="M3 6 L21 6 L21 6.8 L12 13.8 L3 6.8 Z" fill="currentColor" opacity="0.32"/>
9
+ <circle cx="12" cy="12.4" r="1.15" fill="currentColor" opacity="0.8"/>
10
+ </svg>
11
+ """
@@ -0,0 +1,10 @@
1
+ # person — the generic fallback for anyone with no NPC sprite of their own
2
+ # (see butler/housekeeper/cook/gardener for the named Ashcombe Hall staff).
3
+ classes = ["person"]
4
+ svg = """
5
+ <svg viewBox="0 0 24 24" aria-hidden="true">
6
+ <circle cx="12" cy="6.6" r="3.3" fill="currentColor"/>
7
+ <path d="M6 21 C6 15.3 8.7 12.8 12 12.8 C15.3 12.8 18 15.3 18 21 Z" fill="currentColor"/>
8
+ <path d="M12 12.8 C13.1 12.8 14.1 13 15 13.4" fill="none" stroke="currentColor" stroke-width="0.9" opacity="0.4" stroke-linecap="round"/>
9
+ </svg>
10
+ """
@@ -0,0 +1,22 @@
1
+ # poodle — the seed taxonomy's worked example (poodle IsA dog): its own
2
+ # pompom-cut head and tail, distinct from the plain dog sprite it would
3
+ # otherwise fall back to.
4
+ classes = ["poodle"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <ellipse cx="13" cy="15" rx="6.4" ry="4.6" fill="currentColor"/>
8
+ <ellipse cx="10.8" cy="13.2" rx="1.8" ry="1.3" fill="currentColor" opacity="0.28"/>
9
+ <circle cx="7" cy="8.5" r="4" fill="currentColor"/>
10
+ <g fill="currentColor">
11
+ <circle cx="4" cy="6" r="1.6"/>
12
+ <circle cx="7" cy="4.2" r="1.7"/>
13
+ <circle cx="10" cy="6" r="1.6"/>
14
+ </g>
15
+ <rect x="18.5" y="14" width="2.2" height="6" rx="1.1" fill="currentColor"/>
16
+ <circle cx="19.6" cy="13" r="1.5" fill="currentColor"/>
17
+ <g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">
18
+ <line x1="9" y1="19.5" x2="9" y2="22"/>
19
+ <line x1="17" y1="19.5" x2="17" y2="22"/>
20
+ </g>
21
+ </svg>
22
+ """
@@ -0,0 +1,12 @@
1
+ # portable — the generic fallback for any carryable object with no sprite
2
+ # of its own: a drawstring parcel silhouette, replacing the old
3
+ # generic-gift-box outline so it reads as "an unspecified small thing"
4
+ # rather than a gift.
5
+ classes = ["portable"]
6
+ svg = """
7
+ <svg viewBox="0 0 24 24" aria-hidden="true">
8
+ <path d="M12 6 C7.5 6 5 9.4 5 13.4 C5 17.7 8.1 20 12 20 C15.9 20 19 17.7 19 13.4 C19 9.4 16.5 6 12 6 Z" fill="currentColor"/>
9
+ <path d="M9.4 6.4 C9.9 4.6 10.8 3.4 12 3.4 C13.2 3.4 14.1 4.6 14.6 6.4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
10
+ <ellipse cx="9.8" cy="11.6" rx="1.6" ry="2.1" fill="currentColor" opacity="0.28"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,12 @@
1
+ # portrait — Ashcombe Hall's own drawing-room portrait (a container, hiding
2
+ # the key): a gilt arched frame around a lighter canvas, with a bust
3
+ # silhouette so it reads as a picture of someone, not an empty frame.
4
+ classes = ["portrait"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <path d="M4 21 V7 C4 4 6.8 2 12 2 C17.2 2 20 4 20 7 V21 Z" fill="currentColor"/>
8
+ <path d="M6.3 19.4 V8.3 C6.3 6.2 8.4 4.8 12 4.8 C15.6 4.8 17.7 6.2 17.7 8.3 V19.4 Z" fill="currentColor" opacity="0.28"/>
9
+ <circle cx="12" cy="10.4" r="2.3" fill="currentColor" opacity="0.7"/>
10
+ <path d="M8 18.4 C8 15.2 9.6 13.4 12 13.4 C14.4 13.4 16 15.2 16 18.4 Z" fill="currentColor" opacity="0.7"/>
11
+ </svg>
12
+ """
@@ -0,0 +1,11 @@
1
+ # room — a schematic floor plan: a washed interior (a solid tint, not a
2
+ # hollow box) plus a doorway arch, kept abstract since a room is a space
3
+ # to draw, not an object to render literally.
4
+ classes = ["room"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="currentColor" opacity="0.14"/>
8
+ <rect x="3" y="3" width="18" height="18" rx="1" fill="none" stroke="currentColor" stroke-width="1.6"/>
9
+ <path d="M9 21 L9 13 A3 3 0 0 1 15 13 L15 21" fill="none" stroke="currentColor" stroke-width="1.6"/>
10
+ </svg>
11
+ """
@@ -0,0 +1,21 @@
1
+ # spider — the spider-and-fly board's own agent. A confident two-lobe body
2
+ # (a lighter carved patch on the abdomen for shading) and eight thin
3
+ # outward legs.
4
+ classes = ["spider"]
5
+ svg = """
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <ellipse cx="12" cy="16" rx="5.2" ry="4.8" fill="currentColor"/>
8
+ <ellipse cx="10.3" cy="14.3" rx="1.8" ry="1.4" fill="currentColor" opacity="0.3"/>
9
+ <circle cx="12" cy="9.6" r="3.2" fill="currentColor"/>
10
+ <g stroke="currentColor" stroke-width="1.1" fill="none" stroke-linecap="round">
11
+ <path d="M9.4 8 L2.2 4.4"/>
12
+ <path d="M9 10.2 L1.4 9.4"/>
13
+ <path d="M9.2 12.6 L2 14.2"/>
14
+ <path d="M10 15.4 L3.4 19"/>
15
+ <path d="M14.6 8 L21.8 4.4"/>
16
+ <path d="M15 10.2 L22.6 9.4"/>
17
+ <path d="M14.8 12.6 L22 14.2"/>
18
+ <path d="M14 15.4 L20.6 19"/>
19
+ </g>
20
+ </svg>
21
+ """
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "2.7.24",
3
+ "version": "2.7.26",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; no codebase index of its own.",
@@ -67,7 +67,8 @@
67
67
  "corpus/",
68
68
  "!corpus/wordnet/generate.mjs",
69
69
  "!corpus/namenet/generate.mjs",
70
- "data/"
70
+ "data/",
71
+ "!data/sprites-large/"
71
72
  ],
72
73
  "publishConfig": {
73
74
  "access": "public"
@@ -139,6 +140,7 @@
139
140
  "build:chat-seed": "node scripts/build-chat-seed.mjs",
140
141
  "build:demo-graph": "node scripts/build-demo-graph.mjs",
141
142
  "build:demo-pack": "node scripts/build-demo-pack.mjs",
143
+ "build:demo-sprites-pack": "node scripts/build-demo-sprites-pack.mjs",
142
144
  "build:demo-memory": "node scripts/build-demo-memory.mjs",
143
145
  "gen:tool-docs": "node scripts/generate-tool-docs.mjs",
144
146
  "gen:collisions": "node scripts/generate-real-word-collisions.mjs",
@@ -0,0 +1,62 @@
1
+ // sprite-large-template-files.mjs — the filesystem side of the sprite TIER
2
+ // meant to be looked at closely (400px target, gradient/highlight shading):
3
+ // read data/sprites-large/*.toml and expand each file's by-name material
4
+ // references (sprite-materials.mjs's own `expandMaterialReferences`) before
5
+ // handing the templates to the SAME pure resolver the icon tier already
6
+ // uses (src/domain/sprite-templates.mjs). Mirrors sprite-template-files.mjs's
7
+ // own readSpriteTemplateFiles idiom exactly (same directory-of-toml-files
8
+ // shape, same per-file-defensive load) — the two loaders stay separate
9
+ // files, never merged into one, because they point at different directories
10
+ // and this one carries the extra material-expansion step the icon tier has
11
+ // no use for.
12
+ //
13
+ // Node-only (readdirSync/readFileSync), and — per package.json's own
14
+ // "!data/sprites-large/" exclusion — never reachable from an npm-installed
15
+ // consumer at all: only this git checkout (dev) or the deployed site's own
16
+ // fetched pack (scripts/build-demo-site.mjs's sprites-pack build step, which
17
+ // calls this loader once, in Node, at build time) can read this tier. The
18
+ // browser bundle never imports this module directly, the same reason
19
+ // sprite-template-files.mjs's own header gives for the icon tier.
20
+
21
+ import { readFileSync, readdirSync } from "node:fs";
22
+ import { fileURLToPath } from "node:url";
23
+ import { join, dirname } from "node:path";
24
+ import { parse as parseToml } from "smol-toml";
25
+ import { expandMaterialReferences } from "../../domain/sprite-materials.mjs";
26
+
27
+ const HERE = dirname(fileURLToPath(import.meta.url));
28
+ /** The sprite-tier template directory (data, not code) — every *.toml file
29
+ * inside is loaded, in filename order, except `_materials.toml` itself
30
+ * (that file is a shared reference table, not a template — see
31
+ * sprite-materials.mjs, its palette is a plain JS constant, not read from
32
+ * here; a stray `_materials.toml` is tolerated defensively if one is ever
33
+ * added back, simply by never matching a real `classes` list). */
34
+ export const SPRITE_LARGE_TEMPLATES_DIR = join(HERE, "..", "..", "..", "data", "sprites-large");
35
+
36
+ /** Read every *.toml file in `dir` (sorted, deterministic), expand each
37
+ * file's material references against sprite-materials.mjs's shared
38
+ * palette, and return the flat array of fully-expanded template objects. A
39
+ * missing directory or an unparseable file is DEFENSIVE (per-file: a broken
40
+ * file is skipped, not fatal to the others) — a production page load can
41
+ * never go blank because one hand-authored sprite has a typo.
42
+ * test/adapters/sprite-large-template-files.test.mjs parses the real files
43
+ * directly (not through this lenient loader) so a broken file still fails a
44
+ * test loudly. */
45
+ export function readSpriteLargeTemplateFiles(dir = SPRITE_LARGE_TEMPLATES_DIR) {
46
+ let files;
47
+ try {
48
+ files = readdirSync(dir).filter((f) => f.endsWith(".toml") && !f.startsWith("_")).sort();
49
+ } catch {
50
+ return [];
51
+ }
52
+ const templates = [];
53
+ for (const file of files) {
54
+ try {
55
+ const parsed = parseToml(readFileSync(join(dir, file), "utf8"));
56
+ templates.push(parsed);
57
+ } catch {
58
+ continue; // one malformed file never takes the others down
59
+ }
60
+ }
61
+ return expandMaterialReferences(templates);
62
+ }
@@ -0,0 +1,52 @@
1
+ // sprite-template-files.mjs — the filesystem side of the sprite template
2
+ // library: read data/sprites/*.toml and hand the raw parsed tables to
3
+ // src/domain/sprite-templates.mjs's pure resolver, mirroring
4
+ // construction-banks.mjs's own readConstructionFiles idiom exactly (same
5
+ // directory-of-toml-files shape, same per-file-defensive load).
6
+ //
7
+ // This module is Node-only (readdirSync/readFileSync) and is never imported
8
+ // by src/surfaces/web/*-browser-entry.mjs — the browser pages get their
9
+ // template set as embedded page JSON instead (adventure-viz.mjs's/
10
+ // spider-fly-viz.mjs's own renderAdventureHtml/renderSpiderFlyHtml already
11
+ // embed the world/grid payload the same way; scripts/build-demo-site.mjs
12
+ // calls this loader once, in Node, and hands the result to those render
13
+ // functions), the same reason Ashcombe Hall's own JSONL world source is read
14
+ // once at build time rather than bundled into the browser.
15
+
16
+ import { readFileSync, readdirSync } from "node:fs";
17
+ import { fileURLToPath } from "node:url";
18
+ import { join, dirname } from "node:path";
19
+ import { parse as parseToml } from "smol-toml";
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url));
22
+ /** The sprite-template directory (data, not code) — every *.toml file inside
23
+ * is loaded, in filename order, so a new sprite is a new committed file,
24
+ * never an edit to this loader. */
25
+ export const SPRITE_TEMPLATES_DIR = join(HERE, "..", "..", "..", "data", "sprites");
26
+
27
+ /** Read every *.toml file in `dir` (sorted, deterministic) and return the raw
28
+ * parsed template objects as a flat array. A missing directory or an
29
+ * unparseable file is DEFENSIVE (per-file: a broken file is skipped, not
30
+ * fatal to the others, the same posture readConstructionFiles takes for its
31
+ * own bank) — a production page load can never go blank because one hand-
32
+ * authored sprite has a typo. `test/adapters/sprite-templates.test.mjs`
33
+ * parses the real files directly (not through this lenient loader) so a
34
+ * broken file still fails a test loudly. */
35
+ export function readSpriteTemplateFiles(dir = SPRITE_TEMPLATES_DIR) {
36
+ let files;
37
+ try {
38
+ files = readdirSync(dir).filter((f) => f.endsWith(".toml")).sort();
39
+ } catch {
40
+ return [];
41
+ }
42
+ const templates = [];
43
+ for (const file of files) {
44
+ try {
45
+ const parsed = parseToml(readFileSync(join(dir, file), "utf8"));
46
+ templates.push(parsed);
47
+ } catch {
48
+ continue; // one malformed file never takes the others down
49
+ }
50
+ }
51
+ return templates;
52
+ }
@@ -154,26 +154,41 @@ function directSuperclassesOf(term, factRows) {
154
154
  return out;
155
155
  }
156
156
 
157
- /**
158
- * Resolve a class name to a sprite: `className` itself if the registry
159
- * carries it directly, otherwise the nearest rdfs:subClassOf ancestor
160
- * (breadth-first, nearest first styled after ask.mjs's own ancestorsOf)
161
- * that the registry carries, otherwise `rootFallback` ("animal" by default —
162
- * override it for a registry whose own declared root is "object" or "plant").
163
- * Pure; `factRows` is read only, never mutated.
164
- */
165
- export function resolveSpriteForClass(className, factRows, spriteRegistry, { rootFallback = "animal" } = {}) {
157
+ /** The full rdfs:subClassOf ancestor chain for `className`, nearest-first,
158
+ * INCLUDING the starting term itself as `chain[0]` the same breadth-first
159
+ * walk resolveSpriteForClass performs below, extracted so
160
+ * sprite-templates.mjs's richer, property-aware resolver can repeat the
161
+ * same specificity check at each level (fully-specific variant >
162
+ * parameterized template > plain class sprite) without re-deriving the BFS.
163
+ * Pure; `factRows` is read only, never mutated. */
164
+ export function classAncestorChain(className, factRows) {
166
165
  const start = normFactTerm(className);
167
166
  const seen = new Set();
168
167
  const queue = [start];
168
+ const chain = [];
169
169
  while (queue.length) {
170
170
  const term = queue.shift();
171
171
  if (seen.has(term)) continue;
172
172
  seen.add(term);
173
- if (Object.prototype.hasOwnProperty.call(spriteRegistry, term)) return spriteRegistry[term];
173
+ chain.push(term);
174
174
  for (const parent of directSuperclassesOf(term, factRows)) {
175
175
  if (!seen.has(parent)) queue.push(parent);
176
176
  }
177
177
  }
178
+ return chain;
179
+ }
180
+
181
+ /**
182
+ * Resolve a class name to a sprite: `className` itself if the registry
183
+ * carries it directly, otherwise the nearest rdfs:subClassOf ancestor
184
+ * (breadth-first, nearest first — styled after ask.mjs's own ancestorsOf)
185
+ * that the registry carries, otherwise `rootFallback` ("animal" by default —
186
+ * override it for a registry whose own declared root is "object" or "plant").
187
+ * Pure; `factRows` is read only, never mutated.
188
+ */
189
+ export function resolveSpriteForClass(className, factRows, spriteRegistry, { rootFallback = "animal" } = {}) {
190
+ for (const term of classAncestorChain(className, factRows)) {
191
+ if (Object.prototype.hasOwnProperty.call(spriteRegistry, term)) return spriteRegistry[term];
192
+ }
178
193
  return spriteRegistry[rootFallback];
179
194
  }
@@ -0,0 +1,73 @@
1
+ // sprite-materials.mjs — the shared {light, base, dark} colour-triple palette
2
+ // the sprite tier's gradient-shaded materials draw from, one treatment per
3
+ // key. It exists so a hex triple is written ONCE and referenced by name from
4
+ // every data/sprites-large/*.toml file that uses it, rather than the same
5
+ // three hex values being hand-copied into six or more files (the drift this
6
+ // project's own conventions already try to avoid elsewhere, e.g.
7
+ // SOURCE_PRIOR/SPRITE_REGISTRY's single-frozen-table idiom).
8
+ //
9
+ // A raw taught mgx:madeOf value maps to a TREATMENT here, not one-for-one:
10
+ // "gold" and "metal" both read as the same warm shiny-metal treatment,
11
+ // "paper" and "cardboard" both read as the same matte-paper treatment — a
12
+ // per-file [parameters.material.values] table names which of ITS OWN
13
+ // plausible raw values map to which treatment (see data/sprites-large/*.toml),
14
+ // this module only owns the treatments themselves.
15
+ //
16
+ // expandMaterialReferences is the one place that indirection gets resolved:
17
+ // it turns a short by-name reference (`gold = "metal"`, valid TOML, safe to
18
+ // repeat across files) into the full triple object sprite-templates.mjs's
19
+ // resolver actually substitutes into an svg's three gradient stops. Domain
20
+ // code downstream of this never needs to know the palette exists at all — it
21
+ // only ever sees a [parameters.*.values] entry that is already either a
22
+ // plain string (single-placeholder fill) or a complete {light,base,dark}
23
+ // object (multi-placeholder fill), the same two shapes a hand-built test
24
+ // fixture can supply directly with no palette involved.
25
+
26
+ /** The 8 rendering treatments this pass covers (the operator's own read of
27
+ * init's 17 raw mgx:madeOf strings: 8 are distinct shiny/matte/finish
28
+ * treatments, the remaining 9 food/organic materials are a different
29
+ * rendering problem and out of scope here). Every value is a real, plain
30
+ * frozen {light, base, dark} hex triple — no computed shades — so the
31
+ * gradient a sprite draws is exactly the three stops this table names. */
32
+ export const MATERIAL_PALETTE = Object.freeze({
33
+ metal: Object.freeze({ light: "#f0dfa0", base: "#c9a24b", dark: "#8a6a1e" }),
34
+ paper: Object.freeze({ light: "#fdfaf1", base: "#e8dcbf", dark: "#b7a47c" }),
35
+ glass: Object.freeze({ light: "#eaf6f7", base: "#bfe2e7", dark: "#7fb6c1" }),
36
+ ceramic: Object.freeze({ light: "#fefefe", base: "#e6e1d8", dark: "#aba69c" }),
37
+ wood: Object.freeze({ light: "#c98f56", base: "#8a5a2e", dark: "#5a3a1c" }),
38
+ plastic: Object.freeze({ light: "#f3f3f3", base: "#c8c8c8", dark: "#8a8a8a" }),
39
+ wax: Object.freeze({ light: "#fff6de", base: "#f0d98c", dark: "#c9a94a" }),
40
+ "woven material": Object.freeze({ light: "#e8d9b7", base: "#c9a869", dark: "#8a6c3d" }),
41
+ });
42
+
43
+ /**
44
+ * Every [parameters.*.values] string entry, in every template, expanded from
45
+ * a treatment KEY (e.g. "metal") to that treatment's full {light,base,dark}
46
+ * object from `palette` — but only for a parameter that declares
47
+ * `placeholders` (the multi-placeholder shape); a parameter with only the
48
+ * older singular `placeholder` is left alone, since its values are literal
49
+ * fills, never palette references. A values entry that is already an object
50
+ * (a one-off hand-authored triple, not drawn from the shared palette) is
51
+ * left alone too. A string naming no known treatment is left as the plain
52
+ * string it was — spriteTemplateProblems then flags the leftover reference
53
+ * rather than this function silently rendering the wrong colour. Pure;
54
+ * `templates` is read only.
55
+ */
56
+ export function expandMaterialReferences(templates, palette = MATERIAL_PALETTE) {
57
+ return (templates || []).map((t) => {
58
+ if (!t?.parameters) return t;
59
+ const parameters = {};
60
+ for (const [name, param] of Object.entries(t.parameters)) {
61
+ if (!param?.placeholders || !param?.values) {
62
+ parameters[name] = param;
63
+ continue;
64
+ }
65
+ const values = {};
66
+ for (const [key, value] of Object.entries(param.values)) {
67
+ values[key] = typeof value === "string" && palette[value] ? palette[value] : value;
68
+ }
69
+ parameters[name] = { ...param, values };
70
+ }
71
+ return { ...t, parameters };
72
+ });
73
+ }
@@ -0,0 +1,34 @@
1
+ // sprite-size.mjs — a taught size-descriptive property (small/large/long/
2
+ // tall — words the corpus's own WordNet-derived property vocabulary already
3
+ // carries under mgx:hasProperty) resolved to a plain numeric render scale,
4
+ // NOT a new template file: vector sprite art scales for free, so this
5
+ // dimension costs nothing extra in the pack. The same "prove the mechanism,
6
+ // no live consumer required yet" posture the icon tier's dog-with-colour
7
+ // demo already established — no shipped world currently carries a size
8
+ // property on a real object, so nothing calls this yet.
9
+
10
+ const SIZE_PROPERTY_PREDICATE = "mgx:hasProperty";
11
+
12
+ /** Closed vocabulary only: a size word absent from this table never moves
13
+ * the scale, it simply doesn't match — the same never-guess posture every
14
+ * other property lookup in this pack takes. */
15
+ const SIZE_SCALE = Object.freeze({
16
+ small: 0.8,
17
+ large: 1.3,
18
+ long: 1.2,
19
+ tall: 1.25,
20
+ });
21
+
22
+ /**
23
+ * The render scale factor for `propertyFacts` (an instance's own small
24
+ * `{predicate, object}` fact set): the first mgx:hasProperty value found in
25
+ * SIZE_SCALE, else 1 (no size fact at all => the sprite's own natural size).
26
+ * Pure; `propertyFacts` is read only, never required to be non-empty.
27
+ */
28
+ export function sizeScaleFor(propertyFacts) {
29
+ for (const f of propertyFacts || []) {
30
+ if (f?.predicate !== SIZE_PROPERTY_PREDICATE) continue;
31
+ if (Object.prototype.hasOwnProperty.call(SIZE_SCALE, f.object)) return SIZE_SCALE[f.object];
32
+ }
33
+ return 1;
34
+ }
@@ -0,0 +1,222 @@
1
+ // sprite-templates.mjs — the property-aware sprite resolver, layered ON TOP
2
+ // of sprite-map.mjs's flat class registry rather than replacing it (its own
3
+ // header explains why the ancestor walk stays exactly as-is). Where
4
+ // resolveSpriteForClass only ever asks "what class is this", this module also
5
+ // asks "what does this INSTANCE'S OWN mgx:hasProperty-shaped fact say about
6
+ // it" — the operator's own worked example: a dog typed "dog" with a taught
7
+ // "black" property resolves to a filled-in coloured template, one typed
8
+ // "sheepdog" with the same property resolves through the ordinary ancestor
9
+ // walk to the SAME template, and a dog with no colour property at all falls
10
+ // through to the plain dog sprite.
11
+ //
12
+ // Naming convention for the two template kinds a specific class can carry
13
+ // beyond its own plain `{class}.toml` (data/sprites/*.toml, one file per
14
+ // sprite):
15
+ // - a PARAMETERIZED template — `{class}-with-{property}.toml` (this pass
16
+ // ships `dog-with-colour.toml`) — declares a `[parameters.<name>]` table
17
+ // naming the fact predicate that carries the value (`property`, e.g.
18
+ // "mgx:hasProperty"), a placeholder token the `svg` string contains
19
+ // (`placeholder`, e.g. "{{FILL}}"), and a `[parameters.<name>.values]`
20
+ // table translating an observed property VALUE into the literal
21
+ // substitution (e.g. `black = "#22201d"`). A value with no entry in that
22
+ // map is not a match for this template at all — it falls through to a
23
+ // less specific one, never a guessed/invented substitution.
24
+ // - a fully-specific hand-authored VARIANT — `{class}-with-{property}-
25
+ // {value}.toml` (e.g. a hypothetical `dog-with-colour-black.toml`, not
26
+ // authored this pass) — carries the same `classes` as the class it
27
+ // specializes, plus a `[match]` table (`property`, `value`) naming the
28
+ // exact fact it requires, so it outranks the parameterized template
29
+ // when both would otherwise apply.
30
+ //
31
+ // A parameterized template's `[parameters.<name>]` table comes in two
32
+ // shapes, picked by which of `placeholder`/`placeholders` it declares:
33
+ // - single-placeholder (the shape above): one `placeholder` token, and
34
+ // every `[parameters.<name>.values]` entry is a plain string substituted
35
+ // for it directly.
36
+ // - multi-placeholder (data/sprites-large/*.toml's gradient-shaded
37
+ // materials): a `placeholders` table instead, naming several tokens at
38
+ // once (e.g. `{ light = "{{FILL_LIGHT}}", base = "{{FILL}}", dark =
39
+ // "{{FILL_DARK}}" }`), and every `[parameters.<name>.values]` entry is a
40
+ // table with the SAME sub-keys (e.g. `{ light = "#f0dfa0", base =
41
+ // "#c9a24b", dark = "#8a6a1e" }`), one substituted per token. A value
42
+ // missing even one of the declared sub-keys is never a partial match —
43
+ // the same never-guess posture as an unmapped value in the single shape.
44
+ // sprite-materials.mjs's `expandMaterialReferences` is what lets a
45
+ // sprite-large file write a short by-name reference (`gold = "metal"`)
46
+ // instead of hand-copying the triple — this module never has to know
47
+ // that indirection exists, it only ever sees the expanded table shape.
48
+ //
49
+ // An object with no taught material still gets a real gradient at the
50
+ // sprite tier (data/sprites-large/*.toml's own non-material files), built
51
+ // from currentColor via `color-mix(in srgb, currentColor N%, white/black)`
52
+ // rather than `stop-opacity` — opacity blends toward whatever sits BEHIND
53
+ // the shape, so its light/dark direction silently flips between a light
54
+ // theme (currentColor dark-on-light) and a dark one (currentColor
55
+ // light-on-dark); color-mix lightens/darkens currentColor itself, so the
56
+ // same corner of the shape reads as the lit one on either theme.
57
+ //
58
+ // Specificity order, checked at EACH term of the class's ancestor chain
59
+ // (nearest first, sprite-map.mjs's own classAncestorChain) before moving to
60
+ // the next ancestor: an exact fully-specific variant whose [match] is
61
+ // satisfied > a parameterized template filled with an observed matching
62
+ // value > a plain class template > (repeat at the next ancestor) > the
63
+ // existing flat spriteRegistry entry for that same term (so a class not yet
64
+ // migrated to its own template keeps resolving exactly as it did before this
65
+ // module existed) > once the chain is exhausted, the same three-step check
66
+ // against `rootFallback`, falling back to spriteRegistry's own root entry
67
+ // only if nothing there matches either.
68
+ import { classAncestorChain } from "./sprite-map.mjs";
69
+ import { namespaceSvgIds } from "./svg-instance-ids.mjs";
70
+
71
+ /** Every template in `templates` whose `classes` list names `term`. */
72
+ function templatesForClass(term, templates) {
73
+ return (templates || []).filter((t) => Array.isArray(t?.classes) && t.classes.includes(term));
74
+ }
75
+
76
+ function matchSatisfied(match, propertyFacts) {
77
+ if (!match || !match.property || match.value === undefined) return false;
78
+ return (propertyFacts || []).some((f) => f.predicate === match.property && f.object === match.value);
79
+ }
80
+
81
+ /** Substitute one matched `[parameters.*.values]` entry into `svg`: a plain
82
+ * string fills the parameter's single `placeholder` token; an object fills
83
+ * every token `param.placeholders` names from the SAME sub-key — returning
84
+ * null (never a partial gradient) if the object is missing even one of the
85
+ * sub-keys the parameter declares. */
86
+ function fillFromValue(svg, param, value) {
87
+ if (typeof value === "string" && param.placeholder) return svg.split(param.placeholder).join(value);
88
+ if (value && typeof value === "object" && param.placeholders) {
89
+ let out = svg;
90
+ for (const [sub, token] of Object.entries(param.placeholders)) {
91
+ if (!Object.prototype.hasOwnProperty.call(value, sub)) return null;
92
+ out = out.split(token).join(value[sub]);
93
+ }
94
+ return out;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /** Fill a parameterized template's `svg` from the first of its own
100
+ * `[parameters.*]` whose observed property value maps to a substitution —
101
+ * or null when no property fact names a mapped value (never a guess). */
102
+ function parameterizedFill(template, propertyFacts) {
103
+ for (const param of Object.values(template.parameters || {})) {
104
+ const values = param?.values || {};
105
+ const hit = (propertyFacts || []).find(
106
+ (f) => f.predicate === param.property && Object.prototype.hasOwnProperty.call(values, f.object),
107
+ );
108
+ if (!hit) continue;
109
+ const filled = fillFromValue(template.svg, param, values[hit.object]);
110
+ if (filled) return filled;
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /** Resolve ONE class term (no ancestor walk here — the caller repeats this
116
+ * at every level of the chain) against the template set, in specificity
117
+ * order: fully-specific match variant > parameterized template filled with
118
+ * an observed value > plain class template. Returns the SVG string, or null
119
+ * when nothing at this level matches. */
120
+ function resolveAtTerm(term, propertyFacts, templates) {
121
+ const candidates = templatesForClass(term, templates);
122
+ const matched = candidates.find((t) => t.match && matchSatisfied(t.match, propertyFacts));
123
+ if (matched) return matched.svg;
124
+ for (const t of candidates) {
125
+ if (t.match || !t.parameters) continue;
126
+ const filled = parameterizedFill(t, propertyFacts);
127
+ if (filled) return filled;
128
+ }
129
+ const plain = candidates.find((t) => !t.match && !t.parameters);
130
+ return plain ? plain.svg : null;
131
+ }
132
+
133
+ function resolveSpriteAssetRaw(className, factRows, propertyFacts, templates, spriteRegistry, rootFallback) {
134
+ for (const term of classAncestorChain(className, factRows)) {
135
+ const hit = resolveAtTerm(term, propertyFacts, templates);
136
+ if (hit) return hit;
137
+ if (Object.prototype.hasOwnProperty.call(spriteRegistry, term)) return spriteRegistry[term];
138
+ }
139
+ const rootHit = resolveAtTerm(rootFallback, propertyFacts, templates);
140
+ if (rootHit) return rootHit;
141
+ return spriteRegistry[rootFallback];
142
+ }
143
+
144
+ /**
145
+ * Resolve `className` to sprite SVG markup, property-aware: the ancestor
146
+ * chain (from `factRows`, sprite-map.mjs's own walk) is checked nearest-
147
+ * first, and at each term the template set (`templates`, the parsed
148
+ * data/sprites/*.toml set) is tried before falling back to `spriteRegistry`
149
+ * for that same term — so a class with no template at all resolves exactly
150
+ * as resolveSpriteForClass already does. `propertyFacts` is the instance's
151
+ * own small `{predicate, object}` fact set (e.g. its mgx:hasProperty rows) —
152
+ * read only, never required to be non-empty. Pure.
153
+ *
154
+ * `instanceKey`, when given, namespaces every gradient id the resolved svg
155
+ * declares (svg-instance-ids.mjs's own `namespaceSvgIds`) — pass the
156
+ * instance's own identity (e.g. its subject name) whenever more than one
157
+ * resolved sprite can appear in the same document at once, so two
158
+ * differently-valued instances of the SAME template (a gold lamp and a
159
+ * ceramic lamp both on screen) never share one `<linearGradient id>` and
160
+ * silently render each other's colours. Omit it for a single-instance
161
+ * caller (a template with no ids at all is untouched either way).
162
+ */
163
+ export function resolveSpriteAsset(className, factRows, propertyFacts, templates, spriteRegistry, { rootFallback = "animal", instanceKey } = {}) {
164
+ const svg = resolveSpriteAssetRaw(className, factRows, propertyFacts, templates, spriteRegistry, rootFallback);
165
+ return instanceKey ? namespaceSvgIds(svg, instanceKey) : svg;
166
+ }
167
+
168
+ /** Every internal-consistency problem with one parsed template, as plain
169
+ * strings — empty when the template is well-formed. Used both by
170
+ * test/adapters/sprite-templates.test.mjs (against the real loaded
171
+ * data/sprites/ directory) and available to any future loader that wants to
172
+ * warn rather than silently drop a broken file. Checks: `classes` is a
173
+ * non-empty array, `svg` is a real `<svg` string, a `[parameters.*]` table
174
+ * names a `property` and exactly one of `placeholder`/`placeholders` (every
175
+ * token named appears in `svg`), its `values` map is non-empty and every
176
+ * entry matches the shape its own `placeholder`/`placeholders` choice
177
+ * expects, and a `[match]` table names both `property` and `value`. */
178
+ export function spriteTemplateProblems(template) {
179
+ const problems = [];
180
+ const t = template || {};
181
+ if (!Array.isArray(t.classes) || t.classes.length === 0) problems.push("classes is missing or empty");
182
+ if (typeof t.svg !== "string" || !t.svg.trim().startsWith("<svg")) problems.push("svg is missing or not an <svg> string");
183
+ for (const [name, param] of Object.entries(t.parameters || {})) {
184
+ if (!param?.property) problems.push(`parameters.${name}.property is missing`);
185
+ if (param?.placeholder && param?.placeholders) {
186
+ problems.push(`parameters.${name} sets both placeholder and placeholders — pick one`);
187
+ } else if (param?.placeholders) {
188
+ const tokens = Object.entries(param.placeholders);
189
+ if (tokens.length === 0) problems.push(`parameters.${name}.placeholders is empty`);
190
+ for (const [sub, token] of tokens) {
191
+ if (typeof t.svg === "string" && !t.svg.includes(token)) {
192
+ problems.push(`parameters.${name}.placeholders.${sub} ${JSON.stringify(token)} does not appear in svg`);
193
+ }
194
+ }
195
+ } else if (!param?.placeholder) {
196
+ problems.push(`parameters.${name}.placeholder is missing`);
197
+ } else if (typeof t.svg === "string" && !t.svg.includes(param.placeholder)) {
198
+ problems.push(`parameters.${name}.placeholder ${JSON.stringify(param.placeholder)} does not appear in svg`);
199
+ }
200
+ if (!param?.values || Object.keys(param.values).length === 0) {
201
+ problems.push(`parameters.${name}.values is empty`);
202
+ } else {
203
+ for (const [key, value] of Object.entries(param.values)) {
204
+ if (param.placeholders) {
205
+ if (!value || typeof value !== "object") {
206
+ problems.push(`parameters.${name}.values.${key} is not an expanded {${Object.keys(param.placeholders).join("/")}} object — an unresolved material reference?`);
207
+ } else {
208
+ for (const sub of Object.keys(param.placeholders)) {
209
+ if (!Object.prototype.hasOwnProperty.call(value, sub)) problems.push(`parameters.${name}.values.${key} is missing "${sub}"`);
210
+ }
211
+ }
212
+ } else if (typeof value !== "string") {
213
+ problems.push(`parameters.${name}.values.${key} must be a plain string for a single-placeholder parameter`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ if (t.match && (!t.match.property || t.match.value === undefined)) {
219
+ problems.push("match is missing property or value");
220
+ }
221
+ return problems;
222
+ }
@@ -0,0 +1,40 @@
1
+ // svg-instance-ids.mjs — a resolved sprite's own `id="…"`/`url(#…)` pairs
2
+ // (a gradient def and its fill/stroke reference) made unique per rendered
3
+ // instance. It exists because two DIFFERENT resolved values of the SAME
4
+ // class's SAME template — e.g. a gold lamp and a ceramic lamp, both on
5
+ // screen at once — share the identical `id="lamp-fill"` string (the id lives
6
+ // in the TEMPLATE, not the substituted colour), and duplicate ids are a real
7
+ // bug, not a cosmetic one: a browser's `url(#id)` reference resolves to
8
+ // whichever element with that id it saw FIRST in the document, so every
9
+ // later instance silently renders the FIRST instance's colours. Namespacing
10
+ // each id (and every reference to it) by the caller's own instanceKey before
11
+ // two instances share a DOM keeps each gradient its own.
12
+ //
13
+ // A resolved sprite with no ids at all (most non-material templates carry
14
+ // none) is untouched — this is a no-op unless there's actually a collision
15
+ // to prevent.
16
+
17
+ /** Every id `svg` declares, in first-appearance order. */
18
+ function idsIn(svg) {
19
+ return [...new Set([...svg.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1]))];
20
+ }
21
+
22
+ /**
23
+ * Suffix every `id="X"` and every `url(#X)` reference to it in `svg` with
24
+ * `-{instanceKey}` (sanitized to id-safe characters) — so the SAME template
25
+ * resolved for two different instances never collides once both are in the
26
+ * same document. `instanceKey` falsy (undefined/""/0) returns `svg`
27
+ * unchanged, the same never-touch-it-unless-asked posture as any other
28
+ * optional pass-through in this pack. Pure.
29
+ */
30
+ export function namespaceSvgIds(svg, instanceKey) {
31
+ if (!instanceKey || typeof svg !== "string") return svg;
32
+ const safe = String(instanceKey).replace(/[^a-zA-Z0-9_-]/g, "-");
33
+ let out = svg;
34
+ for (const id of idsIn(svg)) {
35
+ const namespaced = `${id}-${safe}`;
36
+ out = out.split(`id="${id}"`).join(`id="${namespaced}"`);
37
+ out = out.split(`url(#${id})`).join(`url(#${namespaced})`);
38
+ }
39
+ return out;
40
+ }
@@ -72,6 +72,31 @@ export function spriteClassForObject(rows, subject) {
72
72
  return typeRow ? typeRow.object : "portable";
73
73
  }
74
74
 
75
+ /** `rows` plus one synthetic rdfs:subClassOf edge from `subject`'s own name
76
+ * to its declared sprite class (spriteClassForObject's own result) — never
77
+ * written back to the corpus, just handed to sprite-templates.mjs's
78
+ * resolver so a NAMED object (`cabinet`, `butler`) can carry its own sprite
79
+ * template while an unauthored object of the same declared class still
80
+ * falls back through it cleanly. Ashcombe Hall's own objects have no
81
+ * rdfs:subClassOf chain of their own (each is directly typed, e.g. `cabinet
82
+ * rdf:type furniture`); this is the exact same ancestor-walk mechanism
83
+ * spider-fly's real poodle-IsA-dog taxonomy already exercises, just
84
+ * synthesized at render time for a world whose taxonomy doesn't reach this
85
+ * deep. A no-op when the subject's own name already IS its declared class
86
+ * (nothing to synthesize). Pure. */
87
+ export function spriteAncestryRows(rows, subject) {
88
+ const declaredClass = spriteClassForObject(rows, subject);
89
+ if (subject === declaredClass) return rows;
90
+ return [...(rows || []), { subject, predicate: "rdfs:subClassOf", object: declaredClass }];
91
+ }
92
+
93
+ /** Every fact row belonging to `subject` — the small `{predicate, object}`
94
+ * set sprite-templates.mjs's resolver checks a parameterized/match template
95
+ * against (e.g. an mgx:hasProperty row). Pure. */
96
+ export function factsForSubject(rows, subject) {
97
+ return (rows || []).filter((r) => r.subject === subject);
98
+ }
99
+
75
100
  /** Every subject actually visible in `here`, sorted, each with its sprite
76
101
  * class — mirroring adventure.mjs's own private `visibleRoomOf` walk (one
77
102
  * containment hop through an OPEN container) so this can never draw a
@@ -133,14 +158,26 @@ export function roomCaptionText(rows, state, here) {
133
158
  /** The self-contained adventure page. Pure given `worldPayload` (the build
134
159
  * step's own read of the real Ashcombe Hall world — `{ facts, rules,
135
160
  * opening }`), the same "byte-identical for identical input" invariant
136
- * every other viz page in this project holds. `?preview=1` switches into
137
- * the small, auto-playing, non-interactive mode the home page's hero iframe
138
- * embeds, matching spider-fly.html's own dual-purpose file. */
139
- export function renderAdventureHtml({ title = DEFAULT_TITLE, worldPayload = { facts: [], rules: [], opening: "" } } = {}) {
161
+ * every other viz page in this project holds. `spriteTemplates` is the
162
+ * build step's own read of data/sprites/*.toml (sprite-template-files.mjs's
163
+ * readSpriteTemplateFiles), embedded as page data the same way the world
164
+ * payload is the browser cannot read the filesystem, so the parsed
165
+ * templates travel as JSON rather than as a bundled fs read. Defaults to
166
+ * `[]` (every sprite falls back to the flat SPRITE_REGISTRY, unchanged from
167
+ * before this module existed) so existing callers that don't pass one keep
168
+ * working. `?preview=1` switches into the small, auto-playing, non-
169
+ * interactive mode the home page's hero iframe embeds, matching
170
+ * spider-fly.html's own dual-purpose file. */
171
+ export function renderAdventureHtml({
172
+ title = DEFAULT_TITLE,
173
+ worldPayload = { facts: [], rules: [], opening: "" },
174
+ spriteTemplates = [],
175
+ } = {}) {
140
176
  const pageData = embedJson({
141
177
  world: worldPayload,
142
178
  previewMaxTicks: PREVIEW_MAX_TICKS,
143
179
  tickWaitMs: TICK_WAIT_MS,
180
+ spriteTemplates,
144
181
  });
145
182
 
146
183
  return `<!doctype html>
@@ -242,6 +279,8 @@ const ADVENTURE = ${pageData};
242
279
  const createTicker = ${createTicker.toString()};
243
280
  const spriteClassForObject = ${spriteClassForObject.toString()};
244
281
  const roomSceneObjects = ${roomSceneObjects.toString()};
282
+ const spriteAncestryRows = ${spriteAncestryRows.toString()};
283
+ const factsForSubject = ${factsForSubject.toString()};
245
284
  const esc = ${escapeHtml.toString()};
246
285
  const el = (id) => document.getElementById(id);
247
286
  const spriteRow = el("spriteRow");
@@ -303,11 +342,28 @@ const ADVENTURE = ${pageData};
303
342
  chatqEl.focus();
304
343
  });
305
344
 
345
+ // ---- sprite resolution — property/instance-aware (sprite-templates.mjs's
346
+ // resolveSpriteAsset), keyed on the OBJECT'S OWN NAME (via
347
+ // spriteAncestryRows' synthetic subClassOf edge to its declared class) so
348
+ // a named object (cabinet, butler) can carry its own data/sprites/*.toml
349
+ // template while the data-cls attribute still reflects the DECLARED class
350
+ // (container/furniture/portable/person/room) — the CSS accent-color rules
351
+ // stay keyed on that declared class unchanged. The player's own "you" row
352
+ // has no backing fact row, so it resolves directly by its fixed
353
+ // "adventurer" class with no ancestry/property rows to check.
354
+ function resolveObjectSprite(rows, s) {
355
+ if (s.subject === "you") return tmctAdventure.resolveSpriteAsset("adventurer", [], [], ADVENTURE.spriteTemplates, tmctAdventure.SPRITE_REGISTRY);
356
+ return tmctAdventure.resolveSpriteAsset(
357
+ s.subject, spriteAncestryRows(rows, s.subject), factsForSubject(rows, s.subject),
358
+ ADVENTURE.spriteTemplates, tmctAdventure.SPRITE_REGISTRY,
359
+ );
360
+ }
361
+
306
362
  function redraw(snap) {
307
363
  const objects = roomSceneObjects(snap.rows, snap.state, snap.here);
308
364
  const sprites = [{ subject: "you", spriteClass: "adventurer" }, ...objects];
309
365
  spriteRow.innerHTML = sprites.map((s) => {
310
- const svg = tmctAdventure.resolveSpriteForClass(s.spriteClass, [], tmctAdventure.SPRITE_REGISTRY);
366
+ const svg = resolveObjectSprite(snap.rows, s);
311
367
  return '<div><div class="sprite" data-cls="' + esc(s.spriteClass) + '">' + svg + '</div>'
312
368
  + '<div class="sprite-label">' + esc(s.subject) + "</div></div>";
313
369
  }).join("");
@@ -96,13 +96,18 @@ export function threadCellsForSpiderPlan(agents, geometry) {
96
96
  }
97
97
 
98
98
  /** The self-contained spider-and-fly page. Pure — the same output for the
99
- * same `title` every time; every other piece of state this page shows is
100
- * computed live in the browser once the sibling bundle loads. `?preview=1`
101
- * on the page's own URL switches it into the small, auto-playing,
102
- * non-interactive mode the home page's hero iframe embeds (§11) — one file
103
- * serves both the hero and the "open full-screen" link, matching how
104
- * ledger.html/plan.html are each one file embedded two ways. */
105
- export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
99
+ * same `title`/`spriteTemplates` every time; every other piece of state
100
+ * this page shows is computed live in the browser once the sibling bundle
101
+ * loads. `spriteTemplates` is the build step's own read of
102
+ * data/sprites/*.toml (sprite-template-files.mjs), embedded as page data
103
+ * the same reason adventure-viz.mjs's own worldPayload is the browser
104
+ * bundle stays fs-free. Defaults to `[]` (every agent falls back to the
105
+ * flat SPRITE_REGISTRY, unchanged from before this module existed).
106
+ * `?preview=1` on the page's own URL switches it into the small, auto-
107
+ * playing, non-interactive mode the home page's hero iframe embeds (§11) —
108
+ * one file serves both the hero and the "open full-screen" link, matching
109
+ * how ledger.html/plan.html are each one file embedded two ways. */
110
+ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [] } = {}) {
106
111
  const gridData = embedJson({
107
112
  gridSize: GRID_SIZE,
108
113
  webCells: webCellIds(),
@@ -114,6 +119,7 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE } = {}) {
114
119
  tickWaitMs: TICK_WAIT_MS,
115
120
  maxFlyMass: FLY_INITIAL_MASS,
116
121
  maxSpiderMass: SPIDER_INITIAL_MASS,
122
+ spriteTemplates,
117
123
  });
118
124
 
119
125
  return `<!doctype html>
@@ -314,8 +320,14 @@ const SPIDERFLY = ${gridData};
314
320
  node = document.createElement("div");
315
321
  node.className = "sprite";
316
322
  node.dataset.cls = cls;
323
+ // Property-aware resolution (sprite-templates.mjs's resolveSpriteAsset):
324
+ // no agent here carries an mgx:hasProperty fact today, so propertyFacts
325
+ // stays empty and every agent resolves through its plain class template
326
+ // (or the flat SPRITE_REGISTRY, for a class with none) — the same output
327
+ // as before this module existed, just wired for the day an agent does
328
+ // carry one.
317
329
  const sprite = window.tmctSpiderFly
318
- ? tmctSpiderFly.resolveSpriteForClass(cls, (session && session.taxonomyRows) || [], tmctSpiderFly.SPRITE_REGISTRY)
330
+ ? tmctSpiderFly.resolveSpriteAsset(cls, (session && session.taxonomyRows) || [], [], SPIDERFLY.spriteTemplates, tmctSpiderFly.SPRITE_REGISTRY)
319
331
  : "";
320
332
  node.innerHTML = sprite;
321
333
  if (!preview) {
@@ -38,6 +38,7 @@ import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
38
38
  import { foldWorldState, worldDigestRows, roomAffordances } from "../../services/adventure.mjs";
39
39
  import { runAdventureAutoplayTick } from "../../services/adventure-autoplay.mjs";
40
40
  import { resolveSpriteForClass, SPRITE_REGISTRY } from "../../domain/sprite-map.mjs";
41
+ import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
41
42
 
42
43
  /** A live in-memory adventure this page's ticker AND chat dock can both
43
44
  * drive. Returns `{ memoryDir, autoplayTick, turn, snapshot }`.
@@ -121,5 +122,5 @@ export async function createAdventureSession(worldPayload) {
121
122
  // affordances the chat dock's own pills read from — the same posture
122
123
  // spider-fly-browser-entry.mjs's own globalThis.tmctSpiderFly re-export takes.
123
124
  globalThis.tmctAdventure = {
124
- createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, worldDigestRows, roomAffordances,
125
+ createAdventureSession, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset, worldDigestRows, roomAffordances,
125
126
  };
@@ -54,6 +54,7 @@ import {
54
54
  } from "../../domain/spider-fly-world.mjs";
55
55
  import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, liveWebs, DEFAULT_VISION_RADIUS } from "../../services/spider-fly.mjs";
56
56
  import { resolveSpriteForClass, SPRITE_REGISTRY } from "../../domain/sprite-map.mjs";
57
+ import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
57
58
 
58
59
  /** A live in-memory game the page's ticker and chat dock can both drive.
59
60
  * Returns { memoryDir, sessionId, opening, initial, taxonomyRows, tick,
@@ -149,6 +150,6 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
149
150
  // a spider's remaining silk-thread path from its returned direction list, and
150
151
  // computing the POV overlay's visible-cell mask, both need them.
151
152
  globalThis.tmctSpiderFly = {
152
- createSpiderFlySession, normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY,
153
+ createSpiderFlySession, normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
153
154
  cellId, parseCellId, DIRECTION_DELTA, visibleCells, DEFAULT_VISION_RADIUS,
154
155
  };