@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
// why the mechanism exists, just not exercised by any class shown here).
|
|
46
46
|
|
|
47
47
|
import { classAncestorChain, SPRITE_REGISTRY } from "../domain/sprite-map.mjs";
|
|
48
|
-
import { resolveSpriteAsset } from "../domain/sprite-templates.mjs";
|
|
48
|
+
import { resolveSpriteAsset, matchConstraints } from "../domain/sprite-templates.mjs";
|
|
49
49
|
import { MATERIAL_PALETTE } from "../domain/sprite-materials.mjs";
|
|
50
50
|
import { spriteFactRows } from "../domain/sprite-facts.mjs";
|
|
51
51
|
import { SEED_TAXONOMY } from "../domain/spider-fly-world.mjs";
|
|
@@ -247,7 +247,16 @@ export function parameterVariantsFor(template) {
|
|
|
247
247
|
* generic root-fallback shape, not its own silhouette (this module's own
|
|
248
248
|
* header names the 19 large-tier classes this applies to). Every
|
|
249
249
|
* [parameters.*] value and every [match] variant renders through the real
|
|
250
|
-
* resolveSpriteAsset with the exact property
|
|
250
|
+
* resolveSpriteAsset with the exact property facts it declares — and a
|
|
251
|
+
* parameter sitting ON a [match] variant is rendered with BOTH facts, so a
|
|
252
|
+
* facing profile's mood swatch shows the profile wearing that mood and is
|
|
253
|
+
* labeled for the pair ("left + happy"). Rendering it on the parameter fact
|
|
254
|
+
* alone would resolve a different template altogether (the front-facing
|
|
255
|
+
* one) and label it as if it belonged to the profile. A value that a
|
|
256
|
+
* match-free template of the same class already offers is listed once, from
|
|
257
|
+
* that template: the facing pair and the `*-with-emotion.toml` file both
|
|
258
|
+
* carry all six moods, and one swatch per mood is the catalog's unit. A
|
|
259
|
+
* class
|
|
251
260
|
* with templates but no plain among them also gets one extra swatch,
|
|
252
261
|
* labeled `fallback: true`, showing exactly what the real resolver returns
|
|
253
262
|
* for that class with NO taught fact — honest engine behaviour, never
|
|
@@ -264,19 +273,33 @@ export function tierSwatchesFor(cls, templates, registry, tier) {
|
|
|
264
273
|
svg: resolveSpriteAsset(cls, [], [], templates, registry, { instanceKey: `${cls}-${tier}-plain` }),
|
|
265
274
|
});
|
|
266
275
|
}
|
|
276
|
+
const valueKey = (v) => `${v.paramName}:${v.rawValue}`;
|
|
277
|
+
const shownByUnmatchedTemplate = new Set(
|
|
278
|
+
forClass.filter((t) => !t.match).flatMap((t) => parameterVariantsFor(t).map(valueKey)),
|
|
279
|
+
);
|
|
267
280
|
for (const t of forClass) {
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
// A variant may require several facts at once (sprite-templates.mjs's own
|
|
282
|
+
// matchConstraints), so the swatch is rendered with EVERY fact its
|
|
283
|
+
// [match] asks for and labeled for the whole set — "left + moving" for a
|
|
284
|
+
// combined facing-and-pose file. A one-constraint variant reads exactly
|
|
285
|
+
// as it did before the plural spelling existed.
|
|
286
|
+
const constraints = matchConstraints(t);
|
|
287
|
+
const matchFact = constraints.map((c) => ({ predicate: c.property, object: c.value }));
|
|
288
|
+
const variantLabel = constraints.map((c) => c.value).join(" + ");
|
|
289
|
+
const variantKey = constraints.map((c) => c.value).join("-");
|
|
290
|
+
const matchPrefix = constraints.length ? `${variantLabel} + ` : "";
|
|
291
|
+
if (constraints.length) {
|
|
270
292
|
swatches.push({
|
|
271
|
-
tier, label:
|
|
272
|
-
svg: resolveSpriteAsset(cls, [],
|
|
293
|
+
tier, label: variantLabel, kind: "variant", property: constraints.map((c) => c.property).join(" + "),
|
|
294
|
+
svg: resolveSpriteAsset(cls, [], matchFact, templates, registry, { instanceKey: `${cls}-${tier}-match-${variantKey}` }),
|
|
273
295
|
});
|
|
274
296
|
}
|
|
275
297
|
for (const v of parameterVariantsFor(t)) {
|
|
276
|
-
|
|
298
|
+
if (constraints.length && shownByUnmatchedTemplate.has(valueKey(v))) continue;
|
|
299
|
+
const propertyFacts = [...matchFact, { predicate: v.property, object: v.rawValue }];
|
|
277
300
|
swatches.push({
|
|
278
|
-
tier, label: v.rawValue
|
|
279
|
-
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${v.paramName}-${v.rawValue}` }),
|
|
301
|
+
tier, label: `${matchPrefix}${v.rawValue}`, kind: "material", property: v.property, treatment: v.treatment,
|
|
302
|
+
svg: resolveSpriteAsset(cls, [], propertyFacts, templates, registry, { instanceKey: `${cls}-${tier}-${matchPrefix ? `${variantKey}-` : ""}${v.paramName}-${v.rawValue}` }),
|
|
280
303
|
});
|
|
281
304
|
}
|
|
282
305
|
}
|
|
@@ -311,121 +334,95 @@ export function buildSpriteCatalogEntries({ iconTemplates = [], largeTemplates =
|
|
|
311
334
|
});
|
|
312
335
|
}
|
|
313
336
|
|
|
314
|
-
// ---- scene composer
|
|
337
|
+
// ---- scene composer ----
|
|
315
338
|
//
|
|
316
|
-
// The free-text "there is a..." box (
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
// (className -> real swatch labels, read straight off this page's own
|
|
339
|
+
// The free-text "there is a..." box (adventure-viz.mjs's roomSceneObjects/
|
|
340
|
+
// room-frame is the same shape) over THIS page's own already-real classes.
|
|
341
|
+
// The parser itself is scene-compose.mjs, which resolves a typed class name
|
|
342
|
+
// through the real resolver; this page owns only the class index it is matched
|
|
343
|
+
// against (className -> real swatch labels, read straight off this page's own
|
|
321
344
|
// already-rendered `.card`/`.swatch-label` markup at load time — see this
|
|
322
345
|
// module's own header for why that beats re-embedding the same SVG data a
|
|
323
|
-
// second time) is built client-side
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
346
|
+
// second time), which is built client-side, since walking rendered DOM has no
|
|
347
|
+
// meaning in this pure module.
|
|
348
|
+
|
|
349
|
+
export { extractSceneItems } from "../domain/scene-compose.mjs";
|
|
350
|
+
|
|
351
|
+
// ---- the catalog question lane ----
|
|
352
|
+
//
|
|
353
|
+
// There isn't one. Every catalog-shaped question the dock takes — "how many
|
|
354
|
+
// sprite classes are there", "list the sprite classes", "what parameters does
|
|
355
|
+
// a person sprite take", "what emotions does a person sprite accept" — is
|
|
356
|
+
// answered by the ordinary chat engine reading the same sprite-facts rows this
|
|
357
|
+
// page embeds, through the membership, count and property lanes it already runs
|
|
358
|
+
// for every other caller. The predicate spellings sprite-facts.mjs mints
|
|
359
|
+
// (mgx:take-parameter, mgx:accept-emotion, mgx:offer-variant) are what those
|
|
360
|
+
// lanes read, so the page hands the line over and renders what comes back.
|
|
361
|
+
|
|
362
|
+
// ---- the animated swatches (pure) ----
|
|
363
|
+
//
|
|
364
|
+
// Each card's large tier carries three animated swatches, one per axis the
|
|
365
|
+
// sprite tier actually varies on: mood (mgx:feels), facing (mgx:faces) and
|
|
366
|
+
// pose (mgx:pose). The page builds their frames client-side out of its own
|
|
367
|
+
// already-rendered swatches (see the inline script below — the same
|
|
368
|
+
// read-the-DOM posture the scene composer's class index takes), but the
|
|
369
|
+
// frame ORDER and the tempo are real logic worth pinning on their own, so
|
|
370
|
+
// they live here as pure functions the page splices in by `.toString()`.
|
|
371
|
+
// One delay constant serves all three, so the three can't drift apart into
|
|
372
|
+
// three tempos.
|
|
373
|
+
//
|
|
374
|
+
// A `frame` is `{svg, label}`. An empty return means there is nothing worth
|
|
375
|
+
// animating for that class, and the page leaves its static swatch alone.
|
|
376
|
+
|
|
377
|
+
/** The step between animated swatch frames, in milliseconds — the mood
|
|
378
|
+
* cycle's own established tempo, shared by the facing sweep and the pose
|
|
379
|
+
* toggle. */
|
|
380
|
+
export const CYCLE_FRAME_DELAY_MS = 800;
|
|
381
|
+
|
|
382
|
+
/** The turntable order the facing sweep steps through: one continuous
|
|
383
|
+
* rotation from the sprite's own left profile round to its right. `null`
|
|
384
|
+
* is the centre pose — the plain sprite, no mgx:faces fact at all — which
|
|
385
|
+
* sits in the middle of the sweep exactly where the geometry puts it, so
|
|
386
|
+
* the turn reads as a rotation rather than a jump back through front. */
|
|
387
|
+
export const FACING_TURN_ORDER = Object.freeze(["left", "half-left", null, "half-right", "right"]);
|
|
388
|
+
|
|
389
|
+
/** The mood cycle's frames: the class's plain sprite, then every mood it
|
|
390
|
+
* has a rendered swatch for, in the order the templates declare them. Two
|
|
391
|
+
* moods is the floor — one mood alternating with plain is a blink, not a
|
|
392
|
+
* cycle. Pure; self-contained (no outer refs), `.toString()`-splice safe. */
|
|
393
|
+
export function moodFrameSequence(plainFrame, moodFrames) {
|
|
394
|
+
const moods = (moodFrames || []).filter(Boolean);
|
|
395
|
+
if (moods.length < 2) return [];
|
|
396
|
+
return plainFrame ? [plainFrame, ...moods] : moods;
|
|
335
397
|
}
|
|
336
398
|
|
|
337
|
-
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
* first, so the longer name always wins the position it starts at. Pure. */
|
|
352
|
-
export function extractSceneItems(text, classIndex) {
|
|
353
|
-
const index = classIndex || {};
|
|
354
|
-
const candidates = Object.keys(index)
|
|
355
|
-
.map((name) => ({ name, words: name.toLowerCase().split(/\s+/).filter(Boolean) }))
|
|
356
|
-
.filter((c) => c.words.length)
|
|
357
|
-
.sort((a, b) => b.words.length - a.words.length || b.name.length - a.name.length);
|
|
358
|
-
const tokens = tokenizeSceneText(text);
|
|
359
|
-
const used = new Array(tokens.length).fill(false);
|
|
360
|
-
const items = [];
|
|
361
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
362
|
-
if (used[i]) continue;
|
|
363
|
-
const hit = candidates.find(({ words }) => {
|
|
364
|
-
if (i + words.length > tokens.length) return false;
|
|
365
|
-
for (let k = 0; k < words.length; k += 1) {
|
|
366
|
-
if (used[i + k] || tokens[i + k].word !== words[k]) return false;
|
|
367
|
-
}
|
|
368
|
-
return true;
|
|
369
|
-
});
|
|
370
|
-
if (!hit) continue;
|
|
371
|
-
let materialLabel = null;
|
|
372
|
-
if (i > 0 && !used[i - 1]) {
|
|
373
|
-
const materials = index[hit.name]?.materials || {};
|
|
374
|
-
const prevWord = tokens[i - 1].word;
|
|
375
|
-
if (Object.prototype.hasOwnProperty.call(materials, prevWord)) {
|
|
376
|
-
materialLabel = prevWord;
|
|
377
|
-
used[i - 1] = true;
|
|
378
|
-
}
|
|
399
|
+
/** The facing sweep's frames: `order` (FACING_TURN_ORDER) filtered down to
|
|
400
|
+
* the facings this class actually has a rendered swatch for, with
|
|
401
|
+
* `centreFrame` — the plain, undirected sprite — dropped into the centre
|
|
402
|
+
* slot. `facingFrames` is keyed by each swatch's own label, which is its
|
|
403
|
+
* real mgx:faces value. A class with one facing or none has no sweep.
|
|
404
|
+
* Pure; self-contained, `.toString()`-splice safe. */
|
|
405
|
+
export function turnFrameSequence(order, centreFrame, facingFrames) {
|
|
406
|
+
const byFacing = facingFrames || {};
|
|
407
|
+
const frames = [];
|
|
408
|
+
for (const facing of order || []) {
|
|
409
|
+
if (facing === null) {
|
|
410
|
+
if (centreFrame) frames.push({ svg: centreFrame.svg, label: "centre" });
|
|
411
|
+
} else if (byFacing[facing]) {
|
|
412
|
+
frames.push({ svg: byFacing[facing].svg, label: facing });
|
|
379
413
|
}
|
|
380
|
-
for (let k = 0; k < hit.words.length; k += 1) used[i + k] = true;
|
|
381
|
-
items.push({ className: hit.name, materialLabel });
|
|
382
414
|
}
|
|
383
|
-
return
|
|
415
|
+
return frames.length >= 2 ? frames : [];
|
|
384
416
|
}
|
|
385
417
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
* including a catalog-shaped question about a class with no rows on record —
|
|
395
|
-
* the caller falls through to the engine, whose refusal is the honest miss.
|
|
396
|
-
* Self-contained (no outer refs), `.toString()`-splice safe. Pure. */
|
|
397
|
-
export function answerSpriteQuestion(text, rows) {
|
|
398
|
-
const all = Array.isArray(rows) ? rows : [];
|
|
399
|
-
const q = String(text ?? "").trim().toLowerCase().replace(/[?.!\s]+$/, "");
|
|
400
|
-
|
|
401
|
-
if (/^what (?:classes|sprites|sprite classes) (?:can you|do you|are there)(?:\s+(?:render|draw|show))?$/.test(q)) {
|
|
402
|
-
const classes = all
|
|
403
|
-
.filter((r) => r.predicate === "rdf:type" && r.object === "sprite class")
|
|
404
|
-
.map((r) => String(r.subject).replace(/ sprite$/, ""));
|
|
405
|
-
if (!classes.length) return null;
|
|
406
|
-
const sample = classes.slice(0, 10).join(", ");
|
|
407
|
-
return {
|
|
408
|
-
answer: `${classes.length} sprite classes are on record — ${sample}, … (the catalog below lists every one).`,
|
|
409
|
-
grounding: classes.length,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const m = q.match(/^(?:what|which) parameters? (?:does|do|can|will) (?:a |an |the )?(.+?)(?: sprite)? (?:take|accept|use)s?$/);
|
|
414
|
-
if (m) {
|
|
415
|
-
const subject = `${m[1]} sprite`;
|
|
416
|
-
const params = all.filter((r) => r.subject === subject && r.predicate === "mgx:take-parameter").map((r) => r.object);
|
|
417
|
-
if (!params.length) return null;
|
|
418
|
-
let grounding = params.length;
|
|
419
|
-
const parts = params.map((p) => {
|
|
420
|
-
const values = all.filter((r) => r.subject === subject && r.predicate === `mgx:accept-${p}`).map((r) => r.object);
|
|
421
|
-
grounding += values.length;
|
|
422
|
-
return values.length ? `${p} (${values.join(", ")})` : p;
|
|
423
|
-
});
|
|
424
|
-
const lead = params.length === 1 ? "one parameter" : `${params.length} parameters`;
|
|
425
|
-
return { answer: `a ${subject} takes ${lead}: ${parts.join("; ")}.`, grounding };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return null;
|
|
418
|
+
/** The pose toggle's frames: the class's plain idle sprite alternating with
|
|
419
|
+
* its mgx:pose = "moving" one. Both have to be there — a class with no
|
|
420
|
+
* moving sprite has no second state to toggle into, and gets nothing
|
|
421
|
+
* rather than a still frame pretending to animate. Pure; self-contained,
|
|
422
|
+
* `.toString()`-splice safe. */
|
|
423
|
+
export function movingFrameSequence(idleFrame, movingFrame) {
|
|
424
|
+
if (!idleFrame || !movingFrame) return [];
|
|
425
|
+
return [{ svg: idleFrame.svg, label: "idle" }, { svg: movingFrame.svg, label: "moving" }];
|
|
429
426
|
}
|
|
430
427
|
|
|
431
428
|
// ---- rendering ----
|
|
@@ -505,14 +502,16 @@ function sectionHtml(group, entries, { clusterBy = null } = {}) {
|
|
|
505
502
|
* ancestor chains.
|
|
506
503
|
*
|
|
507
504
|
* `spritesBundleAvailable: true` (ledger-viz.mjs's own ledgerBundleAvailable
|
|
508
|
-
* idiom) is what adds the
|
|
509
|
-
* sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
|
|
505
|
+
* idiom) is what adds the two interactive panels at all: the page then embeds
|
|
506
|
+
* the sprite-facts rows (src/domain/sprite-facts.mjs, derived purely from the
|
|
510
507
|
* same two template sets) and references the sibling
|
|
511
508
|
* ./sprites-browser.bundle.js scripts/build-demo-site.mjs builds alongside
|
|
512
|
-
* it.
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
509
|
+
* it. Both panels need that bundle — the dock for its chat session, the scene
|
|
510
|
+
* composer for the resolver its parser asks. Left false, the page is the
|
|
511
|
+
* catalog alone: no dock, no composer, no bundle reference, nothing extra to
|
|
512
|
+
* 404 — including the favicon links, since the CLI's standalone export (also
|
|
513
|
+
* `spritesBundleAvailable: false`) can't carry a dangling relative
|
|
514
|
+
* ./favicon.svg either. */
|
|
516
515
|
export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false } = {}) {
|
|
517
516
|
const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
|
|
518
517
|
const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
|
|
@@ -562,22 +561,56 @@ export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates =
|
|
|
562
561
|
</form>
|
|
563
562
|
<div class="pills" id="dockPills" role="group" aria-label="quick questions to ask">
|
|
564
563
|
<button type="button" class="pill" data-q="what parameters does a person sprite take?">person parameters</button>
|
|
565
|
-
<button type="button" class="pill" data-q="what
|
|
566
|
-
<button type="button" class="pill" data-q="what
|
|
564
|
+
<button type="button" class="pill" data-q="what emotions does a person sprite accept?">person emotions</button>
|
|
565
|
+
<button type="button" class="pill" data-q="what materials does a cabinet sprite accept?">cabinet materials</button>
|
|
566
|
+
<button type="button" class="pill" data-q="how many sprite classes are there?">classes on record</button>
|
|
567
567
|
<button type="button" class="pill" data-q="what is a portrait sprite?">about the portrait sprite</button>
|
|
568
568
|
</div>
|
|
569
569
|
<div class="dock-status mono" id="dockStatus">loading the engine…</div>
|
|
570
570
|
</section>
|
|
571
571
|
</div>`;
|
|
572
572
|
|
|
573
|
+
// The bundle carries the scene composer's parser as well as the dock's
|
|
574
|
+
// session, and the catalog script below calls it directly, so it loads ahead
|
|
575
|
+
// of that script rather than beside the dock's own wiring at the end.
|
|
576
|
+
const spriteBundleScript = !spritesBundleAvailable ? "" : `<script src="./sprites-browser.bundle.js"></script>`;
|
|
577
|
+
|
|
578
|
+
// The composer resolves a typed class name through the real resolver, which
|
|
579
|
+
// reaches the page in that bundle. Without it there is nothing to type into,
|
|
580
|
+
// so the panel stays out rather than rendering a box that can't answer.
|
|
581
|
+
const composerHtml = !spritesBundleAvailable ? "" : `<div class="composer">
|
|
582
|
+
<section class="panel compose-panel" aria-label="Describe a scene">
|
|
583
|
+
<h2>describe a scene</h2>
|
|
584
|
+
<form class="composeform" id="composeForm">
|
|
585
|
+
<span class="prompt mono">there is a</span>
|
|
586
|
+
<input id="composeq" type="text" autocomplete="off"
|
|
587
|
+
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
588
|
+
aria-label="Continue the sentence: there is a…">
|
|
589
|
+
</form>
|
|
590
|
+
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
591
|
+
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
592
|
+
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
593
|
+
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
594
|
+
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
595
|
+
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
596
|
+
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
597
|
+
</div>
|
|
598
|
+
</section>
|
|
599
|
+
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
600
|
+
<h2>the scene</h2>
|
|
601
|
+
<div class="scene-frame" id="sceneFrame">
|
|
602
|
+
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
603
|
+
<span class="empty-note" id="sceneEmpty">nothing recognized yet. Try “a doctor with a hat, and a cabinet”.</span>
|
|
604
|
+
</div>
|
|
605
|
+
</section>
|
|
606
|
+
</div>`;
|
|
607
|
+
|
|
573
608
|
const dockScripts = !spritesBundleAvailable ? "" : `<script>
|
|
574
609
|
const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
575
610
|
</script>
|
|
576
|
-
<script src="./sprites-browser.bundle.js"></script>
|
|
577
611
|
<script>
|
|
578
612
|
(function () {
|
|
579
613
|
"use strict";
|
|
580
|
-
const answerSpriteQuestion = ${answerSpriteQuestion.toString()};
|
|
581
614
|
const dockLogEl = document.getElementById("dockLog");
|
|
582
615
|
const dockFormEl = document.getElementById("dockForm");
|
|
583
616
|
const dockqEl = document.getElementById("dockq");
|
|
@@ -598,7 +631,7 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
598
631
|
import("./vendor/wink.js"),
|
|
599
632
|
winkTimeout(WINK_LOAD_TIMEOUT_MS, "wink vendor asset load timed out"),
|
|
600
633
|
]);
|
|
601
|
-
|
|
634
|
+
tmct.page.registerWinkModel(() => ({ winkNLP: mod.winkNLP, model: mod.model }));
|
|
602
635
|
} catch (err) {
|
|
603
636
|
console.warn("tmct sprites: the wink vendor asset failed to load, continuing without the lemma/POS tier", err);
|
|
604
637
|
}
|
|
@@ -626,12 +659,11 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
626
659
|
if (!q) return;
|
|
627
660
|
dockqEl.value = "";
|
|
628
661
|
addDockLine("u", q);
|
|
629
|
-
const local = answerSpriteQuestion(q, SPRITE_CHAT.rows);
|
|
630
|
-
if (local) { addDockLine("a grounded", local.answer); return; }
|
|
631
662
|
if (!session) { addDockLine("a miss", "the engine is still loading. Try again in a moment."); return; }
|
|
632
663
|
withLock(async () => {
|
|
633
|
-
const result = await
|
|
634
|
-
|
|
664
|
+
const result = await tmct.turn(q);
|
|
665
|
+
const missed = !!(result.record && result.record.miss);
|
|
666
|
+
addDockLine(missed ? "a miss" : "a grounded", result.answer);
|
|
635
667
|
});
|
|
636
668
|
});
|
|
637
669
|
|
|
@@ -645,7 +677,7 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
|
|
|
645
677
|
(async () => {
|
|
646
678
|
try {
|
|
647
679
|
await tryLoadWink();
|
|
648
|
-
session = await
|
|
680
|
+
session = await tmct.open({ factRows: SPRITE_CHAT.rows });
|
|
649
681
|
dockqEl.disabled = false;
|
|
650
682
|
dockStatusEl.textContent = SPRITE_CHAT.rows.length + " sprite facts on record. Ask away, or use a quick question.";
|
|
651
683
|
} catch (err) {
|
|
@@ -757,32 +789,7 @@ ${dockCss}</style>
|
|
|
757
789
|
<h1>Sprites</h1>
|
|
758
790
|
<span class="doc-sub">tmct · the sprite library</span>
|
|
759
791
|
</header>
|
|
760
|
-
|
|
761
|
-
<section class="panel compose-panel" aria-label="Describe a scene">
|
|
762
|
-
<h2>describe a scene</h2>
|
|
763
|
-
<form class="composeform" id="composeForm">
|
|
764
|
-
<span class="prompt mono">there is a</span>
|
|
765
|
-
<input id="composeq" type="text" autocomplete="off"
|
|
766
|
-
placeholder="red lamp, a doctor with a hat, and a cabinet"
|
|
767
|
-
aria-label="Continue the sentence: there is a…">
|
|
768
|
-
</form>
|
|
769
|
-
<div class="pills" id="composePills" role="group" aria-label="quick words to add">
|
|
770
|
-
<button type="button" class="pill" data-fill="doctor">doctor</button>
|
|
771
|
-
<button type="button" class="pill" data-fill="hat">hat</button>
|
|
772
|
-
<button type="button" class="pill" data-fill="wood cabinet">wood cabinet</button>
|
|
773
|
-
<button type="button" class="pill" data-fill="glass lamp">glass lamp</button>
|
|
774
|
-
<button type="button" class="pill" data-fill="cat">cat</button>
|
|
775
|
-
<button type="button" class="pill" data-fill="garden">garden</button>
|
|
776
|
-
</div>
|
|
777
|
-
</section>
|
|
778
|
-
<section class="panel viewer-panel" aria-label="The composed scene">
|
|
779
|
-
<h2>the scene</h2>
|
|
780
|
-
<div class="scene-frame" id="sceneFrame">
|
|
781
|
-
<div class="scene-row" id="sceneRow" aria-live="polite"></div>
|
|
782
|
-
<span class="empty-note" id="sceneEmpty">nothing recognized yet. Try “a doctor with a hat, and a cabinet”.</span>
|
|
783
|
-
</div>
|
|
784
|
-
</section>
|
|
785
|
-
</div>
|
|
792
|
+
${composerHtml}
|
|
786
793
|
${dockHtml}
|
|
787
794
|
<div class="topbar">
|
|
788
795
|
<nav aria-label="Jump to group">${navHtml}</nav>
|
|
@@ -797,6 +804,7 @@ ${dockCss}</style>
|
|
|
797
804
|
<script>
|
|
798
805
|
const SPRITE_CATALOG = ${pageData};
|
|
799
806
|
</script>
|
|
807
|
+
${spriteBundleScript}
|
|
800
808
|
<script>
|
|
801
809
|
(function () {
|
|
802
810
|
"use strict";
|
|
@@ -824,10 +832,10 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
824
832
|
// THIS page's own already-rendered card and swatch-label markup (this
|
|
825
833
|
// module's own header explains why: never a second embedded copy of the
|
|
826
834
|
// same swatch data), so the composed scene below only ever shows a sprite
|
|
827
|
-
// this same page already proved the resolver draws.
|
|
835
|
+
// this same page already proved the resolver draws. Which class a typed
|
|
836
|
+
// word names is the bundle's extractSceneItems, which asks the real
|
|
837
|
+
// resolver — the page never matches a class name itself.
|
|
828
838
|
const esc = ${escapeHtml.toString()};
|
|
829
|
-
const tokenizeSceneText = ${tokenizeSceneText.toString()};
|
|
830
|
-
const extractSceneItems = ${extractSceneItems.toString()};
|
|
831
839
|
|
|
832
840
|
function buildClassIndexFromDom() {
|
|
833
841
|
const index = {};
|
|
@@ -854,72 +862,83 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
854
862
|
return index;
|
|
855
863
|
}
|
|
856
864
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
865
|
+
function wireSceneComposer() {
|
|
866
|
+
const composeqEl = document.getElementById("composeq");
|
|
867
|
+
if (!composeqEl) return;
|
|
868
|
+
const extractSceneItems = tmct.page.extractSceneItems;
|
|
869
|
+
const composeFormEl = document.getElementById("composeForm");
|
|
870
|
+
const composePillsEl = document.getElementById("composePills");
|
|
871
|
+
const sceneRowEl = document.getElementById("sceneRow");
|
|
872
|
+
const sceneEmptyEl = document.getElementById("sceneEmpty");
|
|
873
|
+
const classIndex = buildClassIndexFromDom();
|
|
874
|
+
|
|
875
|
+
function renderScene(text) {
|
|
876
|
+
const items = extractSceneItems(text, classIndex);
|
|
877
|
+
if (!items.length) {
|
|
878
|
+
sceneRowEl.innerHTML = "";
|
|
879
|
+
sceneEmptyEl.hidden = false;
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
sceneEmptyEl.hidden = true;
|
|
883
|
+
sceneRowEl.innerHTML = items.map((item) => {
|
|
884
|
+
const entry = classIndex[item.className];
|
|
885
|
+
if (!entry) return "";
|
|
886
|
+
const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
|
|
887
|
+
const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
|
|
888
|
+
return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
|
|
889
|
+
}).join("");
|
|
870
890
|
}
|
|
871
|
-
sceneEmptyEl.hidden = true;
|
|
872
|
-
sceneRowEl.innerHTML = items.map((item) => {
|
|
873
|
-
const entry = classIndex[item.className];
|
|
874
|
-
if (!entry) return "";
|
|
875
|
-
const svg = (item.materialLabel && entry.materials[item.materialLabel]) || entry.defaultSvg || "";
|
|
876
|
-
const label = item.materialLabel ? item.materialLabel + " " + item.className : item.className;
|
|
877
|
-
return '<div class="scene-card"><div class="scene-sprite">' + svg + '</div><div class="scene-label">' + esc(label) + "</div></div>";
|
|
878
|
-
}).join("");
|
|
879
|
-
}
|
|
880
891
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
//
|
|
900
|
-
//
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
|
|
892
|
+
composeqEl.addEventListener("input", () => renderScene(composeqEl.value));
|
|
893
|
+
composeFormEl.addEventListener("submit", (e) => { e.preventDefault(); renderScene(composeqEl.value); });
|
|
894
|
+
composePillsEl.addEventListener("click", (e) => {
|
|
895
|
+
const btn = e.target.closest(".pill");
|
|
896
|
+
if (!btn) return;
|
|
897
|
+
const phrase = btn.dataset.fill || "";
|
|
898
|
+
const current = composeqEl.value.trim();
|
|
899
|
+
composeqEl.value = current ? current + ", a " + phrase : phrase;
|
|
900
|
+
composeqEl.focus();
|
|
901
|
+
renderScene(composeqEl.value);
|
|
902
|
+
});
|
|
903
|
+
renderScene("");
|
|
904
|
+
}
|
|
905
|
+
wireSceneComposer();
|
|
906
|
+
|
|
907
|
+
// ---- the animated swatches — a card's large tier gains one moving
|
|
908
|
+
// swatch per axis its own templates actually vary on: mood (mgx:feels),
|
|
909
|
+
// facing (mgx:faces) and pose (mgx:pose). Frames are the card's own
|
|
910
|
+
// already-rendered swatches (the same read-the-DOM posture classIndex
|
|
911
|
+
// takes above — never a second embedded copy), ordered by the pure
|
|
912
|
+
// *FrameSequence functions spliced in below, and one shared interval
|
|
913
|
+
// steps every cycle in sync at one shared delay. The mood cycle leads the
|
|
914
|
+
// row; the facing sweep and the pose toggle take over the static plain
|
|
915
|
+
// and happy swatches' own places rather than adding cells, so the row
|
|
916
|
+
// keeps its width. Reduced motion disables the auto-step only; each frame
|
|
917
|
+
// is a button, so a click or Enter always advances one frame by hand.
|
|
918
|
+
// Injected AFTER buildClassIndexFromDom ran, so a moving frame never
|
|
919
|
+
// leaks into the composer's material index.
|
|
920
|
+
const CYCLE_FRAME_DELAY_MS = ${CYCLE_FRAME_DELAY_MS};
|
|
921
|
+
const FACING_TURN_ORDER = ${embedJson(FACING_TURN_ORDER)};
|
|
922
|
+
const MOOD_PROPERTY = "mgx:feels";
|
|
923
|
+
const FACING_PROPERTY = "mgx:faces";
|
|
924
|
+
const POSE_PROPERTY = "mgx:pose";
|
|
925
|
+
const moodFrameSequence = ${moodFrameSequence.toString()};
|
|
926
|
+
const turnFrameSequence = ${turnFrameSequence.toString()};
|
|
927
|
+
const movingFrameSequence = ${movingFrameSequence.toString()};
|
|
905
928
|
const cycleSteppers = [];
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
if (frames.length < 2) continue;
|
|
917
|
-
const baseImg = swatchRow.querySelector(".swatch.plain .swatch-img, .swatch.fallback .swatch-img");
|
|
918
|
-
if (baseImg) frames.unshift({ svg: baseImg.innerHTML, label: card.dataset.cls });
|
|
929
|
+
|
|
930
|
+
function frameFromSwatch(swatch) {
|
|
931
|
+
const img = swatch && swatch.querySelector(".swatch-img");
|
|
932
|
+
const label = swatch && swatch.querySelector(".swatch-label");
|
|
933
|
+
if (!img || !label) return null;
|
|
934
|
+
return { svg: img.innerHTML, label: label.textContent.trim() };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function makeCycleSwatch(frames, ariaLabel, kind) {
|
|
919
938
|
const holder = document.createElement("div");
|
|
920
|
-
holder.className = "swatch large cycle";
|
|
921
|
-
holder.innerHTML = '<button type="button" class="swatch-img" aria-label="
|
|
922
|
-
+
|
|
939
|
+
holder.className = "swatch large cycle cycle-" + kind;
|
|
940
|
+
holder.innerHTML = '<button type="button" class="swatch-img" aria-label="' + esc(ariaLabel)
|
|
941
|
+
+ '"></button><div class="swatch-caption"><span class="swatch-label"></span></div>';
|
|
923
942
|
const frameImgEl = holder.querySelector(".swatch-img");
|
|
924
943
|
const frameLabelEl = holder.querySelector(".swatch-label");
|
|
925
944
|
let frameIndex = 0;
|
|
@@ -930,12 +949,43 @@ const SPRITE_CATALOG = ${pageData};
|
|
|
930
949
|
const stepFrame = () => { frameIndex = (frameIndex + 1) % frames.length; showFrame(); };
|
|
931
950
|
frameImgEl.addEventListener("click", stepFrame);
|
|
932
951
|
showFrame();
|
|
933
|
-
swatchRow.insertBefore(holder, swatchRow.firstChild);
|
|
934
952
|
cycleSteppers.push(stepFrame);
|
|
953
|
+
return holder;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
for (const card of cards) {
|
|
957
|
+
const swatchRow = card.querySelector('.tier-row[data-tier="large"] .swatches');
|
|
958
|
+
if (!swatchRow) continue;
|
|
959
|
+
const cls = card.dataset.cls;
|
|
960
|
+
const rows = Array.from(swatchRow.querySelectorAll(".swatch"))
|
|
961
|
+
.map((el) => ({ el, property: el.dataset.property || "", frame: frameFromSwatch(el) }))
|
|
962
|
+
.filter((r) => r.frame);
|
|
963
|
+
const baseSwatch = swatchRow.querySelector(".swatch.plain, .swatch.fallback");
|
|
964
|
+
const baseFrame = frameFromSwatch(baseSwatch);
|
|
965
|
+
|
|
966
|
+
const moodRows = rows.filter((r) => r.property === MOOD_PROPERTY);
|
|
967
|
+
const moodFrames = moodFrameSequence(baseFrame && { svg: baseFrame.svg, label: cls }, moodRows.map((r) => r.frame));
|
|
968
|
+
if (moodFrames.length) {
|
|
969
|
+
swatchRow.insertBefore(makeCycleSwatch(moodFrames, "step through the " + cls + " expressions", "mood"), swatchRow.firstChild);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const facingFrames = {};
|
|
973
|
+
for (const r of rows) { if (r.property === FACING_PROPERTY) facingFrames[r.frame.label] = r.frame; }
|
|
974
|
+
const turnFrames = turnFrameSequence(FACING_TURN_ORDER, baseFrame, facingFrames);
|
|
975
|
+
if (turnFrames.length && baseSwatch) {
|
|
976
|
+
swatchRow.replaceChild(makeCycleSwatch(turnFrames, "watch the " + cls + " turn through every direction", "turn"), baseSwatch);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const movingRow = rows.find((r) => r.property === POSE_PROPERTY && r.frame.label === "moving");
|
|
980
|
+
const movingFrames = movingFrameSequence(baseFrame, movingRow && movingRow.frame);
|
|
981
|
+
if (movingFrames.length) {
|
|
982
|
+
const happyRow = moodRows.find((r) => r.frame.label === "happy");
|
|
983
|
+
swatchRow.replaceChild(makeCycleSwatch(movingFrames, "watch the " + cls + " move", "moving"), (happyRow || movingRow).el);
|
|
984
|
+
}
|
|
935
985
|
}
|
|
936
986
|
const reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
937
987
|
if (cycleSteppers.length && !reducedMotion) {
|
|
938
|
-
setInterval(() => { for (const stepFrame of cycleSteppers) stepFrame(); },
|
|
988
|
+
setInterval(() => { for (const stepFrame of cycleSteppers) stepFrame(); }, CYCLE_FRAME_DELAY_MS);
|
|
939
989
|
}
|
|
940
990
|
})();
|
|
941
991
|
</script>
|