@maka/maka-cli 5.123.0 → 5.123.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.
Files changed (254) hide show
  1. package/README.md +64 -64
  2. package/bundle/typescript/package.json +120 -120
  3. package/bundle/typescript/src/README.md +64 -64
  4. package/bundle/typescript/src/commands/aws/aws.command.js +2 -2
  5. package/bundle/typescript/src/commands/aws/ci.sub.cmd.js +5 -5
  6. package/bundle/typescript/src/commands/aws/ckp.sub.cmd.js +12 -12
  7. package/bundle/typescript/src/commands/aws/csg.sub.cmd.js +3 -3
  8. package/bundle/typescript/src/commands/aws/di.sub.cmd.js +8 -8
  9. package/bundle/typescript/src/commands/aws/rkp.sub.cmd.js +5 -5
  10. package/bundle/typescript/src/commands/aws/sth.sub.cmd.js +3 -3
  11. package/bundle/typescript/src/commands/aws/ti.sub.cmd.js +5 -5
  12. package/bundle/typescript/src/commands/build/build.js +16 -16
  13. package/bundle/typescript/src/commands/build/docker.sub.cmd.js +9 -9
  14. package/bundle/typescript/src/commands/create.js +36 -36
  15. package/bundle/typescript/src/commands/debug.js +4 -4
  16. package/bundle/typescript/src/commands/deploy/deploy.command.js +2 -2
  17. package/bundle/typescript/src/commands/deploy/galaxy.sub.cmd.js +2 -2
  18. package/bundle/typescript/src/commands/deploy/github.sub.cmd.js +8 -8
  19. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js +2 -2
  20. package/bundle/typescript/src/commands/env/env.command.js +8 -8
  21. package/bundle/typescript/src/commands/env/set.sub.cmd.js +11 -11
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -9
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/source.js +7 -7
  25. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +443 -443
  26. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +6 -6
  27. package/bundle/typescript/src/commands/game/sideQuest/game.js +1 -1
  28. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +206 -206
  29. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +9 -9
  30. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +556 -556
  31. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +1447 -1447
  32. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +14 -14
  33. package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.js +2 -2
  34. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +2 -2
  35. package/bundle/typescript/src/commands/generate.js +2 -2
  36. package/bundle/typescript/src/commands/lint.js +10 -10
  37. package/bundle/typescript/src/commands/maka.cmd.js +26 -26
  38. package/bundle/typescript/src/commands/murder.js +14 -14
  39. package/bundle/typescript/src/commands/proxy.js +13 -13
  40. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js +6 -6
  41. package/bundle/typescript/src/commands/run/docker.sub.cmd.js +11 -11
  42. package/bundle/typescript/src/commands/run/run.command.js +24 -24
  43. package/bundle/typescript/src/commands/set/global.sub.cmd.js +8 -8
  44. package/bundle/typescript/src/commands/set/local.sub.cmd.js +2 -2
  45. package/bundle/typescript/src/commands/set/set.command.js +3 -3
  46. package/bundle/typescript/src/commands/testCmd/test.command.js +2 -2
  47. package/bundle/typescript/src/exec.js +0 -0
  48. package/bundle/typescript/src/generators/api.js +11 -11
  49. package/bundle/typescript/src/generators/collection.js +6 -6
  50. package/bundle/typescript/src/generators/hook.js +10 -10
  51. package/bundle/typescript/src/generators/kits/auth/auth.generator.js +2 -2
  52. package/bundle/typescript/src/generators/kits/auth/auth0-meteor.auth.gen.js +612 -612
  53. package/bundle/typescript/src/generators/kits/auth/basic-meteor.auth.gen.js +48 -48
  54. package/bundle/typescript/src/generators/kits/auth/provider.auth.gen.js +12 -12
  55. package/bundle/typescript/src/generators/kits/esri/esri.generator.js +3 -3
  56. package/bundle/typescript/src/generators/kits/esri/map.esri.gen.js +7 -7
  57. package/bundle/typescript/src/generators/kits/frames/frames.generator.js +16 -16
  58. package/bundle/typescript/src/generators/kits/mui/auth.mui.gen.js +2 -2
  59. package/bundle/typescript/src/generators/kits/mui/auth0.mui.gen.js +5 -5
  60. package/bundle/typescript/src/generators/kits/mui/frame.mui.gen.js +4 -4
  61. package/bundle/typescript/src/generators/kits/mui/mui.generator.js +22 -22
  62. package/bundle/typescript/src/generators/services/translation.js +5 -5
  63. package/bundle/typescript/src/templates/aws/cloudformation/create-ec2-instance.template +14 -14
  64. package/bundle/typescript/src/templates/meteor/api/api.js +20 -20
  65. package/bundle/typescript/src/templates/meteor/api/api.js.ts +20 -20
  66. package/bundle/typescript/src/templates/meteor/api/apollo-gateway.js +50 -50
  67. package/bundle/typescript/src/templates/meteor/api/apollo-gateway.js.ts +55 -55
  68. package/bundle/typescript/src/templates/meteor/api/graphql-api.js +63 -63
  69. package/bundle/typescript/src/templates/meteor/api/graphql-api.js.ts +69 -69
  70. package/bundle/typescript/src/templates/meteor/api/methods.js +39 -39
  71. package/bundle/typescript/src/templates/meteor/api/methods.js.ts +40 -40
  72. package/bundle/typescript/src/templates/meteor/api/register-api.js +3 -3
  73. package/bundle/typescript/src/templates/meteor/api/register-api.js.ts +3 -3
  74. package/bundle/typescript/src/templates/meteor/api/resolvers.js +9 -9
  75. package/bundle/typescript/src/templates/meteor/api/resolvers.js.ts +9 -9
  76. package/bundle/typescript/src/templates/meteor/api/typeDefs.js +6 -6
  77. package/bundle/typescript/src/templates/meteor/api/typeDefs.js.ts +6 -6
  78. package/bundle/typescript/src/templates/meteor/app/client/body.html +3 -3
  79. package/bundle/typescript/src/templates/meteor/app/client/head.html +12 -12
  80. package/bundle/typescript/src/templates/meteor/app/client/main.js +15 -15
  81. package/bundle/typescript/src/templates/meteor/app/client/main.js.jsx +15 -15
  82. package/bundle/typescript/src/templates/meteor/app/client/main.js.ts +17 -17
  83. package/bundle/typescript/src/templates/meteor/app/client/main.js.tsx +17 -17
  84. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js +20 -20
  85. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.jsx +107 -107
  86. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.ts +20 -20
  87. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.tsx +108 -108
  88. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js +39 -39
  89. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.jsx +33 -33
  90. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.ts +24 -24
  91. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.tsx +33 -33
  92. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js +9 -9
  93. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.jsx +9 -9
  94. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.ts +9 -9
  95. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.tsx +9 -9
  96. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js +1 -1
  97. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.jsx +20 -20
  98. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.ts +1 -1
  99. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.tsx +20 -20
  100. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/routes.js.jsx +28 -28
  101. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/routes.js.tsx +28 -28
  102. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js +1 -1
  103. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js.jsx +95 -95
  104. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js.tsx +98 -98
  105. package/bundle/typescript/src/templates/meteor/app/imports/ui/test-helpers.js +24 -24
  106. package/bundle/typescript/src/templates/meteor/app/imports/ui/test-helpers.js.ts +24 -24
  107. package/bundle/typescript/src/templates/meteor/app/lib/main.js +3 -3
  108. package/bundle/typescript/src/templates/meteor/app/lib/main.js.jsx +3 -3
  109. package/bundle/typescript/src/templates/meteor/app/lib/main.js.ts +3 -3
  110. package/bundle/typescript/src/templates/meteor/app/lib/main.js.tsx +3 -3
  111. package/bundle/typescript/src/templates/meteor/app/maka.d.ts +533 -533
  112. package/bundle/typescript/src/templates/meteor/app/meteor.d.ts +181 -181
  113. package/bundle/typescript/src/templates/meteor/app/public/manifest.json +27 -27
  114. package/bundle/typescript/src/templates/meteor/app/public/sw.js +30 -30
  115. package/bundle/typescript/src/templates/meteor/app/public/sw.js.jsx +30 -30
  116. package/bundle/typescript/src/templates/meteor/app/public/sw.js.ts +30 -30
  117. package/bundle/typescript/src/templates/meteor/app/public/sw.js.tsx +30 -30
  118. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js +179 -179
  119. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.jsx +179 -179
  120. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.ts +179 -179
  121. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.tsx +179 -179
  122. package/bundle/typescript/src/templates/meteor/app/server/main.js +13 -13
  123. package/bundle/typescript/src/templates/meteor/app/server/main.js.jsx +13 -13
  124. package/bundle/typescript/src/templates/meteor/app/server/main.js.ts +14 -14
  125. package/bundle/typescript/src/templates/meteor/app/server/main.js.tsx +14 -14
  126. package/bundle/typescript/src/templates/meteor/app/tsconfig.json +42 -42
  127. package/bundle/typescript/src/templates/meteor/collection/collection.js +51 -51
  128. package/bundle/typescript/src/templates/meteor/collection/collection.js.ts +55 -55
  129. package/bundle/typescript/src/templates/meteor/collection/tests.js +27 -27
  130. package/bundle/typescript/src/templates/meteor/collection/tests.js.ts +27 -27
  131. package/bundle/typescript/src/templates/meteor/config/deploy.sh.template +101 -101
  132. package/bundle/typescript/src/templates/meteor/config/docker-compose.yaml +17 -17
  133. package/bundle/typescript/src/templates/meteor/config/nginx.conf.template +31 -31
  134. package/bundle/typescript/src/templates/meteor/config/pm2.config.js +27 -27
  135. package/bundle/typescript/src/templates/meteor/config/process.env +5 -5
  136. package/bundle/typescript/src/templates/meteor/config/settings.json +11 -11
  137. package/bundle/typescript/src/templates/meteor/config/ssh.json +6 -6
  138. package/bundle/typescript/src/templates/meteor/dbc/config.js +70 -70
  139. package/bundle/typescript/src/templates/meteor/dbc/config.js.ts +70 -70
  140. package/bundle/typescript/src/templates/meteor/dbc/conn.js +37 -37
  141. package/bundle/typescript/src/templates/meteor/dbc/conn.js.ts +37 -37
  142. package/bundle/typescript/src/templates/meteor/dbc/register-dbc.js +3 -3
  143. package/bundle/typescript/src/templates/meteor/dbc/register-dbc.js.ts +3 -3
  144. package/bundle/typescript/src/templates/meteor/github/workflows/deploy.yaml +150 -150
  145. package/bundle/typescript/src/templates/meteor/method/method.js +48 -48
  146. package/bundle/typescript/src/templates/meteor/method/method.js.ts +48 -48
  147. package/bundle/typescript/src/templates/meteor/package/package.js +37 -37
  148. package/bundle/typescript/src/templates/meteor/package/package.js.jsx +37 -37
  149. package/bundle/typescript/src/templates/meteor/package/package.js.ts +36 -36
  150. package/bundle/typescript/src/templates/meteor/package/package.js.tsx +37 -37
  151. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js +5 -5
  152. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.jsx +5 -5
  153. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.ts +5 -5
  154. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.tsx +5 -5
  155. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js +5 -5
  156. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.jsx +5 -5
  157. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.ts +5 -5
  158. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.tsx +5 -5
  159. package/bundle/typescript/src/templates/meteor/project/.maka/config.json +29 -29
  160. package/bundle/typescript/src/templates/meteor/project/README.md +33 -33
  161. package/bundle/typescript/src/templates/meteor/project/build/README +4 -4
  162. package/bundle/typescript/src/templates/meteor/project/gitignore +10 -10
  163. package/bundle/typescript/src/templates/meteor/project/package.json +29 -29
  164. package/bundle/typescript/src/templates/meteor/publish/publish.js +7 -7
  165. package/bundle/typescript/src/templates/meteor/publish/publish.js.ts +7 -7
  166. package/bundle/typescript/src/templates/meteor/service/logger/logger.js +27 -27
  167. package/bundle/typescript/src/templates/meteor/service/logger/logger.js.ts +27 -27
  168. package/bundle/typescript/src/templates/meteor/stylesheet/stylesheet.css +5 -5
  169. package/bundle/typescript/src/templates/meteor/stylesheet/stylesheet.css.less +5 -5
  170. package/bundle/typescript/src/templates/meteor/template/template-tests.js +1 -1
  171. package/bundle/typescript/src/templates/meteor/template/template-tests.js.jsx +55 -55
  172. package/bundle/typescript/src/templates/meteor/template/template-tests.js.ts +1 -1
  173. package/bundle/typescript/src/templates/meteor/template/template-tests.js.tsx +55 -55
  174. package/bundle/typescript/src/templates/meteor/template/template.css +3 -3
  175. package/bundle/typescript/src/templates/meteor/template/template.css.less +3 -3
  176. package/bundle/typescript/src/templates/meteor/template/template.css.scss +3 -3
  177. package/bundle/typescript/src/templates/meteor/template/template.html +5 -5
  178. package/bundle/typescript/src/templates/meteor/template/template.js +20 -20
  179. package/bundle/typescript/src/templates/meteor/template/template.js.jsx +77 -77
  180. package/bundle/typescript/src/templates/meteor/template/template.js.ts +1 -1
  181. package/bundle/typescript/src/templates/meteor/template/template.js.tsx +89 -89
  182. package/bundle/typescript/src/templates/react/component/component.css +6 -6
  183. package/bundle/typescript/src/templates/react/component/component.css.less +6 -6
  184. package/bundle/typescript/src/templates/react/component/component.css.scss +3 -3
  185. package/bundle/typescript/src/templates/react/component/component.js.jsx +59 -59
  186. package/bundle/typescript/src/templates/react/component/component.js.tsx +65 -65
  187. package/bundle/typescript/src/templates/react/hook/hook.css +3 -3
  188. package/bundle/typescript/src/templates/react/hook/hook.css.scss +3 -3
  189. package/bundle/typescript/src/templates/react/hook/hook.js.jsx +19 -19
  190. package/bundle/typescript/src/templates/react/hook/hook.js.tsx +19 -19
  191. package/bundle/typescript/src/templates/react/kit/auth/provider/authentication-provider.js.jsx +306 -306
  192. package/bundle/typescript/src/templates/react/kit/auth/provider/authentication-provider.js.tsx +350 -350
  193. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.jsx +178 -178
  194. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.tsx +178 -178
  195. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.jsx +59 -59
  196. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.tsx +67 -67
  197. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.jsx +273 -273
  198. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.tsx +275 -275
  199. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.jsx +182 -182
  200. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.tsx +199 -199
  201. package/bundle/typescript/src/templates/react/kit/auth/service/meteor-auth-service.js.jsx +138 -138
  202. package/bundle/typescript/src/templates/react/kit/auth/service/meteor-auth-service.js.tsx +153 -153
  203. package/bundle/typescript/src/templates/react/kit/esri/map/basic-map.js.jsx +29 -29
  204. package/bundle/typescript/src/templates/react/kit/esri/map/basic-map.js.tsx +32 -32
  205. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.css +63 -63
  206. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.jsx +153 -153
  207. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.tsx +164 -164
  208. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.css +105 -105
  209. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.jsx +158 -158
  210. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.tsx +166 -166
  211. package/bundle/typescript/src/templates/react/kit/mui/auth/forgot-password/basic-forgot-password-page.js.jsx +34 -34
  212. package/bundle/typescript/src/templates/react/kit/mui/auth/forgot-password/basic-forgot-password-page.js.tsx +33 -33
  213. package/bundle/typescript/src/templates/react/kit/mui/auth/login/basic-login-page.js.jsx +53 -53
  214. package/bundle/typescript/src/templates/react/kit/mui/auth/login/basic-login-page.js.tsx +53 -53
  215. package/bundle/typescript/src/templates/react/kit/mui/auth/logout/basic-logout-page.js.jsx +25 -25
  216. package/bundle/typescript/src/templates/react/kit/mui/auth/logout/basic-logout-page.js.tsx +25 -25
  217. package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.jsx +41 -41
  218. package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.tsx +34 -34
  219. package/bundle/typescript/src/templates/react/kit/mui/auth/register/basic-register-page.js.jsx +33 -33
  220. package/bundle/typescript/src/templates/react/kit/mui/auth/register/basic-register-page.js.tsx +33 -33
  221. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.jsx +71 -71
  222. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.tsx +71 -71
  223. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.jsx +123 -123
  224. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.tsx +123 -123
  225. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.jsx +56 -56
  226. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.tsx +74 -74
  227. package/bundle/typescript/src/templates/react/kit/mui/frame/app-bars/basic-app-bar.js.jsx +43 -43
  228. package/bundle/typescript/src/templates/react/kit/mui/frame/app-bars/basic-app-bar.js.tsx +43 -43
  229. package/bundle/typescript/src/templates/react/kit/mui/frame/drawers/basic-drawer.js.jsx +121 -121
  230. package/bundle/typescript/src/templates/react/kit/mui/frame/drawers/basic-drawer.js.tsx +117 -117
  231. package/bundle/typescript/src/templates/react/kit/mui/frame/footers/basic-sticky-footer.js.jsx +41 -41
  232. package/bundle/typescript/src/templates/react/kit/mui/frame/footers/basic-sticky-footer.js.tsx +41 -41
  233. package/bundle/typescript/src/templates/react/route/route.js +8 -8
  234. package/bundle/typescript/src/templates/react/route/route.js.jsx +5 -5
  235. package/bundle/typescript/src/templates/react/route/route.js.ts +8 -8
  236. package/bundle/typescript/src/templates/react/route/route.js.tsx +5 -5
  237. package/bundle/typescript/src/templates/react/test/test.js +1 -1
  238. package/bundle/typescript/src/templates/react/test/test.js.jsx +40 -40
  239. package/bundle/typescript/src/templates/react/test/test.js.ts +1 -1
  240. package/bundle/typescript/src/templates/react/test/test.js.tsx +40 -40
  241. package/bundle/typescript/src/templates/service/docker/Dockerfile +54 -54
  242. package/bundle/typescript/src/templates/service/eslint/babel.config.js +16 -16
  243. package/bundle/typescript/src/templates/service/eslint/config.eslint.js +69 -69
  244. package/bundle/typescript/src/templates/service/i18n/i18n.js +33 -33
  245. package/bundle/typescript/src/templates/service/i18n/i18n.js.ts +33 -33
  246. package/bundle/typescript/src/templates/service/i18n/locales.js +3 -3
  247. package/bundle/typescript/src/templates/service/i18n/locales.js.ts +3 -3
  248. package/bundle/typescript/src/templates/service/openssl/openssl.conf +22 -22
  249. package/bundle/typescript/src/templates/service/test/jest/jest.config.js +24 -24
  250. package/bundle/typescript/src/templates/service/test/jest/jest.setup.js +7 -7
  251. package/bundle/typescript/src/tools/mpi/sdk-runtime.d.ts +1 -0
  252. package/bundle/typescript/src/tools/mpi/sdk-runtime.js +29 -0
  253. package/bundle/typescript/src/tools/mpi/sdk-runtime.js.map +1 -1
  254. package/package.json +120 -120
@@ -63,8 +63,8 @@ export function isFixerName(name) {
63
63
  export function wardBrief(player) {
64
64
  return partyCanDispel(player)
65
65
  ? `- WARDS: this runner can dispel, so a "ward" device is fair game.`
66
- : `- WARDS: NOBODY at this table can dispel. Do NOT write a "ward" device -- there is no
67
- findable answer to a ward, so a mundane team simply cannot pass one. Use a "lock" or a
66
+ : `- WARDS: NOBODY at this table can dispel. Do NOT write a "ward" device -- there is no
67
+ findable answer to a ward, so a mundane team simply cannot pass one. Use a "lock" or a
68
68
  "panel"; both have a way through that anyone can find.`;
69
69
  }
70
70
  export function buildPlayerDossier(player) {
@@ -139,34 +139,34 @@ export function buildPlayerDossier(player) {
139
139
  const armor = a.getArmorValue();
140
140
  if (armor > 0)
141
141
  gearBits.push(`armor +${armor} soak`);
142
- return `
143
- THE RUNNER (build this job FOR this specific character):
144
- - Name: ${a.name}${a.archetypeName ? ` -- archetype: ${a.archetypeName}` : ''}${a.gender ? ` -- ${a.gender === 'male' ? 'a man (he/him)' : 'a woman (she/her)'}` : ' -- gender unstated (use they/them, never guess)'}
145
- - Attributes: ${attrs}
146
- ${topSkills ? `- Top skills: ${topSkills}` : ''}
147
- - Combat calibration: attack pool ~${attack}d6, defense pool ~${defense}d6${gearBits.length > 0 ? ` (${gearBits.join(', ')})` : ''}
148
- ${caps.map(c => `- ${c}`).join('\n')}
149
-
150
- Tailoring rules:
151
- - Include at least one route, subplot, or payoff that rewards THIS runner's toolkit (matrix
152
- content and hasNode rooms for a decker/technomancer; astral angles or Awakened opposition
153
- for a mage; melee-shaped trouble for an adept; social leverage and people worth talking round
154
- for a talker; recon value for a drone owner). Pick what fits this character and this
155
- story -- don't bolt on every plane at once.
156
- - Never gate FINISHING the run behind a capability they lack: no matrix-plane winCondition
157
- item unless they can already jack in (deck or resonance) or a findable Cyberdeck is placed
158
- in a reachable room; no ward/"spell"-bypass-only route for someone who can't cast.
159
- - Don't hand them loot they can't use as a puzzle rewardItem or the winCondition rewardItem
160
- (spellcasting gear for an adept, a second cyberdeck or drone for someone who already has
161
- one). Useless-to-them gear may still exist as ordinary world dressing or valuables.
162
- - The runner ALREADY OWNS everything the dossier lists -- never duplicate any of it (their
163
- deck, drone, weapon, focus, commlink) in the "starting"-role items. Round those out with
164
- complementary basics instead: ammunition, a medkit, armor, tools -- or fewer items.
165
- - Scale opposition to the calibration pools above: rank-and-file threats should roll a few
166
- dice below the runner's pools, and a centerpiece enforcer can match or slightly exceed
167
- them. Don't send soft civilians at a combat monster, or elite security at a runner built
168
- for talking their way through.
169
- ${wardBrief(player)}
142
+ return `
143
+ THE RUNNER (build this job FOR this specific character):
144
+ - Name: ${a.name}${a.archetypeName ? ` -- archetype: ${a.archetypeName}` : ''}${a.gender ? ` -- ${a.gender === 'male' ? 'a man (he/him)' : 'a woman (she/her)'}` : ' -- gender unstated (use they/them, never guess)'}
145
+ - Attributes: ${attrs}
146
+ ${topSkills ? `- Top skills: ${topSkills}` : ''}
147
+ - Combat calibration: attack pool ~${attack}d6, defense pool ~${defense}d6${gearBits.length > 0 ? ` (${gearBits.join(', ')})` : ''}
148
+ ${caps.map(c => `- ${c}`).join('\n')}
149
+
150
+ Tailoring rules:
151
+ - Include at least one route, subplot, or payoff that rewards THIS runner's toolkit (matrix
152
+ content and hasNode rooms for a decker/technomancer; astral angles or Awakened opposition
153
+ for a mage; melee-shaped trouble for an adept; social leverage and people worth talking round
154
+ for a talker; recon value for a drone owner). Pick what fits this character and this
155
+ story -- don't bolt on every plane at once.
156
+ - Never gate FINISHING the run behind a capability they lack: no matrix-plane winCondition
157
+ item unless they can already jack in (deck or resonance) or a findable Cyberdeck is placed
158
+ in a reachable room; no ward/"spell"-bypass-only route for someone who can't cast.
159
+ - Don't hand them loot they can't use as a puzzle rewardItem or the winCondition rewardItem
160
+ (spellcasting gear for an adept, a second cyberdeck or drone for someone who already has
161
+ one). Useless-to-them gear may still exist as ordinary world dressing or valuables.
162
+ - The runner ALREADY OWNS everything the dossier lists -- never duplicate any of it (their
163
+ deck, drone, weapon, focus, commlink) in the "starting"-role items. Round those out with
164
+ complementary basics instead: ammunition, a medkit, armor, tools -- or fewer items.
165
+ - Scale opposition to the calibration pools above: rank-and-file threats should roll a few
166
+ dice below the runner's pools, and a centerpiece enforcer can match or slightly exceed
167
+ them. Don't send soft civilians at a combat monster, or elite security at a runner built
168
+ for talking their way through.
169
+ ${wardBrief(player)}
170
170
  `.trim();
171
171
  }
172
172
  // ============================ Prompt builders ============================
@@ -200,11 +200,11 @@ function buildTeamBlock(crew) {
200
200
  .map(([s, r]) => `${s} ${r}`).join(', ');
201
201
  return `- ${m.name} (${m.archetypeName ?? 'street contact'}): body ${c.body ?? 3}, agility ${c.agility ?? 3}, logic ${c.logic ?? 3}${(c.magic ?? 0) > 0 ? `, magic ${c.magic}` : ''}${topSkills ? ` -- ${topSkills}` : ''}`;
202
202
  }).join('\n');
203
- return `
204
- THE TEAM: the runner does NOT work alone -- their crew rides along on this job:
205
- ${rows}
206
- The opposition must credibly threaten a team of ${crew.length + 1} runners: scale guard
207
- counts and quality up accordingly (one extra capable defender per crew member is a good
203
+ return `
204
+ THE TEAM: the runner does NOT work alone -- their crew rides along on this job:
205
+ ${rows}
206
+ The opposition must credibly threaten a team of ${crew.length + 1} runners: scale guard
207
+ counts and quality up accordingly (one extra capable defender per crew member is a good
208
208
  baseline), and let the fiction acknowledge a crew, not a loner.`;
209
209
  }
210
210
  export function buildSkeletonPrompt(tier, player, previousError, rumor, crew, canonBlock, runType = 'standard') {
@@ -254,409 +254,409 @@ export function buildSkeletonPrompt(tier, player, previousError, rumor, crew, ca
254
254
  // fixer" possible at all and why the objective below can be a `give`.
255
255
  // The two halves have to agree about WHERE the file is, so this block
256
256
  // says host-or-offline-server in the same vocabulary room.ts uses.
257
- const dataRunBlock = runType !== 'data' ? '' : `
258
- THIS JOB IS A DATA RUN -- a quiet one. The runner is being paid to STEAL A FILE, not to
259
- take ground. Build it accordingly:
260
- - The winCondition item is PAYDATA: a "matrix"-plane item, category Document, sitting
261
- either INSIDE a hasNode host, or on an OFFLINE SERVER device in a room (a box with no
262
- Matrix presence, whose files open only to a runner whose BODY is in that room). Pick one
263
- and commit to it -- the offline server is the harder, more interesting shape when the
264
- fiction wants a vault you must walk into.
265
- - THE OPPOSITION IS ICE, not muscle. Every NPC besides the client is "matrix"-plane and
266
- stationed in the host. No guards patrolling meat corridors, no gunfight in the lobby: a
267
- data run that ends in a firefight is a smash-and-grab wearing the wrong name. A site can
268
- still be WATCHED (cameras) -- surveillance is pressure without being a gun.
269
- - Keep it SHORT and tight. One site, one host, one file.
270
- - It pays LESS than a full run of the same tier -- roughly half. The runner is trading pay
271
- for a job that never puts a gun in their face, and the fixer prices it that way. Say so
272
- in the client's own terms if it comes up at the meet.
273
- - The delivery still matters: the file comes out of the vault written to a chip, and the
274
- chip is a physical object the runner carries and hands over. A "give" objective is the
275
- natural shape.
257
+ const dataRunBlock = runType !== 'data' ? '' : `
258
+ THIS JOB IS A DATA RUN -- a quiet one. The runner is being paid to STEAL A FILE, not to
259
+ take ground. Build it accordingly:
260
+ - The winCondition item is PAYDATA: a "matrix"-plane item, category Document, sitting
261
+ either INSIDE a hasNode host, or on an OFFLINE SERVER device in a room (a box with no
262
+ Matrix presence, whose files open only to a runner whose BODY is in that room). Pick one
263
+ and commit to it -- the offline server is the harder, more interesting shape when the
264
+ fiction wants a vault you must walk into.
265
+ - THE OPPOSITION IS ICE, not muscle. Every NPC besides the client is "matrix"-plane and
266
+ stationed in the host. No guards patrolling meat corridors, no gunfight in the lobby: a
267
+ data run that ends in a firefight is a smash-and-grab wearing the wrong name. A site can
268
+ still be WATCHED (cameras) -- surveillance is pressure without being a gun.
269
+ - Keep it SHORT and tight. One site, one host, one file.
270
+ - It pays LESS than a full run of the same tier -- roughly half. The runner is trading pay
271
+ for a job that never puts a gun in their face, and the fixer prices it that way. Say so
272
+ in the client's own terms if it comes up at the meet.
273
+ - The delivery still matters: the file comes out of the vault written to a chip, and the
274
+ chip is a physical object the runner carries and hands over. A "give" objective is the
275
+ natural shape.
276
276
  `;
277
277
  // The hub's active street rumor becomes the job's backdrop -- gossip
278
278
  // the runner heard at the noodle stand pays off as foreshadowing when
279
279
  // the next job turns out to be tangled in exactly that (see
280
280
  // Game.advanceRumor).
281
281
  const rumorBlock = rumor
282
- ? `\nSTREET RUMOR the runner has been hearing around their home district: "${rumor}"
283
- Let this job CONNECT to the rumor -- as its cause, its consequence, or the truth behind it.
284
- The connection should be discoverable in play (the client's briefing, an NPC's talk, a
282
+ ? `\nSTREET RUMOR the runner has been hearing around their home district: "${rumor}"
283
+ Let this job CONNECT to the rumor -- as its cause, its consequence, or the truth behind it.
284
+ The connection should be discoverable in play (the client's briefing, an NPC's talk, a
285
285
  document), not just implied by the title.`
286
286
  : '';
287
- return `
288
- You are generating the STRUCTURE (skeleton) of a self-contained Shadowrun-flavored text
289
- adventure "run". This is job #${tier} in an ongoing campaign -- scale stakes accordingly.
290
- Prose comes later in separate passes: here, every "concept" is ONE tight sentence of intent,
291
- and everything else is structure. Get the STRUCTURE exactly right -- every name is a
292
- cross-reference other passes and the game engine will rely on verbatim.
293
- ${rumorBlock}
294
- ${dataRunBlock}
295
- ${canonBlock ? `\n${canonBlock}\n` : ''}
296
- ${dossier ? `\n${dossier}\n` : ''}
297
- ${JSON_ONLY}
298
-
299
- {
300
- "name": string, // the run's title
301
- "story": string, // 1-2 SHORT teaser sentences shown before play -- name the shape of the
302
- // trouble, never the objective, target names, locations, or passphrases
303
- // (those are learned at the meet with the client).
304
- "completionMessage": string, // 1-2 sentences shown when the job completes
305
- "rooms": [{
306
- "name": string, "concept": string, "roomType"?: string,
307
- "outdoors": boolean, // TRUE only if OPEN TO THE SKY. A courtyard, a street, a
308
- // rooftop, a dock: true. Anything with a roof -- a market
309
- // HALL, a parking garage, a docking bay, a courtROOM -- is
310
- // false, however outdoor its name sounds. Say it for every
311
- // room; the engine cannot tell from the name and a wrong
312
- // guess sends a taxi inside a building.
313
- "isStartRoom"?: boolean, // EXACTLY ONE room
314
- "reachable"?: boolean, // false ONLY for a remote contact's room (rare -- a voice on the
315
- // other end of a call, never anyone the player must meet)
316
- "owner"?: string, // an NPC's name, if it's their room. USE THIS for any territorial or
317
- // guarded space (a gang cache, a fixer's office, a shop): the room's
318
- // goods become THEIRS, and the engine makes them contest theft --
319
- // an unowned prize in a guarded vault walks out unchallenged.
320
- "hasNode"?: boolean, // a live Matrix host standing over this room: its own virtual space,
321
- // ENTERED by a persona that holds a mark on it. Hacking it releases
322
- // the room's maglocks AND unseals its matrix-plane files, which live
323
- // INSIDE the host -- from the outside grid it is one sealed icon.
324
- // A HOST IS ORDINARY, NOT CORPORATE. "Your local Stuffer Shack has
325
- // a host icon" (p.219), and the core book publishes that convenience
326
- // store as a worked sample host (p.245-246) -- so a pawnshop, a bar,
327
- // a clinic or a corner store plausibly has one, rated 3-4 as "low-end
328
- // commercial, private business" (p.247). Withholding hosts from
329
- // everything but corp/tech sites is what made an ordinary storefront
330
- // read "thin grid" on the map when canon says otherwise.
331
- // SET IT ON PREMISES, NOT ON EVERY ROOM: the businesses and secure
332
- // sites a scene contains, not each corridor, alley, stairwell or
333
- // room inside one building -- a site is covered by its host, and
334
- // interior rooms of the same premises share it rather than each
335
- // sprouting their own. Give every host a hostRating chosen for the
336
- // place it actually is; a scene where every rating is 3 is as wrong
337
- // as one where nothing is a host.
338
- // Ice NPCs and matrix-plane items belong ONLY in hasNode rooms, and
339
- // any hasNode room holding matrix items MUST have an ice NPC
340
- // stationed in it -- so a corner-store host with no paydata in it
341
- // carries no ice, and costs the scene nothing.
342
- "nodeAccess"?: "wireless", // Hosts are always reachable from the Matrix -- that is what
343
- // makes them hosts. There is no air-gapped host: for a vault that
344
- // must be entered in the flesh, put an OFFLINE SERVER device in the
345
- // room's items instead and leave the host off. Legacy "wired" values
346
- // are migrated automatically; do not author them.
347
- "hostGrid"?: "public" | "local" | "global:<corp>", // OPTIONAL. Which grid the host sits on (SR5 p.233 cross-grid -2). Leave it out unless the fiction says -- hosts are reachable from every grid.
348
- "hostRating"?: number, // 1-12, SR5 p.247. What KIND of host this is, and the only thing
349
- // that tells a pawnshop apart from an arcology on the grid map.
350
- // 1-2 personal sites, pirate archives. 3-4 low-end commercial,
351
- // private business. 5-6 social media, local police. 7-8 local
352
- // corporate, low-level government. 9-10 regional corporate, secure
353
- // sites. 11-12 megacorporate HQ, military command. Rate it for the
354
- // place it actually is; omit it and the host is treated as a 3.
355
- "warded"?: boolean, // astral barrier: projecting mages can't enter. Use sparingly.
356
- "watched"?: boolean, // security cameras cover this room (feeds ride the site's hosts;
357
- // hasNode rooms are always watched implicitly). Use for lobbies,
358
- // corridors, and secure floors in corp/guarded locations -- loud
359
- // acts on camera cost runners dearly, and deckers loop the feeds.
360
- "exits": [{ "direction": "north"|"east"|"south"|"west"|"up"|"down", "targetRoom": string,
361
- "keyRequired"?: string, // an items[].name with role "key"
362
- "device"?: string, // a devices[].name that holds this door shut. That
363
- // device's own "room" MUST be the room declaring this
364
- // exit -- you have to stand where the mechanism is.
365
- "guard"?: string }] // an npcs[].name who STANDS in this doorway and will
366
- // not move. A person, not a mechanism: the way past
367
- // is talking them round or putting them down.
368
- // NEVER set "device" and "guard" on one exit -- a
369
- // door is gated by a thing OR a person, never both.
370
- // Doors are TWO-WAY automatically: declare each connection from ONE
371
- // side only (with its keyRequired/puzzle on that one declaration) --
372
- // never declare the reverse direction too.
373
- }],
374
- // STAFF YOUR COMMERCE. Any room that sells, serves, or treats -- a
375
- // market, shop, bar, restaurant, clinic, pawnshop -- MUST have an
376
- // npcs[] entry stationed in it. A shop with nobody behind the counter
377
- // is a room the runner walks into and finds blank, no matter how good
378
- // its description reads.
379
- "npcs": [{
380
- "name": string, "concept": string, "startLocation": string,
381
- "plane"?: "meat"|"matrix"|"astral", // "matrix" = ICE guarding a hasNode room;
382
- // "astral" = a spirit. Default meat.
383
- "hostile"?: boolean // OPPOSITION: this one KNOWS the runner does not belong and will
384
- // act on it -- a posted guard, a patrolling enforcer, a gang
385
- // holding the room. Default false, and LEAVE IT FALSE unless the
386
- // concept is genuinely someone's security. NOT for staff who
387
- // merely work for the wrong people: a bartender, a clerk, a
388
- // ripperdoc and a fixer are all NEUTRAL even inside an enemy
389
- // building, and marking them turns an ordinary scene into a
390
- // brawl. If in doubt, leave it out -- a guard who stays calm is
391
- // a missed beat, a hostile bartender is a broken scene.
392
- // ICE NEEDS NO FLAG: a "matrix" NPC is intrusion countermeasures
393
- // and is treated as opposition automatically. Write it only to
394
- // say FALSE, for the rare tame construct.
395
- }],
396
- "devices": [{
397
- "name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
398
- // scanner, a ward) -- a THING, never a person
399
- "room": string, // REQUIRED. A device always stands in ONE room -- the one you must be
400
- // in to reach it. If it holds a door, declare that exit FROM THIS
401
- // SAME ROOM with "device": "<this name>".
402
- "kind": "lock"|"panel"|"ward", // lock = a mechanism: picked, breached, or opened by its
403
- // key. panel = an interface: a code, or hacked. ward = a working:
404
- // cast at or dispelled. EXACTLY these lowercase strings.
405
- // A device is always a THING. A doorman or a checkpoint guard is
406
- // NOT a device -- write them into npcs[] and name them in that
407
- // exit's "guard"; people are got past socially, not picked.
408
- "grantsItem"?: string // an items[].name with role "reward" -- what opening it hands
409
- // over. Omit for a device that holds a door: the open way IS
410
- // the reward.
411
- }],
412
- "items": [{
413
- "name": string,
414
- "role": "winCondition"|"reward"|"key"|"clue"|"loot"|"starting",
415
- "category": string, // one of: Weapon, MeleeWeapon, RangedWeapon, Armor, BodyArmor,
416
- // ArmorClothing, Helmet, Hat, LegArmor, ArmArmor, Gloves, Commlink,
417
- // Cyberdeck, Drone, Credstick, CurrencyContainer, Key, Trinket,
418
- // Document, Datachip, Vice, Book, Tool, Clothing, Glasses, Boots,
419
- // Medical, StimPatch, SpellFormula, Grimoire, Focus, Ammunition,
420
- // Food, Drink (consumables -- hot food heals stun, drink is liquid
421
- // courage; fine as dressing in bars/kitchens, never as prizes),
422
- // Cyberware (RARE prize loot -- an uninstalled augmentation part.
423
- // Its name must be one of: ${AUGMENTATIONS.map(a => a.name).join(', ')};
424
- // never one the runner already has installed, never role "starting")
425
- "room"?: string, "heldBy"?: string, // exactly one of these for roles winCondition/key/
426
- // clue/loot; NEITHER for roles reward/starting
427
- "plane"?: "meat"|"matrix", // "matrix" = paydata: only in a hasNode room, category
428
- // Datachip/Document. A matrix-plane winCondition item makes
429
- // the job a datarun (its room MUST have ice).
430
- "forPuzzle"?: string // role "clue" only: which puzzle's passphrase this clue reveals.
431
- // EVERY puzzle needs exactly one clue item, category "Book",
432
- // placed in a DIFFERENT room than that puzzle (door puzzles: any
433
- // room). The clue's text is written in a later pass.
434
- // ${isContinuation ? 'Role "starting" is FORBIDDEN in this scene -- the runner arrives fully equipped (see dossier).' : 'Role "starting" items are the fresh runner\'s basic kit.'}
435
- }],
436
- "winCondition": {
437
- "item": string, // the items[].name with role "winCondition" -- MUST have room or heldBy
438
- // set (never heldBy "toNpc" -- the player has to find it), and it is
439
- // the central plot object of "story"
440
- "toNpc": string, // an npcs[].name -- who wants it delivered
441
- "payout": number, // the job's pay in nuyen; the engine actually transfers it -- half at
442
- // the meet, the rest on delivery. ~2000-6000 for job #1, roughly half
443
- // again more per job after that
444
- "rewardItem"?: string // optional bonus: an items[].name with role "reward"
445
- },
446
- "client": string // the npcs[].name of the run's CLIENT -- see CLIENT below
447
- }
448
-
449
- CLIENT: every run has a CLIENT -- "a Mr. Johnson", the employer's deniable face, the person
450
- who holds THE MEET (briefs the runner in person, negotiates the rate, pays the whole fee as
451
- one lump sum on delivery). Invent a COVER NAME with corporate anonymity to it -- "Mr. Osprey",
452
- "Ms. Veil", "Señora Marrow" -- never literally "Mr. Johnson" and never "${FIXER_NAME}" (that's
453
- the runner's own fixer back home, who brokered this meet and is NOT in this scene). Station
454
- the client in a REACHABLE room that makes a characterful meet spot -- a bar booth, a noodle
455
- stall, the back seat of a parked limo, a chapel pew -- reachable from the start room WITHOUT
456
- passing any puzzle, key, or hostile ground: the meet opens the job, it is never gated behind
457
- it. The client MUST hold a Commlink-category item (role "loot", heldBy them) so the runner
458
- can also call them, and the player must have a commlink available (${isContinuation ? 'the runner already carries their own -- see the dossier' : '"starting" role, or findable in a reachable room'}).
459
- The client usually has no combat block -- they hired OUT the violence. They do NOT have to
460
- be winCondition.toNpc (delivering to the client at the meet spot is one good shape; a
461
- dead-drop to a different contact is another); any toNpc in an unreachable room needs a
462
- commlink of their own too.
463
-
464
- Hard structural rules (validated mechanically -- a violation is rejected):
465
- - Every exits[].targetRoom, npcs[].startLocation, items[].room name a real rooms[].name;
466
- every items[].heldBy names a real npcs[].name; every exits[].device names a real
467
- puzzles[].name; every keyRequired names a real items[].name with role "key".
468
- - "client" names a real npcs[].name, stationed in a REACHABLE room, holding a
469
- Commlink-category item.
470
- - Exactly one room has isStartRoom true, and it must be OUTDOOR street-level ground -- a
471
- street, alley, plaza, lot, dock, yard -- with "outdoors": true AND a name/roomType that
472
- says so. The runner
473
- arrives by cab at that curb and hails the ride home from the same spot; an indoor start
474
- room breaks the transport fiction. Never lock the start room's only exits behind keys
475
- placed beyond them: every key must be reachable WITHOUT passing through the door it opens.
476
- - A "reachable": false room never connects to a reachable one, and nothing the player must
477
- retrieve (winCondition item, keys, clues) is ever placed in one.
478
- - EVERY device names its own "room". If it holds a door, that door's exit is declared
479
- from THAT SAME room with "device": "<its name>".
480
- - EVERY WAY THROUGH IS FINDABLE -- no exceptions, and no dependence on who is playing:
481
- * every "panel" gets a code AND exactly one "clue"-role Book item (forDevice set) in a
482
- DIFFERENT room, reachable without passing the panel itself;
483
- * every "lock" gets a "key"-role item placed in the world, reachable without passing
484
- the lock itself.
485
- Write the clue or the key in the same breath as the device. A runner with the matching
486
- skill can still take the shortcut -- pick the lock, hack the panel -- but the found way
487
- must exist for a runner who has neither.
488
- - The winCondition item must not be heldBy winCondition.toNpc. If a matrix-plane item is the
489
- winCondition item, its room has hasNode true AND a "matrix"-plane NPC stationed there.
287
+ return `
288
+ You are generating the STRUCTURE (skeleton) of a self-contained Shadowrun-flavored text
289
+ adventure "run". This is job #${tier} in an ongoing campaign -- scale stakes accordingly.
290
+ Prose comes later in separate passes: here, every "concept" is ONE tight sentence of intent,
291
+ and everything else is structure. Get the STRUCTURE exactly right -- every name is a
292
+ cross-reference other passes and the game engine will rely on verbatim.
293
+ ${rumorBlock}
294
+ ${dataRunBlock}
295
+ ${canonBlock ? `\n${canonBlock}\n` : ''}
296
+ ${dossier ? `\n${dossier}\n` : ''}
297
+ ${JSON_ONLY}
298
+
299
+ {
300
+ "name": string, // the run's title
301
+ "story": string, // 1-2 SHORT teaser sentences shown before play -- name the shape of the
302
+ // trouble, never the objective, target names, locations, or passphrases
303
+ // (those are learned at the meet with the client).
304
+ "completionMessage": string, // 1-2 sentences shown when the job completes
305
+ "rooms": [{
306
+ "name": string, "concept": string, "roomType"?: string,
307
+ "outdoors": boolean, // TRUE only if OPEN TO THE SKY. A courtyard, a street, a
308
+ // rooftop, a dock: true. Anything with a roof -- a market
309
+ // HALL, a parking garage, a docking bay, a courtROOM -- is
310
+ // false, however outdoor its name sounds. Say it for every
311
+ // room; the engine cannot tell from the name and a wrong
312
+ // guess sends a taxi inside a building.
313
+ "isStartRoom"?: boolean, // EXACTLY ONE room
314
+ "reachable"?: boolean, // false ONLY for a remote contact's room (rare -- a voice on the
315
+ // other end of a call, never anyone the player must meet)
316
+ "owner"?: string, // an NPC's name, if it's their room. USE THIS for any territorial or
317
+ // guarded space (a gang cache, a fixer's office, a shop): the room's
318
+ // goods become THEIRS, and the engine makes them contest theft --
319
+ // an unowned prize in a guarded vault walks out unchallenged.
320
+ "hasNode"?: boolean, // a live Matrix host standing over this room: its own virtual space,
321
+ // ENTERED by a persona that holds a mark on it. Hacking it releases
322
+ // the room's maglocks AND unseals its matrix-plane files, which live
323
+ // INSIDE the host -- from the outside grid it is one sealed icon.
324
+ // A HOST IS ORDINARY, NOT CORPORATE. "Your local Stuffer Shack has
325
+ // a host icon" (p.219), and the core book publishes that convenience
326
+ // store as a worked sample host (p.245-246) -- so a pawnshop, a bar,
327
+ // a clinic or a corner store plausibly has one, rated 3-4 as "low-end
328
+ // commercial, private business" (p.247). Withholding hosts from
329
+ // everything but corp/tech sites is what made an ordinary storefront
330
+ // read "thin grid" on the map when canon says otherwise.
331
+ // SET IT ON PREMISES, NOT ON EVERY ROOM: the businesses and secure
332
+ // sites a scene contains, not each corridor, alley, stairwell or
333
+ // room inside one building -- a site is covered by its host, and
334
+ // interior rooms of the same premises share it rather than each
335
+ // sprouting their own. Give every host a hostRating chosen for the
336
+ // place it actually is; a scene where every rating is 3 is as wrong
337
+ // as one where nothing is a host.
338
+ // Ice NPCs and matrix-plane items belong ONLY in hasNode rooms, and
339
+ // any hasNode room holding matrix items MUST have an ice NPC
340
+ // stationed in it -- so a corner-store host with no paydata in it
341
+ // carries no ice, and costs the scene nothing.
342
+ "nodeAccess"?: "wireless", // Hosts are always reachable from the Matrix -- that is what
343
+ // makes them hosts. There is no air-gapped host: for a vault that
344
+ // must be entered in the flesh, put an OFFLINE SERVER device in the
345
+ // room's items instead and leave the host off. Legacy "wired" values
346
+ // are migrated automatically; do not author them.
347
+ "hostGrid"?: "public" | "local" | "global:<corp>", // OPTIONAL. Which grid the host sits on (SR5 p.233 cross-grid -2). Leave it out unless the fiction says -- hosts are reachable from every grid.
348
+ "hostRating"?: number, // 1-12, SR5 p.247. What KIND of host this is, and the only thing
349
+ // that tells a pawnshop apart from an arcology on the grid map.
350
+ // 1-2 personal sites, pirate archives. 3-4 low-end commercial,
351
+ // private business. 5-6 social media, local police. 7-8 local
352
+ // corporate, low-level government. 9-10 regional corporate, secure
353
+ // sites. 11-12 megacorporate HQ, military command. Rate it for the
354
+ // place it actually is; omit it and the host is treated as a 3.
355
+ "warded"?: boolean, // astral barrier: projecting mages can't enter. Use sparingly.
356
+ "watched"?: boolean, // security cameras cover this room (feeds ride the site's hosts;
357
+ // hasNode rooms are always watched implicitly). Use for lobbies,
358
+ // corridors, and secure floors in corp/guarded locations -- loud
359
+ // acts on camera cost runners dearly, and deckers loop the feeds.
360
+ "exits": [{ "direction": "north"|"east"|"south"|"west"|"up"|"down", "targetRoom": string,
361
+ "keyRequired"?: string, // an items[].name with role "key"
362
+ "device"?: string, // a devices[].name that holds this door shut. That
363
+ // device's own "room" MUST be the room declaring this
364
+ // exit -- you have to stand where the mechanism is.
365
+ "guard"?: string }] // an npcs[].name who STANDS in this doorway and will
366
+ // not move. A person, not a mechanism: the way past
367
+ // is talking them round or putting them down.
368
+ // NEVER set "device" and "guard" on one exit -- a
369
+ // door is gated by a thing OR a person, never both.
370
+ // Doors are TWO-WAY automatically: declare each connection from ONE
371
+ // side only (with its keyRequired/puzzle on that one declaration) --
372
+ // never declare the reverse direction too.
373
+ }],
374
+ // STAFF YOUR COMMERCE. Any room that sells, serves, or treats -- a
375
+ // market, shop, bar, restaurant, clinic, pawnshop -- MUST have an
376
+ // npcs[] entry stationed in it. A shop with nobody behind the counter
377
+ // is a room the runner walks into and finds blank, no matter how good
378
+ // its description reads.
379
+ "npcs": [{
380
+ "name": string, "concept": string, "startLocation": string,
381
+ "plane"?: "meat"|"matrix"|"astral", // "matrix" = ICE guarding a hasNode room;
382
+ // "astral" = a spirit. Default meat.
383
+ "hostile"?: boolean // OPPOSITION: this one KNOWS the runner does not belong and will
384
+ // act on it -- a posted guard, a patrolling enforcer, a gang
385
+ // holding the room. Default false, and LEAVE IT FALSE unless the
386
+ // concept is genuinely someone's security. NOT for staff who
387
+ // merely work for the wrong people: a bartender, a clerk, a
388
+ // ripperdoc and a fixer are all NEUTRAL even inside an enemy
389
+ // building, and marking them turns an ordinary scene into a
390
+ // brawl. If in doubt, leave it out -- a guard who stays calm is
391
+ // a missed beat, a hostile bartender is a broken scene.
392
+ // ICE NEEDS NO FLAG: a "matrix" NPC is intrusion countermeasures
393
+ // and is treated as opposition automatically. Write it only to
394
+ // say FALSE, for the rare tame construct.
395
+ }],
396
+ "devices": [{
397
+ "name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
398
+ // scanner, a ward) -- a THING, never a person
399
+ "room": string, // REQUIRED. A device always stands in ONE room -- the one you must be
400
+ // in to reach it. If it holds a door, declare that exit FROM THIS
401
+ // SAME ROOM with "device": "<this name>".
402
+ "kind": "lock"|"panel"|"ward", // lock = a mechanism: picked, breached, or opened by its
403
+ // key. panel = an interface: a code, or hacked. ward = a working:
404
+ // cast at or dispelled. EXACTLY these lowercase strings.
405
+ // A device is always a THING. A doorman or a checkpoint guard is
406
+ // NOT a device -- write them into npcs[] and name them in that
407
+ // exit's "guard"; people are got past socially, not picked.
408
+ "grantsItem"?: string // an items[].name with role "reward" -- what opening it hands
409
+ // over. Omit for a device that holds a door: the open way IS
410
+ // the reward.
411
+ }],
412
+ "items": [{
413
+ "name": string,
414
+ "role": "winCondition"|"reward"|"key"|"clue"|"loot"|"starting",
415
+ "category": string, // one of: Weapon, MeleeWeapon, RangedWeapon, Armor, BodyArmor,
416
+ // ArmorClothing, Helmet, Hat, LegArmor, ArmArmor, Gloves, Commlink,
417
+ // Cyberdeck, Drone, Credstick, CurrencyContainer, Key, Trinket,
418
+ // Document, Datachip, Vice, Book, Tool, Clothing, Glasses, Boots,
419
+ // Medical, StimPatch, SpellFormula, Grimoire, Focus, Ammunition,
420
+ // Food, Drink (consumables -- hot food heals stun, drink is liquid
421
+ // courage; fine as dressing in bars/kitchens, never as prizes),
422
+ // Cyberware (RARE prize loot -- an uninstalled augmentation part.
423
+ // Its name must be one of: ${AUGMENTATIONS.map(a => a.name).join(', ')};
424
+ // never one the runner already has installed, never role "starting")
425
+ "room"?: string, "heldBy"?: string, // exactly one of these for roles winCondition/key/
426
+ // clue/loot; NEITHER for roles reward/starting
427
+ "plane"?: "meat"|"matrix", // "matrix" = paydata: only in a hasNode room, category
428
+ // Datachip/Document. A matrix-plane winCondition item makes
429
+ // the job a datarun (its room MUST have ice).
430
+ "forPuzzle"?: string // role "clue" only: which puzzle's passphrase this clue reveals.
431
+ // EVERY puzzle needs exactly one clue item, category "Book",
432
+ // placed in a DIFFERENT room than that puzzle (door puzzles: any
433
+ // room). The clue's text is written in a later pass.
434
+ // ${isContinuation ? 'Role "starting" is FORBIDDEN in this scene -- the runner arrives fully equipped (see dossier).' : 'Role "starting" items are the fresh runner\'s basic kit.'}
435
+ }],
436
+ "winCondition": {
437
+ "item": string, // the items[].name with role "winCondition" -- MUST have room or heldBy
438
+ // set (never heldBy "toNpc" -- the player has to find it), and it is
439
+ // the central plot object of "story"
440
+ "toNpc": string, // an npcs[].name -- who wants it delivered
441
+ "payout": number, // the job's pay in nuyen; the engine actually transfers it -- half at
442
+ // the meet, the rest on delivery. ~2000-6000 for job #1, roughly half
443
+ // again more per job after that
444
+ "rewardItem"?: string // optional bonus: an items[].name with role "reward"
445
+ },
446
+ "client": string // the npcs[].name of the run's CLIENT -- see CLIENT below
447
+ }
448
+
449
+ CLIENT: every run has a CLIENT -- "a Mr. Johnson", the employer's deniable face, the person
450
+ who holds THE MEET (briefs the runner in person, negotiates the rate, pays the whole fee as
451
+ one lump sum on delivery). Invent a COVER NAME with corporate anonymity to it -- "Mr. Osprey",
452
+ "Ms. Veil", "Señora Marrow" -- never literally "Mr. Johnson" and never "${FIXER_NAME}" (that's
453
+ the runner's own fixer back home, who brokered this meet and is NOT in this scene). Station
454
+ the client in a REACHABLE room that makes a characterful meet spot -- a bar booth, a noodle
455
+ stall, the back seat of a parked limo, a chapel pew -- reachable from the start room WITHOUT
456
+ passing any puzzle, key, or hostile ground: the meet opens the job, it is never gated behind
457
+ it. The client MUST hold a Commlink-category item (role "loot", heldBy them) so the runner
458
+ can also call them, and the player must have a commlink available (${isContinuation ? 'the runner already carries their own -- see the dossier' : '"starting" role, or findable in a reachable room'}).
459
+ The client usually has no combat block -- they hired OUT the violence. They do NOT have to
460
+ be winCondition.toNpc (delivering to the client at the meet spot is one good shape; a
461
+ dead-drop to a different contact is another); any toNpc in an unreachable room needs a
462
+ commlink of their own too.
463
+
464
+ Hard structural rules (validated mechanically -- a violation is rejected):
465
+ - Every exits[].targetRoom, npcs[].startLocation, items[].room name a real rooms[].name;
466
+ every items[].heldBy names a real npcs[].name; every exits[].device names a real
467
+ puzzles[].name; every keyRequired names a real items[].name with role "key".
468
+ - "client" names a real npcs[].name, stationed in a REACHABLE room, holding a
469
+ Commlink-category item.
470
+ - Exactly one room has isStartRoom true, and it must be OUTDOOR street-level ground -- a
471
+ street, alley, plaza, lot, dock, yard -- with "outdoors": true AND a name/roomType that
472
+ says so. The runner
473
+ arrives by cab at that curb and hails the ride home from the same spot; an indoor start
474
+ room breaks the transport fiction. Never lock the start room's only exits behind keys
475
+ placed beyond them: every key must be reachable WITHOUT passing through the door it opens.
476
+ - A "reachable": false room never connects to a reachable one, and nothing the player must
477
+ retrieve (winCondition item, keys, clues) is ever placed in one.
478
+ - EVERY device names its own "room". If it holds a door, that door's exit is declared
479
+ from THAT SAME room with "device": "<its name>".
480
+ - EVERY WAY THROUGH IS FINDABLE -- no exceptions, and no dependence on who is playing:
481
+ * every "panel" gets a code AND exactly one "clue"-role Book item (forDevice set) in a
482
+ DIFFERENT room, reachable without passing the panel itself;
483
+ * every "lock" gets a "key"-role item placed in the world, reachable without passing
484
+ the lock itself.
485
+ Write the clue or the key in the same breath as the device. A runner with the matching
486
+ skill can still take the shortcut -- pick the lock, hack the panel -- but the found way
487
+ must exist for a runner who has neither.
488
+ - The winCondition item must not be heldBy winCondition.toNpc. If a matrix-plane item is the
489
+ winCondition item, its room has hasNode true AND a "matrix"-plane NPC stationed there.
490
490
  - ${isContinuation
491
491
  ? 'ZERO "starting"-role items -- the runner arrives with everything they own. 4-6'
492
- : '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
493
- "loot"-role world items, one "clue" per panel and one "key" per lock.
494
-
495
- Scope: ${scope}. Gritty noir Shadowrun tone: a client, a job, a debt, a betrayal -- and a
496
- fresh premise, not a generic "corp blackmail vault" rerun.${repairNote(previousError)}
492
+ : '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
493
+ "loot"-role world items, one "clue" per panel and one "key" per lock.
494
+
495
+ Scope: ${scope}. Gritty noir Shadowrun tone: a client, a job, a debt, a betrayal -- and a
496
+ fresh premise, not a generic "corp blackmail vault" rerun.${repairNote(previousError)}
497
497
  `.trim();
498
498
  }
499
499
  /** Shared context header for the detail passes: the fixed skeleton. */
500
500
  function skeletonContext(skeleton) {
501
- return `The scene's FIXED skeleton (all names are final -- reference them EXACTLY, never invent new ones):
501
+ return `The scene's FIXED skeleton (all names are final -- reference them EXACTLY, never invent new ones):
502
502
  ${JSON.stringify(skeleton, null, 1)}`;
503
503
  }
504
504
  export function buildRoomsPrompt(skeleton, previousError) {
505
- return `
506
- You are writing the room descriptions for a Shadowrun text adventure.
507
-
508
- ${skeletonContext(skeleton)}
509
-
510
- For EVERY room in the skeleton, write evocative 2-3 sentence prose expanding its "concept" --
511
- gritty cyberpunk noir, concrete sensory detail, no mechanics talk and no spoilers (never
512
- reveal passphrases, hidden items, or plot twists in a room description).
513
- Never write a CODE-SHAPED string into room prose -- no door numbers, unit numbers, serials,
514
- or stencilled digits on a wall. The barriers' answers are the only codes in this world, and
515
- a number in scenery is a number a player will carry to the nearest lock and type.
516
-
517
- "At" SPOTS -- named places WITHIN a room the player can "move to":
518
- - Give 2-4 "spots" ONLY where positioning matters (bars, markets, offices, warehouses --
519
- social or large spaces). SKIP them for corridors, transit streets, and small
520
- single-purpose rooms; most rooms should have none.
521
- - Spot names are short concrete nouns ("bar", "booths", "loading dock", "desk") -- and
522
- WEAVE each spot's name into that room's prose so the player can see it.
523
- - Every room WITH spots must include one doorway spot (a name containing
524
- door/entrance/entry/threshold/gate).
525
- - At most ONE spot per spotted room may carry "tags": ["cover-partial"] (chest-high
526
- cover) or ["cover-good"] (near-total concealment). Most need neither.
527
- - "size": "tiny" | "small" | "medium" | "large" | "huge" -- how far apart the spots
528
- stand and how far a shot travels: tiny is a closet or cubicle (a few meters across),
529
- small a cramped room, medium a normal room, large a proper hall or bar floor, huge
530
- a warehouse or atrium. Pick the smallest size that fits the room's concept.
531
- - Optional "place" per spot -- where it stands: "north" | "south" | "east" | "west"
532
- (that wall), "northeast corner" | "northwest corner" | "southeast corner" |
533
- "southwest corner", or "center". Say this ONLY when the prose
534
- actually puts it there ("the bar runs along the east wall"); leave it off otherwise
535
- and the engine spreads the spot out on its own.
536
- - Optional "levels" -- ONLY for a genuinely multi-level space the prose describes
537
- (a catwalk over a warehouse floor, a mezzanine above a club, a balcony over an
538
- atrium): [{ "name": "catwalk", "side": "north"|"south"|"east"|"west",
539
- "kind": "catwalk"|"balcony"|"mezzanine" }], at most 2. A spot standing on one
540
- carries "level": 1 (or 2 for the second). Most rooms have none.
541
- - "npcSpots": station each npc whose startLocation is THIS room at the spot they'd
542
- work from ({ "<npc name>": "<spot name>" }). Leave wanderers unstationed.
543
- GIVE EACH ONE A DIFFERENT SPOT unless they genuinely share a post -- two people
544
- on one tile read as standing shoulder to shoulder for no reason (a real report).
545
- - Describe the SPACE ONLY -- never write PEOPLE or takeable goods into the prose.
546
- People move, flee, fight, and die, and loot gets pocketed, but the description is
547
- forever: a sentry written "blocking the way, arms crossed" reads absurd after he
548
- ran (a real session). Presence is the engine's job -- "look" reports who stands
549
- where live. The fixed stage (the spots, the architecture, the weather) is what
550
- belongs in the prose.
551
- - "itemSpots": anchor each item placed in THIS room to the spot it sits at
552
- ({ "<item name>": "<spot name>" }) -- a prize belongs at the guarded spot, not
553
- floating in the doorway; loose scatter can stay unanchored.
554
- - "exitSpots": which spot each exit leaves from ({ "<direction>": "<spot name>" }).
555
- Give one to EVERY exit of a spotted room, and copy the spot name EXACTLY as you wrote
556
- it in "spots" -- this is the only thing that tells the engine which way out a doorway
557
- leads. Leave one off and the engine has to GUESS from the spot's name, which is how a
558
- spot called "the back door" ended up bound to the stairs DOWN and stranded in the middle
559
- of the floor. A vertical way out (stairs, a ladder, a floor hatch) needs a spot that
560
- reads vertical, and a door needs one that reads like a door.
561
- An exit blocked by a barrier deserves its
562
- OWN named spot -- a back gate is not the entrance; two gates are two spots -- so the
563
- player always knows which gate is which.
564
- - ONE STORY: the prose and the spot data must agree. When the prose mentions a
565
- stationed NPC, place them AT their npcSpots spot and use that spot's exact name
566
- ("under the awning" only if stationed at "awning") -- never "near the entrance" for
567
- someone stationed elsewhere. An NPC whose job is watching a barrier stands AT that
568
- barrier's exit spot.
569
-
570
- ${JSON_ONLY}
571
- [ { "name": string, "description": string, "roomType": string,
572
- "size"?: string, "spots"?: [ { "name": string, "tags"?: string[], "place"?: string, "level"?: number } ],
573
- "npcSpots"?: { }, "itemSpots"?: { }, "exitSpots"?: { },
574
- "levels"?: [ { "name": string, "side": string, "kind": string } ] } ]
575
- One entry per skeleton room, "name" copied exactly.${repairNote(previousError)}
505
+ return `
506
+ You are writing the room descriptions for a Shadowrun text adventure.
507
+
508
+ ${skeletonContext(skeleton)}
509
+
510
+ For EVERY room in the skeleton, write evocative 2-3 sentence prose expanding its "concept" --
511
+ gritty cyberpunk noir, concrete sensory detail, no mechanics talk and no spoilers (never
512
+ reveal passphrases, hidden items, or plot twists in a room description).
513
+ Never write a CODE-SHAPED string into room prose -- no door numbers, unit numbers, serials,
514
+ or stencilled digits on a wall. The barriers' answers are the only codes in this world, and
515
+ a number in scenery is a number a player will carry to the nearest lock and type.
516
+
517
+ "At" SPOTS -- named places WITHIN a room the player can "move to":
518
+ - Give 2-4 "spots" ONLY where positioning matters (bars, markets, offices, warehouses --
519
+ social or large spaces). SKIP them for corridors, transit streets, and small
520
+ single-purpose rooms; most rooms should have none.
521
+ - Spot names are short concrete nouns ("bar", "booths", "loading dock", "desk") -- and
522
+ WEAVE each spot's name into that room's prose so the player can see it.
523
+ - Every room WITH spots must include one doorway spot (a name containing
524
+ door/entrance/entry/threshold/gate).
525
+ - At most ONE spot per spotted room may carry "tags": ["cover-partial"] (chest-high
526
+ cover) or ["cover-good"] (near-total concealment). Most need neither.
527
+ - "size": "tiny" | "small" | "medium" | "large" | "huge" -- how far apart the spots
528
+ stand and how far a shot travels: tiny is a closet or cubicle (a few meters across),
529
+ small a cramped room, medium a normal room, large a proper hall or bar floor, huge
530
+ a warehouse or atrium. Pick the smallest size that fits the room's concept.
531
+ - Optional "place" per spot -- where it stands: "north" | "south" | "east" | "west"
532
+ (that wall), "northeast corner" | "northwest corner" | "southeast corner" |
533
+ "southwest corner", or "center". Say this ONLY when the prose
534
+ actually puts it there ("the bar runs along the east wall"); leave it off otherwise
535
+ and the engine spreads the spot out on its own.
536
+ - Optional "levels" -- ONLY for a genuinely multi-level space the prose describes
537
+ (a catwalk over a warehouse floor, a mezzanine above a club, a balcony over an
538
+ atrium): [{ "name": "catwalk", "side": "north"|"south"|"east"|"west",
539
+ "kind": "catwalk"|"balcony"|"mezzanine" }], at most 2. A spot standing on one
540
+ carries "level": 1 (or 2 for the second). Most rooms have none.
541
+ - "npcSpots": station each npc whose startLocation is THIS room at the spot they'd
542
+ work from ({ "<npc name>": "<spot name>" }). Leave wanderers unstationed.
543
+ GIVE EACH ONE A DIFFERENT SPOT unless they genuinely share a post -- two people
544
+ on one tile read as standing shoulder to shoulder for no reason (a real report).
545
+ - Describe the SPACE ONLY -- never write PEOPLE or takeable goods into the prose.
546
+ People move, flee, fight, and die, and loot gets pocketed, but the description is
547
+ forever: a sentry written "blocking the way, arms crossed" reads absurd after he
548
+ ran (a real session). Presence is the engine's job -- "look" reports who stands
549
+ where live. The fixed stage (the spots, the architecture, the weather) is what
550
+ belongs in the prose.
551
+ - "itemSpots": anchor each item placed in THIS room to the spot it sits at
552
+ ({ "<item name>": "<spot name>" }) -- a prize belongs at the guarded spot, not
553
+ floating in the doorway; loose scatter can stay unanchored.
554
+ - "exitSpots": which spot each exit leaves from ({ "<direction>": "<spot name>" }).
555
+ Give one to EVERY exit of a spotted room, and copy the spot name EXACTLY as you wrote
556
+ it in "spots" -- this is the only thing that tells the engine which way out a doorway
557
+ leads. Leave one off and the engine has to GUESS from the spot's name, which is how a
558
+ spot called "the back door" ended up bound to the stairs DOWN and stranded in the middle
559
+ of the floor. A vertical way out (stairs, a ladder, a floor hatch) needs a spot that
560
+ reads vertical, and a door needs one that reads like a door.
561
+ An exit blocked by a barrier deserves its
562
+ OWN named spot -- a back gate is not the entrance; two gates are two spots -- so the
563
+ player always knows which gate is which.
564
+ - ONE STORY: the prose and the spot data must agree. When the prose mentions a
565
+ stationed NPC, place them AT their npcSpots spot and use that spot's exact name
566
+ ("under the awning" only if stationed at "awning") -- never "near the entrance" for
567
+ someone stationed elsewhere. An NPC whose job is watching a barrier stands AT that
568
+ barrier's exit spot.
569
+
570
+ ${JSON_ONLY}
571
+ [ { "name": string, "description": string, "roomType": string,
572
+ "size"?: string, "spots"?: [ { "name": string, "tags"?: string[], "place"?: string, "level"?: number } ],
573
+ "npcSpots"?: { }, "itemSpots"?: { }, "exitSpots"?: { },
574
+ "levels"?: [ { "name": string, "side": string, "kind": string } ] } ]
575
+ One entry per skeleton room, "name" copied exactly.${repairNote(previousError)}
576
576
  `.trim();
577
577
  }
578
578
  export function buildNpcsPrompt(skeleton, previousError) {
579
- return `
580
- You are writing the characters for a Shadowrun text adventure.
581
-
582
- ${skeletonContext(skeleton)}
583
-
584
- For EVERY npc in the skeleton, expand its "concept":
585
- - "description": private GM notes on personality/motives (never shown verbatim to the player).
586
- - "dialogue": 2-4 short in-character lines. For "${skeleton.client}" (the CLIENT -- the
587
- Johnson holding the meet) this is the REAL briefing -- the objective (the winCondition
588
- item), where to start looking, the stakes, and the exact payout figure from the skeleton
589
- (the engine pays exactly that number, as one lump sum on delivery). Their private
590
- "description" should read like a client rep's dossier -- the employer's deniable face, a
591
- Johnson buying deniability. When referring to places, use EXACT room names from the
592
- skeleton -- never invent locations.
593
- NEVER write a CREDENTIAL into "dialogue" or "description" -- no door code, safe
594
- combination, passphrase, or key-word, not even a hint at the digits. This stage runs in
595
- PARALLEL with the stage that writes the pass each barrier actually accepts, so you
596
- cannot see those passes, and any credential you write here is one you invented: the lock
597
- will refuse it and the player will burn a run trusting it. An NPC may say that a code
598
- EXISTS, who holds it, or where it's written down -- never what it is. The engine hands
599
- the real pass to whoever genuinely holds it, at runtime (see npc.ts).
600
- - "combat": OPTIONAL attribute block -- omit for anyone who'd never fight. Each attribute
601
- 1-8 (soft civilian 1-2, street-level 3-4, hardened enforcer 5-6, elite 7-8):
602
- { "body", "agility", "reaction", "combatSkill", "strength"?, "willpower"?, "logic"?,
603
- "intuition"?, "charisma"?, "magic"?, "resonance"?, "adept"?,
604
- "skills"?: { "firearms"?, "blades"?, "clubs"?, "unarmed"?, "sneaking"?, "perception"?, "palming"?,
605
- "hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?, "locksmith"?, "computer"?,
606
- "negotiation"?, "con"?, "intimidation"?, "etiquette"?, "leadership"?, "performance"?, "disguise"?,
607
- "medicine"?, "assensing"?, "counterspelling"?, "conjuring"?, "gunnery"? } }
608
- "magic" 3-6 ONLY for a genuinely Awakened concept; "plane":"matrix" ice fights with
609
- logic 4-7; give combat blocks to anyone the story expects in a firefight. "skills" is
610
- optional flavor for defined concepts (a sharpshooter's "firearms": 6).
611
-
612
- ${JSON_ONLY}
613
- [ { "name": string, "description": string, "dialogue": string[], "combat"?: { ... } } ]
614
- One entry per skeleton npc, "name" copied exactly.${repairNote(previousError)}
579
+ return `
580
+ You are writing the characters for a Shadowrun text adventure.
581
+
582
+ ${skeletonContext(skeleton)}
583
+
584
+ For EVERY npc in the skeleton, expand its "concept":
585
+ - "description": private GM notes on personality/motives (never shown verbatim to the player).
586
+ - "dialogue": 2-4 short in-character lines. For "${skeleton.client}" (the CLIENT -- the
587
+ Johnson holding the meet) this is the REAL briefing -- the objective (the winCondition
588
+ item), where to start looking, the stakes, and the exact payout figure from the skeleton
589
+ (the engine pays exactly that number, as one lump sum on delivery). Their private
590
+ "description" should read like a client rep's dossier -- the employer's deniable face, a
591
+ Johnson buying deniability. When referring to places, use EXACT room names from the
592
+ skeleton -- never invent locations.
593
+ NEVER write a CREDENTIAL into "dialogue" or "description" -- no door code, safe
594
+ combination, passphrase, or key-word, not even a hint at the digits. This stage runs in
595
+ PARALLEL with the stage that writes the pass each barrier actually accepts, so you
596
+ cannot see those passes, and any credential you write here is one you invented: the lock
597
+ will refuse it and the player will burn a run trusting it. An NPC may say that a code
598
+ EXISTS, who holds it, or where it's written down -- never what it is. The engine hands
599
+ the real pass to whoever genuinely holds it, at runtime (see npc.ts).
600
+ - "combat": OPTIONAL attribute block -- omit for anyone who'd never fight. Each attribute
601
+ 1-8 (soft civilian 1-2, street-level 3-4, hardened enforcer 5-6, elite 7-8):
602
+ { "body", "agility", "reaction", "combatSkill", "strength"?, "willpower"?, "logic"?,
603
+ "intuition"?, "charisma"?, "magic"?, "resonance"?, "adept"?,
604
+ "skills"?: { "firearms"?, "blades"?, "clubs"?, "unarmed"?, "sneaking"?, "perception"?, "palming"?,
605
+ "hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?, "locksmith"?, "computer"?,
606
+ "negotiation"?, "con"?, "intimidation"?, "etiquette"?, "leadership"?, "performance"?, "disguise"?,
607
+ "medicine"?, "assensing"?, "counterspelling"?, "conjuring"?, "gunnery"? } }
608
+ "magic" 3-6 ONLY for a genuinely Awakened concept; "plane":"matrix" ice fights with
609
+ logic 4-7; give combat blocks to anyone the story expects in a firefight. "skills" is
610
+ optional flavor for defined concepts (a sharpshooter's "firearms": 6).
611
+
612
+ ${JSON_ONLY}
613
+ [ { "name": string, "description": string, "dialogue": string[], "combat"?: { ... } } ]
614
+ One entry per skeleton npc, "name" copied exactly.${repairNote(previousError)}
615
615
  `.trim();
616
616
  }
617
617
  export function buildDevicesPrompt(skeleton, previousError) {
618
618
  if (skeleton.devices.length === 0)
619
619
  return '';
620
- return `
621
- You are writing the DEVICES for a Shadowrun text adventure. A device is a THING that is
622
- shut and can be opened -- a lock, a panel, a ward. Never a person.
623
-
624
- ${skeletonContext(skeleton)}
625
-
626
- For EVERY device in the skeleton, expand its "concept" (what it physically is -- that's
627
- fixed, as is its "kind"):
628
- - "description": what the player sees on approaching it, shown verbatim -- PLAIN and
629
- concrete, never a cryptic riddle-poem. Describe it as a THING: what it is, how it's
630
- secured, and what it visibly expects. Write NO people into it -- no guards, no dialogue,
631
- no "they demand": anyone standing watch does their own demanding in play, in their own
632
- voice. Never state the credential itself, and never write ANY OTHER number into it -- no
633
- unit number, no model number, no serial, no floor number. Every code-shaped string a
634
- player reads is one they will go and type.
635
- - "code": ONLY for kind "panel", and REQUIRED for one. The exact credential it accepts, and
636
- it MUST BE THE SAME KIND OF THING your "description" says the panel wants. This is the
637
- single most confusing thing in the game when it drifts: a player who reads "a keypad with
638
- worn digits" and then finds "double or nothing" written in a notebook does not believe
639
- they have the code, and goes looking for a number that does not exist.
640
- * If the description shows a NUMERIC pad, dial, or counter -- digits only, 3-6 of them,
641
- no spaces ("470912"). Then it really does take a number.
642
- * Otherwise (an intercom, a voice-gate, a spoken passphrase, a name, a sign reading
643
- "password") -- ALL LOWERCASE, 2-5 plain words, no punctuation.
644
- Either way a concrete literal thing, never wordplay.
645
- OMIT "code" for a "lock" or a "ward". You cannot talk at a mechanical lock, and a ward
646
- answers to the Art -- neither has anything to hear a phrase with, so a code on one is a
647
- promise the game cannot keep.
648
- - "openMessage" / "refuseMessage": one sentence each, in-fiction.
649
- - "guardsExit": ONLY when the concept is physically TERRAIN blocking a passage -- rubble, a
650
- collapsed rack, a security shutter across a walkway -- name the exact "direction" of one
651
- of its own room's exits that it seals. A device on a container, safe, or item pickup
652
- guards nothing physical: leave it unset.
653
- - "footprint": "small" | "medium" | "large" -- how much of the room the blockage occupies.
654
- Only meaningful alongside "guardsExit".
655
-
656
- ${JSON_ONLY}
657
- [ { "name": string, "description": string, "code"?: string, "openMessage": string,
658
- "refuseMessage": string, "guardsExit"?: string, "footprint"?: string } ]
659
- One entry per skeleton device, "name" copied exactly.${repairNote(previousError)}
620
+ return `
621
+ You are writing the DEVICES for a Shadowrun text adventure. A device is a THING that is
622
+ shut and can be opened -- a lock, a panel, a ward. Never a person.
623
+
624
+ ${skeletonContext(skeleton)}
625
+
626
+ For EVERY device in the skeleton, expand its "concept" (what it physically is -- that's
627
+ fixed, as is its "kind"):
628
+ - "description": what the player sees on approaching it, shown verbatim -- PLAIN and
629
+ concrete, never a cryptic riddle-poem. Describe it as a THING: what it is, how it's
630
+ secured, and what it visibly expects. Write NO people into it -- no guards, no dialogue,
631
+ no "they demand": anyone standing watch does their own demanding in play, in their own
632
+ voice. Never state the credential itself, and never write ANY OTHER number into it -- no
633
+ unit number, no model number, no serial, no floor number. Every code-shaped string a
634
+ player reads is one they will go and type.
635
+ - "code": ONLY for kind "panel", and REQUIRED for one. The exact credential it accepts, and
636
+ it MUST BE THE SAME KIND OF THING your "description" says the panel wants. This is the
637
+ single most confusing thing in the game when it drifts: a player who reads "a keypad with
638
+ worn digits" and then finds "double or nothing" written in a notebook does not believe
639
+ they have the code, and goes looking for a number that does not exist.
640
+ * If the description shows a NUMERIC pad, dial, or counter -- digits only, 3-6 of them,
641
+ no spaces ("470912"). Then it really does take a number.
642
+ * Otherwise (an intercom, a voice-gate, a spoken passphrase, a name, a sign reading
643
+ "password") -- ALL LOWERCASE, 2-5 plain words, no punctuation.
644
+ Either way a concrete literal thing, never wordplay.
645
+ OMIT "code" for a "lock" or a "ward". You cannot talk at a mechanical lock, and a ward
646
+ answers to the Art -- neither has anything to hear a phrase with, so a code on one is a
647
+ promise the game cannot keep.
648
+ - "openMessage" / "refuseMessage": one sentence each, in-fiction.
649
+ - "guardsExit": ONLY when the concept is physically TERRAIN blocking a passage -- rubble, a
650
+ collapsed rack, a security shutter across a walkway -- name the exact "direction" of one
651
+ of its own room's exits that it seals. A device on a container, safe, or item pickup
652
+ guards nothing physical: leave it unset.
653
+ - "footprint": "small" | "medium" | "large" -- how much of the room the blockage occupies.
654
+ Only meaningful alongside "guardsExit".
655
+
656
+ ${JSON_ONLY}
657
+ [ { "name": string, "description": string, "code"?: string, "openMessage": string,
658
+ "refuseMessage": string, "guardsExit"?: string, "footprint"?: string } ]
659
+ One entry per skeleton device, "name" copied exactly.${repairNote(previousError)}
660
660
  `.trim();
661
661
  }
662
662
  export function buildItemsPrompt(skeleton, devices, previousError) {
@@ -667,36 +667,36 @@ export function buildItemsPrompt(skeleton, devices, previousError) {
667
667
  return `- "${i.name}" is the clue for device "${i.forDevice}": its "details" text MUST contain the exact code "${code ?? 'UNKNOWN'}" verbatim (a diary entry, scrawled note, or manual that gives it away once read).`;
668
668
  })
669
669
  .join('\n');
670
- return `
671
- You are writing the items for a Shadowrun text adventure.
672
-
673
- ${skeletonContext(skeleton)}
674
-
675
- For EVERY item in the skeleton, expand it into a full item ("name" and "category" are fixed):
676
- - "description": one sentence of always-visible flavor.
677
- - "details": deeper text shown on close examination or reading. CLUE ITEMS (below) put
678
- their puzzle's credential HERE -- never in "description".
679
- - "size": "Tiny"|"Small"|"Medium"|"Large"|"VeryLarge"; "rating": "Common"|"Standard"|"Rare"|
680
- "Legendary"|"MilitaryGrade" (rating scales combat/heal/deck power -- reserve Rare+ for
681
- earned rewards); "shape"/"color"/"texture": short literals; "weight": realistic kilograms
682
- (0.05 chip, ~2 pistol, ~4 armor); "currencyAmount" only for Credstick/CurrencyContainer.
683
-
684
- Clue requirements (mechanically validated -- the exact phrase must appear in "details"):
685
- ${clueSpecs || '- (no clue items in this scene)'}
686
-
687
- THE ONLY CODES IN THIS WORLD are the clue phrases listed just above. No other item may
688
- carry a code-shaped string -- no serial numbers, no model numbers, no unit or locker or
689
- apartment numbers, no "access code" on a scrap of paper that isn't a listed clue. A player
690
- who reads a number writes it down and takes it to the nearest lock; a second plausible
691
- number is indistinguishable from the real one and it costs them the run. Quantities and
692
- prices are fine (a clip "holds 10 rounds", a stick "carries 2000 nuyen") -- those read as
693
- amounts, not credentials.
694
-
695
- ${JSON_ONLY}
696
- [ { "name": string, "description": string, "size": string, "category": string,
697
- "shape": string, "color": string, "texture": string, "rating": string, "weight": number,
698
- "currencyAmount"?: number, "details"?: string, "transferable"?: boolean } ]
699
- One entry per skeleton item, "name" and "category" copied exactly.${repairNote(previousError)}
670
+ return `
671
+ You are writing the items for a Shadowrun text adventure.
672
+
673
+ ${skeletonContext(skeleton)}
674
+
675
+ For EVERY item in the skeleton, expand it into a full item ("name" and "category" are fixed):
676
+ - "description": one sentence of always-visible flavor.
677
+ - "details": deeper text shown on close examination or reading. CLUE ITEMS (below) put
678
+ their puzzle's credential HERE -- never in "description".
679
+ - "size": "Tiny"|"Small"|"Medium"|"Large"|"VeryLarge"; "rating": "Common"|"Standard"|"Rare"|
680
+ "Legendary"|"MilitaryGrade" (rating scales combat/heal/deck power -- reserve Rare+ for
681
+ earned rewards); "shape"/"color"/"texture": short literals; "weight": realistic kilograms
682
+ (0.05 chip, ~2 pistol, ~4 armor); "currencyAmount" only for Credstick/CurrencyContainer.
683
+
684
+ Clue requirements (mechanically validated -- the exact phrase must appear in "details"):
685
+ ${clueSpecs || '- (no clue items in this scene)'}
686
+
687
+ THE ONLY CODES IN THIS WORLD are the clue phrases listed just above. No other item may
688
+ carry a code-shaped string -- no serial numbers, no model numbers, no unit or locker or
689
+ apartment numbers, no "access code" on a scrap of paper that isn't a listed clue. A player
690
+ who reads a number writes it down and takes it to the nearest lock; a second plausible
691
+ number is indistinguishable from the real one and it costs them the run. Quantities and
692
+ prices are fine (a clip "holds 10 rounds", a stick "carries 2000 nuyen") -- those read as
693
+ amounts, not credentials.
694
+
695
+ ${JSON_ONLY}
696
+ [ { "name": string, "description": string, "size": string, "category": string,
697
+ "shape": string, "color": string, "texture": string, "rating": string, "weight": number,
698
+ "currencyAmount"?: number, "details"?: string, "transferable"?: boolean } ]
699
+ One entry per skeleton item, "name" and "category" copied exactly.${repairNote(previousError)}
700
700
  `.trim();
701
701
  }
702
702
  // ========================= Parse / validate / assemble ====================