@alexkroman1/aai-cli 6.10.1 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-B4RqNF5Z.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-CiKFH9qw.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-CE4HKdgV.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-nRFaBEPJ.mjs} +2 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-FI3xPEMj.mjs} +1 -1
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-6jfaakgc.mjs} +5 -2
- package/dist/cli.mjs +30 -25
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DvYeD8ia.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-B59JIfWO.mjs} +1 -1
- package/dist/{eject-31gjtaHF.mjs → eject-C9WJyyr2.mjs} +1 -1
- package/dist/{init-l6xfU4xX.mjs → init-CpJVjEg0.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-9q-psa4q.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +93 -14
- package/dist/scaffold/package.json +5 -3
- package/dist/scaffold/server.mjs +13 -4
- package/dist/scaffold/vite.config.ts +1 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-CVvSLIDV.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-BvUrnvM3.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio--MUV0cid.mjs} +4 -4
- package/dist/templates/call-audit/agent.test.ts +818 -0
- package/dist/templates/call-audit/agent.ts +171 -0
- package/dist/templates/call-audit/client.tsx +198 -0
- package/dist/templates/call-audit/workflows/audit.ts +298 -0
- package/dist/templates/call-audit/workflows/ingest.ts +251 -0
- package/dist/templates/call-audit/workflows/media.ts +637 -0
- package/dist/templates/call-audit/workflows/summarize.ts +201 -0
- package/dist/templates/call-audit/workflows/sync-api.ts +44 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.test.ts +4 -6
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/pizza-ordering/agent.test.ts +27 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.test.ts +746 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +411 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +209 -0
- package/dist/templates/recap-workflow/agent.test.ts +107 -96
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +48 -41
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/agent.test.ts +26 -23
- package/dist/templates/retail/client.tsx +226 -117
- package/dist/templates/retail/registry.test.ts +38 -6
- package/dist/templates/retail/store.test.ts +82 -15
- package/dist/templates/retail/store.ts +174 -47
- package/dist/templates/retail/system-prompt.md +11 -2
- package/dist/templates/retail/tools/cancel_pending_order.ts +2 -2
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +2 -2
- package/dist/templates/retail/tools/modify_pending_order_items.ts +2 -3
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +2 -2
- package/dist/templates/retail/tools/modify_user_address.ts +2 -2
- package/dist/templates/retail/tools/return_delivered_order_items.ts +2 -2
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +8 -4
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.test.ts +314 -0
- package/dist/templates/spoken-summary/agent.ts +155 -0
- package/dist/templates/spoken-summary/client.tsx +185 -0
- package/dist/templates/spoken-summary/workflows/summarize.ts +237 -0
- package/dist/templates/spoken-summary/workflows/transcribe.ts +138 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/tools/answer_question.ts +17 -4
- package/dist/templates/transcription-workflow/agent.test.ts +246 -123
- package/dist/templates/transcription-workflow/agent.ts +21 -7
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +79 -180
- package/dist/templates/transcription-workflow/workflows/normalize.ts +247 -0
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +10 -9
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +26 -94
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +55 -40
- package/dist/templates/transcription-workflow/workflows/wav.ts +31 -31
- package/dist/templates/travel-concierge/agent.test.ts +64 -33
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +70 -3
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_excursion.ts +2 -2
- package/dist/templates/travel-concierge/tools/book_hotel.ts +2 -2
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +2 -2
- package/dist/templates/travel-concierge/tools/update_ticket.ts +2 -2
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-COxnqstQ.mjs} +221 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-D2AQf2Pl.mjs} +28 -1
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeepReadonly, sessionSlot, type ToolContext } from "@alexkroman1/aai";
|
|
1
|
+
import { type DeepReadonly, dialog, sessionSlot, type ToolContext } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
4
|
// ── Tuning Constants ─────────────────────────────────────────────────────────
|
|
@@ -247,8 +247,16 @@ export interface LastRoll {
|
|
|
247
247
|
|
|
248
248
|
// ── Game State ───────────────────────────────────────────────────────────────
|
|
249
249
|
export interface GameState {
|
|
250
|
+
/**
|
|
251
|
+
* Whether a character exists. The CLIENT's render flag — `client.tsx` shows
|
|
252
|
+
* the character sheet on it — and nothing else.
|
|
253
|
+
*
|
|
254
|
+
* It used to sit beside `phase: "genre" | "playing"`, which was the same bit
|
|
255
|
+
* spelled twice: `phase === "playing"` was true exactly when this was, and
|
|
256
|
+
* neither field gated anything. `phase` is gone and {@link storyFlow} holds
|
|
257
|
+
* the position now — see its own doc for the split.
|
|
258
|
+
*/
|
|
250
259
|
initialized: boolean;
|
|
251
|
-
phase: "genre" | "playing";
|
|
252
260
|
settingGenre: string;
|
|
253
261
|
settingTone: string;
|
|
254
262
|
settingArchetype: string;
|
|
@@ -285,7 +293,6 @@ export interface GameState {
|
|
|
285
293
|
|
|
286
294
|
export const DEFAULT_STATE: GameState = {
|
|
287
295
|
initialized: false,
|
|
288
|
-
phase: "genre",
|
|
289
296
|
settingGenre: "",
|
|
290
297
|
settingTone: "",
|
|
291
298
|
settingArchetype: "",
|
|
@@ -326,7 +333,15 @@ export const DEFAULT_STATE: GameState = {
|
|
|
326
333
|
// `DEFAULT_STATE` is one module-level object shared by every session in the
|
|
327
334
|
// process, so a factory without it would let one player's game show up in
|
|
328
335
|
// another's.
|
|
329
|
-
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE)
|
|
336
|
+
export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE), {
|
|
337
|
+
// The derived-field recalculation every mutating tool used to have to
|
|
338
|
+
// remember. It was written out by hand in `applyConsequences`,
|
|
339
|
+
// `revertConsequences` and `update_state`, which is the shape `after` exists
|
|
340
|
+
// to replace: a new tool that empties a track now cannot store a stale
|
|
341
|
+
// `gameOver`, and `gameOver` is what the story flow's `DOWNED` transition
|
|
342
|
+
// reads. `dispatch-center`'s board is the same pattern one template over.
|
|
343
|
+
after: updateCrisisFlags,
|
|
344
|
+
});
|
|
330
345
|
|
|
331
346
|
/**
|
|
332
347
|
* The IDENTITY projection, used by `syncState` and by `useAgentState`: this
|
|
@@ -335,6 +350,95 @@ export const gameSlot = sessionSlot("game", () => structuredClone(DEFAULT_STATE)
|
|
|
335
350
|
*/
|
|
336
351
|
export const gameProjection = gameSlot.projection((game) => game);
|
|
337
352
|
|
|
353
|
+
// ── The story, as a machine ──────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Where the story is, and what may be done from there, as a plain state map.
|
|
357
|
+
*
|
|
358
|
+
* A {@link DialogSpec} rather than an XState machine. This dialog used four
|
|
359
|
+
* events and one instruction per state, which is all a spec can say — and the
|
|
360
|
+
* `setup({ types: {} as { events: … } })` block it used to carry restated the
|
|
361
|
+
* four names already written in the `on` maps. `type: "final"` is `final: true`,
|
|
362
|
+
* and the instruction is a DECLARED field rather than an untyped `meta` bag, so
|
|
363
|
+
* misspelling it is a compile error instead of a refusal that arrives with no
|
|
364
|
+
* recovery text. `as const` keeps the `on` keys literal, so every `send` and
|
|
365
|
+
* `sendFrom` below is checked against the events this spec declares.
|
|
366
|
+
*
|
|
367
|
+
* Three hand-rolled versions of this question lived in the template, and all
|
|
368
|
+
* three were positional questions answered from data:
|
|
369
|
+
*
|
|
370
|
+
* - **`phase: "genre" | "playing"`** (with `initialized` beside it saying the
|
|
371
|
+
* same thing) gated NOTHING. `action_roll`, `update_state` and
|
|
372
|
+
* `burn_momentum` all ran happily before a character existed — rolling 2d6
|
|
373
|
+
* against the stats of nobody, against clocks that were not there.
|
|
374
|
+
* - **`burn_momentum`'s `if (!last) return { error: "No recent action roll to
|
|
375
|
+
* upgrade. Roll first." }`** is `when` written out by hand. Burning is legal
|
|
376
|
+
* for exactly as long as a roll is standing, which is a position.
|
|
377
|
+
* - **`gameOver`** was set by `updateCrisisFlags` and read by nobody who could
|
|
378
|
+
* act on it, so a player whose health and spirit were both gone could keep
|
|
379
|
+
* rolling for as long as they liked.
|
|
380
|
+
*
|
|
381
|
+
* **What stays in {@link GameState} is what the CLIENT renders** —
|
|
382
|
+
* `initialized`, `crisisMode`, `gameOver` — because `syncState` carries one
|
|
383
|
+
* projection and it is the campaign's. The flow holds the POSITION and the slot
|
|
384
|
+
* holds the campaign, which is the same split `travel-concierge` makes between
|
|
385
|
+
* its gate and its trip: one tool call moves both, in one synchronous window
|
|
386
|
+
* each.
|
|
387
|
+
*
|
|
388
|
+
* **Starting over is a RESET, not a transition.** `setup_character` replaces the
|
|
389
|
+
* campaign with a pristine `DEFAULT_STATE`, so the honest mirror on this side is
|
|
390
|
+
* `storyFlow.reset` — and it is what lets `gameOver` be a genuinely `final`
|
|
391
|
+
* state, which is the whole point of having one: `position().done` means the
|
|
392
|
+
* story ended, and XState delivers no events to a done actor, so an `on: {
|
|
393
|
+
* SETUP }` there would have been dead config that looked live. `load_game`
|
|
394
|
+
* resets for the same reason. `SETUP` therefore appears once, on the only state
|
|
395
|
+
* that can be transitioned out of.
|
|
396
|
+
*/
|
|
397
|
+
const storySpec = {
|
|
398
|
+
initial: "awaitingSetup",
|
|
399
|
+
states: {
|
|
400
|
+
awaitingSetup: {
|
|
401
|
+
instruction:
|
|
402
|
+
"There is no character yet. Take whatever the player gave you — a name, a " +
|
|
403
|
+
"genre, an idea, or nothing — infer the rest yourself, and call " +
|
|
404
|
+
"setup_character with every field filled in. Ask no follow-up questions.",
|
|
405
|
+
on: { SETUP: "playing" },
|
|
406
|
+
},
|
|
407
|
+
playing: {
|
|
408
|
+
initial: "awaitingRoll",
|
|
409
|
+
on: { DOWNED: "gameOver" },
|
|
410
|
+
states: {
|
|
411
|
+
awaitingRoll: {
|
|
412
|
+
instruction:
|
|
413
|
+
"Narrate the scene and offer two or three choices. Any risky action goes " +
|
|
414
|
+
"through action_roll — never narrate a success or a failure yourself.",
|
|
415
|
+
on: { ROLLED: "rollResolved" },
|
|
416
|
+
},
|
|
417
|
+
rollResolved: {
|
|
418
|
+
instruction:
|
|
419
|
+
"A roll is standing. burn_momentum can still upgrade it if the player " +
|
|
420
|
+
"spends momentum; anything that moves the scene on settles it.",
|
|
421
|
+
// A second roll replaces the standing one, so `ROLLED` re-enters.
|
|
422
|
+
on: { ROLLED: "rollResolved", SETTLED: "awaitingRoll" },
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
gameOver: {
|
|
427
|
+
final: true,
|
|
428
|
+
instruction:
|
|
429
|
+
"Health and spirit are both gone. Narrate the ending and stop — nothing " +
|
|
430
|
+
"else may be rolled. setup_character begins a new story.",
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
} as const;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* The flow. Its own slot key beside {@link gameSlot}, and the reason it is not
|
|
437
|
+
* folded into the campaign is that the campaign is what the browser renders: a
|
|
438
|
+
* flow stores an XState snapshot, which is not a character sheet.
|
|
439
|
+
*/
|
|
440
|
+
export const storyFlow = dialog("story", storySpec);
|
|
441
|
+
|
|
338
442
|
/**
|
|
339
443
|
* The game as a READ hands it out: deep-frozen, and typed to say so.
|
|
340
444
|
*
|
|
@@ -468,7 +572,6 @@ export function clockSummary(c: DeepReadonly<Clock>) {
|
|
|
468
572
|
export function stateSummary(state: FrozenGameState) {
|
|
469
573
|
return {
|
|
470
574
|
initialized: state.initialized,
|
|
471
|
-
phase: state.phase,
|
|
472
575
|
settingGenre: state.settingGenre,
|
|
473
576
|
settingTone: state.settingTone,
|
|
474
577
|
settingArchetype: state.settingArchetype,
|
|
@@ -494,8 +597,11 @@ export function stateSummary(state: FrozenGameState) {
|
|
|
494
597
|
currentSceneContext: state.currentSceneContext,
|
|
495
598
|
timeOfDay: state.timeOfDay,
|
|
496
599
|
chaosFactor: state.chaosFactor,
|
|
497
|
-
|
|
498
|
-
|
|
600
|
+
// DERIVED here rather than copied off the state: `after` restores the
|
|
601
|
+
// stored flags only once the calling body has returned, so a summary built
|
|
602
|
+
// inside that body would report the tracks it just emptied as survivable.
|
|
603
|
+
crisisMode: inCrisis(state),
|
|
604
|
+
gameOver: isGameOver(state),
|
|
499
605
|
// NPCs go to the LLM whole (nothing withheld) — copied so a mutation of
|
|
500
606
|
// the summary can't reach live state.
|
|
501
607
|
npcs: state.npcs.filter((n) => n.status !== "deceased").map((n) => ({ ...n })),
|
|
@@ -584,15 +690,38 @@ function changeMomentum(game: GameState, amount: number, deltas: ConsequenceDelt
|
|
|
584
690
|
deltas.momentum += game.momentum - old;
|
|
585
691
|
}
|
|
586
692
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
693
|
+
/**
|
|
694
|
+
* The one rule that ends a story: both tracks empty.
|
|
695
|
+
*
|
|
696
|
+
* A PREDICATE beside the writer, because the same fact is wanted at two
|
|
697
|
+
* different moments. {@link gameSlot}'s `after` writes
|
|
698
|
+
* {@link GameState.gameOver} from it, which is what keeps the stored flag right
|
|
699
|
+
* for every mutating tool — including one that never thinks about it. A tool
|
|
700
|
+
* that REPORTS the flag in its own result reads the predicate directly, because
|
|
701
|
+
* `after` runs only once the body has already built that result: a copy taken
|
|
702
|
+
* from `state.gameOver` would be one mutation behind, and that copy is exactly
|
|
703
|
+
* what `sendFrom` turns into the `DOWNED` transition.
|
|
704
|
+
*/
|
|
705
|
+
export function isGameOver(game: FrozenGameState): boolean {
|
|
706
|
+
return game.health <= 0 && game.spirit <= 0;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Either track empty — what the client paints red. */
|
|
710
|
+
export function inCrisis(game: FrozenGameState): boolean {
|
|
711
|
+
return game.health <= 0 || game.spirit <= 0;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Restore the two derived flags from the tracks they are derived FROM.
|
|
716
|
+
*
|
|
717
|
+
* Called by nothing but {@link gameSlot}'s `after` — it used to be called by
|
|
718
|
+
* hand from three places, which is a rule every future mutating tool has to
|
|
719
|
+
* remember and one of them eventually will not. A stale `gameOver` is not a
|
|
720
|
+
* cosmetic miss: it is what the `DOWNED` transition reads.
|
|
721
|
+
*/
|
|
722
|
+
function updateCrisisFlags(game: GameState): void {
|
|
723
|
+
game.gameOver = isGameOver(game);
|
|
724
|
+
game.crisisMode = inCrisis(game);
|
|
596
725
|
}
|
|
597
726
|
|
|
598
727
|
export function applyConsequences(
|
|
@@ -685,8 +814,6 @@ export function applyConsequences(
|
|
|
685
814
|
}
|
|
686
815
|
}
|
|
687
816
|
|
|
688
|
-
updateCrisisFlags(game);
|
|
689
|
-
|
|
690
817
|
return { consequences, clockEvents, deltas };
|
|
691
818
|
}
|
|
692
819
|
|
|
@@ -713,7 +840,6 @@ export function revertConsequences(game: GameState, deltas: ConsequenceDeltas):
|
|
|
713
840
|
const clock = game.clocks.find((c) => c.id === deltas.clockId);
|
|
714
841
|
if (clock) clock.filled = Math.max(0, clock.filled - deltas.clockTicks);
|
|
715
842
|
}
|
|
716
|
-
updateCrisisFlags(game);
|
|
717
843
|
}
|
|
718
844
|
|
|
719
845
|
// ── Momentum Burn ────────────────────────────────────────────────────────────
|
|
@@ -63,7 +63,7 @@ MOMENTUM (Ironsworn):
|
|
|
63
63
|
- Starts at 2, range -6 to +10
|
|
64
64
|
- Weak Hit: +1. Strong Hit: +2 (or +3 with great effect)
|
|
65
65
|
- Miss: -2 (or -3 if desperate)
|
|
66
|
-
- Burn: the player can spend momentum to upgrade the most recent roll — call burn_momentum (no arguments; it validates against the stored roll). Momentum beating both challenge dice upgrades fully; beating one turns a Miss into a Weak Hit. Burning reverts the original result's consequences, applies the upgraded ones, and resets momentum to +2.
|
|
66
|
+
- Burn: the player can spend momentum to upgrade the most recent roll — call burn_momentum (no arguments; it validates against the stored roll). Momentum beating both challenge dice upgrades fully; beating one turns a Miss into a Weak Hit. Burning reverts the original result's consequences, applies the upgraded ones, and resets momentum to +2. The window is only open while a roll is standing: once you move the scene on with update_state, the roll is spent and burn_momentum refuses.
|
|
67
67
|
|
|
68
68
|
CHAOS FACTOR (Mythic GME):
|
|
69
69
|
- Range 3-9, starts at 5
|
|
@@ -86,6 +86,7 @@ Track up to 12 active NPCs.
|
|
|
86
86
|
CRISIS:
|
|
87
87
|
- Health or spirit at 0 = crisis mode. Both at 0 = game over.
|
|
88
88
|
- In crisis, every miss is more dangerous.
|
|
89
|
+
- Game over is final. Nothing more can be rolled or updated — narrate the ending. setup_character is the only way on, and it begins a new story.
|
|
89
90
|
|
|
90
91
|
KID MODE:
|
|
91
92
|
If kidMode is true: no explicit violence, no death, hopeful tone, age-appropriate content. Enemies are "defeated" not "killed". Think Studio Ghibli, Zelda.
|
|
@@ -97,7 +98,7 @@ CORRECTION SYSTEM:
|
|
|
97
98
|
If the player starts a message with ##, treat it as a correction to the previous turn. Acknowledge the correction and rewrite the scene.
|
|
98
99
|
|
|
99
100
|
FLOW:
|
|
100
|
-
1. Call check_state as your FIRST tool call every turn, before narrating or rolling. Read the returned values as ground truth. NEVER remember or guess stats from prior turns.
|
|
101
|
+
1. Call check_state as your FIRST tool call every turn, before narrating or rolling. Read the returned values as ground truth. NEVER remember or guess stats from prior turns. Its `state` and `instruction` say where the story is and what that expects — every other tool answers with the same pair, and a tool that is not available yet refuses and tells you what has to happen first. You do not have to remember any of this between turns.
|
|
101
102
|
2. Present situations with tension and choice. Two to three options, but accept anything.
|
|
102
103
|
3. For ANY risky action, you MUST call action_roll. NEVER narrate success or failure without rolling. NEVER reduce health, spirit, supply, or momentum yourself — action_roll does this through code. If you narrate damage without calling action_roll, the sidebar will be wrong and the game will break.
|
|
103
104
|
4. After location changes, new NPCs, or other world changes, call update_state. But NEVER manually set health, spirit, supply, or momentum in update_state unless the player is resting or trading — action_roll handles combat and risk.
|
|
@@ -4,16 +4,27 @@ import {
|
|
|
4
4
|
canBurnMomentum,
|
|
5
5
|
checkChaosInterrupt,
|
|
6
6
|
gameSlot,
|
|
7
|
+
inCrisis,
|
|
8
|
+
isGameOver,
|
|
7
9
|
MOVE_LABELS,
|
|
8
10
|
MOVES,
|
|
9
11
|
RESULT_LABELS,
|
|
10
12
|
rollAction,
|
|
13
|
+
storyFlow,
|
|
11
14
|
updateChaosFactor,
|
|
12
15
|
} from "../shared.ts";
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Gated on `playing`, which is what makes "never narrate a success without
|
|
19
|
+
* rolling" hold from the other side too: before `setup_character` there are no
|
|
20
|
+
* stats to roll against, and this used to roll 2d6 against nobody and apply
|
|
21
|
+
* consequences to a game that did not exist. After a game over it refuses
|
|
22
|
+
* outright — `gameOver` was a flag nothing acted on.
|
|
23
|
+
*/
|
|
24
|
+
export default storyFlow.tool({
|
|
15
25
|
description:
|
|
16
26
|
"Core mechanic. Roll 2d6 + stat (capped at 10) vs 2d10 challenge dice. Also applies consequences (health/spirit/supply/momentum changes, clock advancement) based on move type, position, and result. Call for ANY risky action. Pure conversation needs no roll.",
|
|
27
|
+
when: "playing",
|
|
17
28
|
inputSchema: z.object({
|
|
18
29
|
move: z.enum(MOVES).describe("Which move the player is making"),
|
|
19
30
|
stat: z.enum(["edge", "heart", "iron", "shadow", "wits"]).describe("Which stat to roll"),
|
|
@@ -24,73 +35,81 @@ export default gameSlot.updateTool({
|
|
|
24
35
|
purpose: z.string().max(300).describe("What the character is attempting"),
|
|
25
36
|
targetNpcId: z.string().max(32).describe("Target NPC id for social moves").optional(),
|
|
26
37
|
}),
|
|
27
|
-
execute(args,
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
execute: (args, ctx) =>
|
|
39
|
+
gameSlot.update(ctx, (state) => {
|
|
40
|
+
const statValue = state[args.stat];
|
|
41
|
+
const roll = rollAction(args.stat, statValue, args.move);
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// Apply consequences
|
|
44
|
+
const { consequences, clockEvents, deltas } = applyConsequences(
|
|
45
|
+
state,
|
|
46
|
+
roll,
|
|
47
|
+
args.position,
|
|
48
|
+
args.effect,
|
|
49
|
+
args.targetNpcId ?? null,
|
|
50
|
+
);
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
// Persist the roll (with the exact deltas applied) so burn_momentum can
|
|
53
|
+
// validate against it and revert it — the model never supplies dice.
|
|
54
|
+
state.lastRoll = {
|
|
55
|
+
...roll,
|
|
56
|
+
position: args.position,
|
|
57
|
+
effect: args.effect,
|
|
58
|
+
targetNpcId: args.targetNpcId ?? null,
|
|
59
|
+
deltas,
|
|
60
|
+
};
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
// Update chaos factor
|
|
63
|
+
updateChaosFactor(state, roll.result);
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
// Check for chaos interrupt
|
|
66
|
+
const interrupt = checkChaosInterrupt(state);
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
// Increment scene count
|
|
69
|
+
state.sceneCount++;
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
// Can burn momentum?
|
|
72
|
+
const burnTarget = canBurnMomentum(state, roll);
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
74
|
+
return {
|
|
75
|
+
purpose: args.purpose,
|
|
76
|
+
move: MOVE_LABELS[args.move] || args.move,
|
|
77
|
+
moveCode: args.move,
|
|
78
|
+
stat: args.stat,
|
|
79
|
+
statValue,
|
|
80
|
+
actionDice: [roll.d1, roll.d2],
|
|
81
|
+
challengeDice: [roll.c1, roll.c2],
|
|
82
|
+
actionScore: roll.actionScore,
|
|
83
|
+
result: RESULT_LABELS[roll.result],
|
|
84
|
+
resultCode: roll.result,
|
|
85
|
+
match: roll.match,
|
|
86
|
+
matchNote: roll.match
|
|
87
|
+
? roll.result === "STRONG_HIT" || roll.result === "WEAK_HIT"
|
|
88
|
+
? "Fateful roll. Both challenge dice match. An unexpected advantage or twist."
|
|
89
|
+
: "Fateful roll. Both challenge dice match. A dire and dramatic escalation."
|
|
90
|
+
: undefined,
|
|
91
|
+
position: args.position,
|
|
92
|
+
effect: args.effect,
|
|
93
|
+
consequences,
|
|
94
|
+
clockEvents,
|
|
95
|
+
chaosInterrupt: interrupt,
|
|
96
|
+
currentHealth: state.health,
|
|
97
|
+
currentSpirit: state.spirit,
|
|
98
|
+
currentSupply: state.supply,
|
|
99
|
+
currentMomentum: state.momentum,
|
|
100
|
+
chaosFactor: state.chaosFactor,
|
|
101
|
+
// Derived, not copied: `gameSlot`'s `after` restores the stored flags
|
|
102
|
+
// only once this body has returned, so `state.gameOver` here would be
|
|
103
|
+
// the value from before the roll that emptied the track.
|
|
104
|
+
crisisMode: inCrisis(state),
|
|
105
|
+
gameOver: isGameOver(state),
|
|
106
|
+
sceneCount: state.sceneCount,
|
|
107
|
+
canBurnMomentum: Boolean(burnTarget),
|
|
108
|
+
burnWouldYield: burnTarget ? RESULT_LABELS[burnTarget] : undefined,
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
// A roll that emptied both tracks ends the story; anything else leaves a
|
|
112
|
+
// standing roll that `burn_momentum` may still upgrade.
|
|
113
|
+
sendFrom: (result) =>
|
|
114
|
+
result.gameOver ? { type: "DOWNED" as const } : { type: "ROLLED" as const },
|
|
96
115
|
});
|
|
@@ -2,66 +2,101 @@ import {
|
|
|
2
2
|
applyConsequences,
|
|
3
3
|
canBurnMomentum,
|
|
4
4
|
gameSlot,
|
|
5
|
+
inCrisis,
|
|
6
|
+
isGameOver,
|
|
5
7
|
MOMENTUM_RESET,
|
|
6
8
|
RESULT_LABELS,
|
|
7
9
|
revertConsequences,
|
|
10
|
+
storyFlow,
|
|
8
11
|
} from "../shared.ts";
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Gated on `playing.rollResolved` — the state that means "a roll is standing".
|
|
15
|
+
*
|
|
16
|
+
* That gate REPLACES this body's opening `if (!state.lastRoll) return { error:
|
|
17
|
+
* "No recent action roll to upgrade. Roll first." }`, which was `when` written
|
|
18
|
+
* out by hand: "is there a roll to upgrade" is a question about where the game
|
|
19
|
+
* is, not about the data, and the flow's refusal names the position and quotes
|
|
20
|
+
* the state's instruction where that sentence could only assert.
|
|
21
|
+
*
|
|
22
|
+
* The two checks BELOW stay, because they really are about the data: whether
|
|
23
|
+
* the roll was already a Strong Hit, and whether the momentum beats the
|
|
24
|
+
* challenge dice. Those are the rules of the game; the gate is the shape of the
|
|
25
|
+
* turn.
|
|
26
|
+
*/
|
|
27
|
+
export default storyFlow.tool({
|
|
11
28
|
description:
|
|
12
29
|
"Burn momentum to upgrade the most recent action roll. Only valid when current momentum beats the roll's challenge dice (both dice for a full upgrade, one for Miss to Weak Hit). Reverts the original result's consequences, applies the upgraded result, and resets momentum to +2.",
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
when: "playing.rollResolved",
|
|
31
|
+
execute: (_args, ctx) =>
|
|
32
|
+
gameSlot.update(ctx, (state) => {
|
|
33
|
+
const last = state.lastRoll;
|
|
34
|
+
// Reachable only if the position and the campaign disagree — the flow says
|
|
35
|
+
// a roll is standing and nothing recorded one — which no code path
|
|
36
|
+
// produces: `action_roll` writes `lastRoll` and sends `ROLLED` in the same
|
|
37
|
+
// call. Reported rather than thrown, mid-game.
|
|
38
|
+
if (!last) return { error: "No recent action roll to upgrade. Roll first." };
|
|
39
|
+
if (last.result === "STRONG_HIT") {
|
|
40
|
+
return { error: "The last roll was already a Strong Hit. Nothing to upgrade." };
|
|
41
|
+
}
|
|
19
42
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
const upgrade = canBurnMomentum(state, last);
|
|
44
|
+
if (!upgrade) {
|
|
45
|
+
return {
|
|
46
|
+
error:
|
|
47
|
+
state.momentum <= 0
|
|
48
|
+
? "Momentum is 0 or negative. Cannot burn."
|
|
49
|
+
: "Momentum not high enough to improve the result.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
53
|
+
const previousMomentum = state.momentum;
|
|
54
|
+
const previousResult = last.result;
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
// Undo exactly what the original result applied, then apply the
|
|
57
|
+
// upgraded result's effects (bond/disposition/momentum gains).
|
|
58
|
+
revertConsequences(state, last.deltas);
|
|
59
|
+
const upgraded = { ...last, result: upgrade };
|
|
60
|
+
const { consequences, clockEvents } = applyConsequences(
|
|
61
|
+
state,
|
|
62
|
+
upgraded,
|
|
63
|
+
last.position,
|
|
64
|
+
last.effect,
|
|
65
|
+
last.targetNpcId,
|
|
66
|
+
);
|
|
44
67
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
68
|
+
// Burning always resets momentum, overriding any gain from the upgrade.
|
|
69
|
+
state.momentum = MOMENTUM_RESET;
|
|
70
|
+
state.lastRoll = null;
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
return {
|
|
73
|
+
burned: true,
|
|
74
|
+
previousMomentum,
|
|
75
|
+
newMomentum: MOMENTUM_RESET,
|
|
76
|
+
previousResult: RESULT_LABELS[previousResult],
|
|
77
|
+
newResult: RESULT_LABELS[upgrade],
|
|
78
|
+
newResultCode: upgrade,
|
|
79
|
+
challengeDice: [last.c1, last.c2],
|
|
80
|
+
revertedConsequences: true,
|
|
81
|
+
consequences,
|
|
82
|
+
clockEvents,
|
|
83
|
+
currentHealth: state.health,
|
|
84
|
+
currentSpirit: state.spirit,
|
|
85
|
+
currentSupply: state.supply,
|
|
86
|
+
// As `action_roll`: derived rather than copied, because `after` has not
|
|
87
|
+
// run yet when this object is built.
|
|
88
|
+
crisisMode: inCrisis(state),
|
|
89
|
+
gameOver: isGameOver(state),
|
|
90
|
+
};
|
|
91
|
+
}),
|
|
92
|
+
// A burn CONSUMES the standing roll (`lastRoll` is cleared), so the resting
|
|
93
|
+
// event is `SETTLED` rather than `ROLLED` — where `action_roll` leaves one
|
|
94
|
+
// standing, this one spends it. A burn that emptied the last track is as
|
|
95
|
+
// final as a roll that did.
|
|
96
|
+
//
|
|
97
|
+
// `result` is the SUCCESS type — `sendFrom` takes `Exclude<R, ToolFailure>`
|
|
98
|
+
// now, so the `"gameOver" in result` guard that stood in for the failure arm
|
|
99
|
+
// leaking into `R` is a plain property read.
|
|
100
|
+
sendFrom: (result) =>
|
|
101
|
+
result.gameOver ? { type: "DOWNED" as const } : { type: "SETTLED" as const },
|
|
67
102
|
});
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
import { gameSlot, stateSummary } from "../shared.ts";
|
|
1
|
+
import { gameSlot, stateSummary, storyFlow } from "../shared.ts";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `gameSlot.tool`, the READING half: the body is handed the value, so it needs
|
|
5
|
+
* neither a context annotation nor an opening `gameSlot.get(ctx)` — and what it
|
|
6
|
+
* is handed is deep-frozen, which is what makes "this tool only reads" a
|
|
7
|
+
* property of the declaration rather than a claim in a comment.
|
|
8
|
+
*
|
|
9
|
+
* **Legal in every state, which is why it is not a `storyFlow.tool`** — the
|
|
10
|
+
* prompt says to call it FIRST every turn, and that includes the turn before a
|
|
11
|
+
* character exists. It is also the one tool that reports the POSITION, the way
|
|
12
|
+
* `plan-and-execute`'s `plan_status` does: the position comes from the machine
|
|
13
|
+
* rather than from a second reading of the campaign's fields, so "there is no
|
|
14
|
+
* character yet" is the same fact here as the refusal `action_roll` would give.
|
|
15
|
+
*/
|
|
7
16
|
export default gameSlot.tool({
|
|
8
17
|
description:
|
|
9
|
-
"Returns the full current game state. Call this at the start of every turn, before narrating or rolling, and treat the returned values as ground truth — never guess or remember stats from previous turns.",
|
|
10
|
-
execute: (_args, game) =>
|
|
18
|
+
"Returns the full current game state, and where the story is. Call this at the start of every turn, before narrating or rolling, and treat the returned values as ground truth — never guess or remember stats from previous turns.",
|
|
19
|
+
execute: (_args, game, ctx) => ({
|
|
20
|
+
// The position SPREAD rather than renamed. `at`/`next`/`storyOver` said the
|
|
21
|
+
// same three things a gated tool's result already carries as `state`,
|
|
22
|
+
// `instruction` and `done` — so within one agent the model was reading its
|
|
23
|
+
// own position under two key sets, and the system prompt's claim that
|
|
24
|
+
// "every other tool answers with the same pair" was simply false.
|
|
25
|
+
...storyFlow.position(ctx),
|
|
26
|
+
...stateSummary(game),
|
|
27
|
+
}),
|
|
11
28
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from "@alexkroman1/aai";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { GameState } from "../shared.ts";
|
|
4
|
-
import { gameSlot, loadState, saveSlotKey, saveSlotParam } from "../shared.ts";
|
|
4
|
+
import { gameSlot, loadState, saveSlotKey, saveSlotParam, storyFlow } from "../shared.ts";
|
|
5
5
|
|
|
6
6
|
// Requires storage — `aai storage enable` (or DATABASE_URL in .env under
|
|
7
7
|
// `aai dev`); the rest of the game works without it.
|
|
@@ -12,8 +12,20 @@ export default tool({
|
|
|
12
12
|
const saved = await loadState<GameState>(ctx, saveSlotKey(args.slot));
|
|
13
13
|
if (!saved) return { error: "No save found." };
|
|
14
14
|
gameSlot.set(ctx, saved);
|
|
15
|
+
|
|
16
|
+
// The flow has to be restored alongside the campaign, or a loaded game
|
|
17
|
+
// would still be sitting in `awaitingSetup` with every roll tool refusing.
|
|
18
|
+
// A save is written from a settled scene, so it resumes at `awaitingRoll`
|
|
19
|
+
// rather than with a standing roll — `lastRoll` may be set, but a burn
|
|
20
|
+
// window that survived a save and a reload is not one a player is still in.
|
|
21
|
+
storyFlow.reset(ctx);
|
|
22
|
+
let at = storyFlow.send(ctx, { type: "SETUP" });
|
|
23
|
+
if (saved.gameOver) at = storyFlow.send(ctx, { type: "DOWNED" });
|
|
24
|
+
|
|
15
25
|
return {
|
|
16
26
|
loaded: true,
|
|
27
|
+
// The position spread verbatim — the same keys every gated tool reports.
|
|
28
|
+
...at,
|
|
17
29
|
playerName: saved.playerName,
|
|
18
30
|
characterConcept: saved.characterConcept,
|
|
19
31
|
settingGenre: saved.settingGenre,
|