@maka/maka-cli 5.122.1 → 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 (366) 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 +14 -10
  20. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js.map +1 -1
  21. package/bundle/typescript/src/commands/env/env.command.js +8 -8
  22. package/bundle/typescript/src/commands/env/set.sub.cmd.js +11 -11
  23. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.d.ts +15 -0
  24. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.js +12 -0
  25. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.js.map +1 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +10 -0
  27. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +40 -14
  28. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +33 -10
  30. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +155 -72
  31. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +15 -6
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +1 -1
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +33 -33
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +2 -2
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +5 -1
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +6 -6
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +4 -4
  49. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  50. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js.map +1 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.d.ts +20 -0
  53. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +9 -0
  54. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
  55. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +7 -3
  56. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js.map +1 -1
  57. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +3 -3
  58. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  59. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +1 -1
  60. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  61. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +3 -3
  62. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/commands/search.js +9 -9
  64. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +10 -10
  65. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  66. package/bundle/typescript/src/commands/game/sideQuest/commands/source.js +7 -7
  67. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js.map +1 -1
  69. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +24 -0
  70. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +95 -0
  71. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
  72. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-chunks.js +443 -443
  73. package/bundle/typescript/src/commands/game/sideQuest/factories/scene-factory.js +6 -6
  74. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +45 -1
  75. package/bundle/typescript/src/commands/game/sideQuest/game.js +120 -9
  76. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/metatypes.d.ts +53 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/metatypes.js +86 -0
  79. package/bundle/typescript/src/commands/game/sideQuest/metatypes.js.map +1 -0
  80. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +214 -208
  81. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  82. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +215 -18
  83. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +548 -117
  84. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  85. package/bundle/typescript/src/commands/game/sideQuest/models/room.js +9 -9
  86. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +2 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -2
  88. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +96 -4
  90. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +290 -11
  91. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
  92. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene1.json +556 -556
  93. package/bundle/typescript/src/commands/game/sideQuest/scenes/scene2.json +1447 -1447
  94. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.d.ts +11 -1
  95. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js +13 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js.map +1 -1
  97. package/bundle/typescript/src/commands/game/sideQuest/skills.d.ts +17 -0
  98. package/bundle/typescript/src/commands/game/sideQuest/skills.js +23 -0
  99. package/bundle/typescript/src/commands/game/sideQuest/skills.js.map +1 -0
  100. package/bundle/typescript/src/commands/game/sideQuest/spells.d.ts +76 -3
  101. package/bundle/typescript/src/commands/game/sideQuest/spells.js +48 -21
  102. package/bundle/typescript/src/commands/game/sideQuest/spells.js.map +1 -1
  103. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +8 -0
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +90 -82
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +277 -10
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  107. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +121 -2
  108. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +342 -18
  109. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  110. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +65 -2
  111. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +74 -7
  112. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  113. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +1 -1
  114. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  115. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.d.ts +248 -0
  116. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.js +38 -0
  117. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.js.map +1 -0
  118. package/bundle/typescript/src/commands/game/sideQuest/utilities/ephemeral.js +14 -14
  119. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +1 -2
  120. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -1
  121. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +1 -2
  122. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -1
  123. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +5 -0
  124. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  125. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +5 -1
  126. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  127. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +22 -12
  128. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js.map +1 -1
  129. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +1 -2
  130. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  131. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +1 -0
  132. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +20 -0
  133. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  134. package/bundle/typescript/src/commands/game/sideQuest-backlog.sub.cmd.js +2 -2
  135. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +33 -2
  136. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  137. package/bundle/typescript/src/commands/generate.js +2 -2
  138. package/bundle/typescript/src/commands/lint.js +10 -10
  139. package/bundle/typescript/src/commands/login.js +19 -1
  140. package/bundle/typescript/src/commands/login.js.map +1 -1
  141. package/bundle/typescript/src/commands/logout.js +16 -1
  142. package/bundle/typescript/src/commands/logout.js.map +1 -1
  143. package/bundle/typescript/src/commands/maka.cmd.js +26 -26
  144. package/bundle/typescript/src/commands/murder.js +14 -14
  145. package/bundle/typescript/src/commands/proxy.js +13 -13
  146. package/bundle/typescript/src/commands/remote/reconfig.sub.cmd.js +6 -6
  147. package/bundle/typescript/src/commands/run/docker.sub.cmd.js +11 -11
  148. package/bundle/typescript/src/commands/run/run.command.js +24 -24
  149. package/bundle/typescript/src/commands/set/global.sub.cmd.js +8 -8
  150. package/bundle/typescript/src/commands/set/local.sub.cmd.js +2 -2
  151. package/bundle/typescript/src/commands/set/set.command.js +3 -3
  152. package/bundle/typescript/src/commands/testCmd/test.command.js +2 -2
  153. package/bundle/typescript/src/exec.js +0 -0
  154. package/bundle/typescript/src/generators/api.js +11 -11
  155. package/bundle/typescript/src/generators/collection.js +6 -6
  156. package/bundle/typescript/src/generators/hook.js +10 -10
  157. package/bundle/typescript/src/generators/kits/auth/auth.generator.js +2 -2
  158. package/bundle/typescript/src/generators/kits/auth/auth0-meteor.auth.gen.js +612 -612
  159. package/bundle/typescript/src/generators/kits/auth/basic-meteor.auth.gen.js +48 -48
  160. package/bundle/typescript/src/generators/kits/auth/provider.auth.gen.js +12 -12
  161. package/bundle/typescript/src/generators/kits/esri/esri.generator.js +3 -3
  162. package/bundle/typescript/src/generators/kits/esri/map.esri.gen.js +7 -7
  163. package/bundle/typescript/src/generators/kits/frames/frames.generator.js +16 -16
  164. package/bundle/typescript/src/generators/kits/mui/auth.mui.gen.js +2 -2
  165. package/bundle/typescript/src/generators/kits/mui/auth0.mui.gen.js +5 -5
  166. package/bundle/typescript/src/generators/kits/mui/frame.mui.gen.js +4 -4
  167. package/bundle/typescript/src/generators/kits/mui/mui.generator.js +22 -22
  168. package/bundle/typescript/src/generators/services/translation.js +5 -5
  169. package/bundle/typescript/src/templates/aws/cloudformation/create-ec2-instance.template +14 -14
  170. package/bundle/typescript/src/templates/meteor/api/api.js +20 -20
  171. package/bundle/typescript/src/templates/meteor/api/api.js.ts +20 -20
  172. package/bundle/typescript/src/templates/meteor/api/apollo-gateway.js +50 -50
  173. package/bundle/typescript/src/templates/meteor/api/apollo-gateway.js.ts +55 -55
  174. package/bundle/typescript/src/templates/meteor/api/graphql-api.js +63 -63
  175. package/bundle/typescript/src/templates/meteor/api/graphql-api.js.ts +69 -69
  176. package/bundle/typescript/src/templates/meteor/api/methods.js +39 -39
  177. package/bundle/typescript/src/templates/meteor/api/methods.js.ts +40 -40
  178. package/bundle/typescript/src/templates/meteor/api/register-api.js +3 -3
  179. package/bundle/typescript/src/templates/meteor/api/register-api.js.ts +3 -3
  180. package/bundle/typescript/src/templates/meteor/api/resolvers.js +9 -9
  181. package/bundle/typescript/src/templates/meteor/api/resolvers.js.ts +9 -9
  182. package/bundle/typescript/src/templates/meteor/api/typeDefs.js +6 -6
  183. package/bundle/typescript/src/templates/meteor/api/typeDefs.js.ts +6 -6
  184. package/bundle/typescript/src/templates/meteor/app/client/body.html +3 -3
  185. package/bundle/typescript/src/templates/meteor/app/client/head.html +12 -12
  186. package/bundle/typescript/src/templates/meteor/app/client/main.js +15 -15
  187. package/bundle/typescript/src/templates/meteor/app/client/main.js.jsx +15 -15
  188. package/bundle/typescript/src/templates/meteor/app/client/main.js.ts +17 -17
  189. package/bundle/typescript/src/templates/meteor/app/client/main.js.tsx +17 -17
  190. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js +20 -20
  191. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.jsx +107 -107
  192. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.ts +20 -20
  193. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/index.js.tsx +108 -108
  194. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js +39 -39
  195. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.jsx +33 -33
  196. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.ts +24 -24
  197. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/routes.js.tsx +33 -33
  198. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js +9 -9
  199. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.jsx +9 -9
  200. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.ts +9 -9
  201. package/bundle/typescript/src/templates/meteor/app/imports/startup/client/templates.js.tsx +9 -9
  202. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js +1 -1
  203. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.jsx +20 -20
  204. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.ts +1 -1
  205. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/index.js.tsx +20 -20
  206. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/routes.js.jsx +28 -28
  207. package/bundle/typescript/src/templates/meteor/app/imports/startup/lib/routes.js.tsx +28 -28
  208. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js +1 -1
  209. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js.jsx +95 -95
  210. package/bundle/typescript/src/templates/meteor/app/imports/startup/server/index.js.tsx +98 -98
  211. package/bundle/typescript/src/templates/meteor/app/imports/ui/test-helpers.js +24 -24
  212. package/bundle/typescript/src/templates/meteor/app/imports/ui/test-helpers.js.ts +24 -24
  213. package/bundle/typescript/src/templates/meteor/app/lib/main.js +3 -3
  214. package/bundle/typescript/src/templates/meteor/app/lib/main.js.jsx +3 -3
  215. package/bundle/typescript/src/templates/meteor/app/lib/main.js.ts +3 -3
  216. package/bundle/typescript/src/templates/meteor/app/lib/main.js.tsx +3 -3
  217. package/bundle/typescript/src/templates/meteor/app/maka.d.ts +533 -533
  218. package/bundle/typescript/src/templates/meteor/app/meteor.d.ts +181 -181
  219. package/bundle/typescript/src/templates/meteor/app/public/manifest.json +27 -27
  220. package/bundle/typescript/src/templates/meteor/app/public/sw.js +30 -30
  221. package/bundle/typescript/src/templates/meteor/app/public/sw.js.jsx +30 -30
  222. package/bundle/typescript/src/templates/meteor/app/public/sw.js.ts +30 -30
  223. package/bundle/typescript/src/templates/meteor/app/public/sw.js.tsx +30 -30
  224. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js +179 -179
  225. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.jsx +179 -179
  226. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.ts +179 -179
  227. package/bundle/typescript/src/templates/meteor/app/server/helmet-csp.js.tsx +179 -179
  228. package/bundle/typescript/src/templates/meteor/app/server/main.js +13 -13
  229. package/bundle/typescript/src/templates/meteor/app/server/main.js.jsx +13 -13
  230. package/bundle/typescript/src/templates/meteor/app/server/main.js.ts +14 -14
  231. package/bundle/typescript/src/templates/meteor/app/server/main.js.tsx +14 -14
  232. package/bundle/typescript/src/templates/meteor/app/tsconfig.json +42 -42
  233. package/bundle/typescript/src/templates/meteor/collection/collection.js +51 -51
  234. package/bundle/typescript/src/templates/meteor/collection/collection.js.ts +55 -55
  235. package/bundle/typescript/src/templates/meteor/collection/tests.js +27 -27
  236. package/bundle/typescript/src/templates/meteor/collection/tests.js.ts +27 -27
  237. package/bundle/typescript/src/templates/meteor/config/deploy.sh.template +101 -101
  238. package/bundle/typescript/src/templates/meteor/config/docker-compose.yaml +17 -17
  239. package/bundle/typescript/src/templates/meteor/config/nginx.conf.template +31 -31
  240. package/bundle/typescript/src/templates/meteor/config/pm2.config.js +27 -27
  241. package/bundle/typescript/src/templates/meteor/config/process.env +5 -5
  242. package/bundle/typescript/src/templates/meteor/config/settings.json +11 -11
  243. package/bundle/typescript/src/templates/meteor/config/ssh.json +6 -6
  244. package/bundle/typescript/src/templates/meteor/dbc/config.js +70 -70
  245. package/bundle/typescript/src/templates/meteor/dbc/config.js.ts +70 -70
  246. package/bundle/typescript/src/templates/meteor/dbc/conn.js +37 -37
  247. package/bundle/typescript/src/templates/meteor/dbc/conn.js.ts +37 -37
  248. package/bundle/typescript/src/templates/meteor/dbc/register-dbc.js +3 -3
  249. package/bundle/typescript/src/templates/meteor/dbc/register-dbc.js.ts +3 -3
  250. package/bundle/typescript/src/templates/meteor/github/workflows/deploy.yaml +150 -150
  251. package/bundle/typescript/src/templates/meteor/method/method.js +48 -48
  252. package/bundle/typescript/src/templates/meteor/method/method.js.ts +48 -48
  253. package/bundle/typescript/src/templates/meteor/package/package.js +37 -37
  254. package/bundle/typescript/src/templates/meteor/package/package.js.jsx +37 -37
  255. package/bundle/typescript/src/templates/meteor/package/package.js.ts +36 -36
  256. package/bundle/typescript/src/templates/meteor/package/package.js.tsx +37 -37
  257. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js +5 -5
  258. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.jsx +5 -5
  259. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.ts +5 -5
  260. package/bundle/typescript/src/templates/meteor/package/tests/client/package.js.tsx +5 -5
  261. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js +5 -5
  262. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.jsx +5 -5
  263. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.ts +5 -5
  264. package/bundle/typescript/src/templates/meteor/package/tests/server/package.js.tsx +5 -5
  265. package/bundle/typescript/src/templates/meteor/project/.maka/config.json +29 -29
  266. package/bundle/typescript/src/templates/meteor/project/README.md +33 -33
  267. package/bundle/typescript/src/templates/meteor/project/build/README +4 -4
  268. package/bundle/typescript/src/templates/meteor/project/gitignore +10 -10
  269. package/bundle/typescript/src/templates/meteor/project/package.json +29 -29
  270. package/bundle/typescript/src/templates/meteor/publish/publish.js +7 -7
  271. package/bundle/typescript/src/templates/meteor/publish/publish.js.ts +7 -7
  272. package/bundle/typescript/src/templates/meteor/service/logger/logger.js +27 -27
  273. package/bundle/typescript/src/templates/meteor/service/logger/logger.js.ts +27 -27
  274. package/bundle/typescript/src/templates/meteor/stylesheet/stylesheet.css +5 -5
  275. package/bundle/typescript/src/templates/meteor/stylesheet/stylesheet.css.less +5 -5
  276. package/bundle/typescript/src/templates/meteor/template/template-tests.js +1 -1
  277. package/bundle/typescript/src/templates/meteor/template/template-tests.js.jsx +55 -55
  278. package/bundle/typescript/src/templates/meteor/template/template-tests.js.ts +1 -1
  279. package/bundle/typescript/src/templates/meteor/template/template-tests.js.tsx +55 -55
  280. package/bundle/typescript/src/templates/meteor/template/template.css +3 -3
  281. package/bundle/typescript/src/templates/meteor/template/template.css.less +3 -3
  282. package/bundle/typescript/src/templates/meteor/template/template.css.scss +3 -3
  283. package/bundle/typescript/src/templates/meteor/template/template.html +5 -5
  284. package/bundle/typescript/src/templates/meteor/template/template.js +20 -20
  285. package/bundle/typescript/src/templates/meteor/template/template.js.jsx +77 -77
  286. package/bundle/typescript/src/templates/meteor/template/template.js.ts +1 -1
  287. package/bundle/typescript/src/templates/meteor/template/template.js.tsx +89 -89
  288. package/bundle/typescript/src/templates/react/component/component.css +6 -6
  289. package/bundle/typescript/src/templates/react/component/component.css.less +6 -6
  290. package/bundle/typescript/src/templates/react/component/component.css.scss +3 -3
  291. package/bundle/typescript/src/templates/react/component/component.js.jsx +59 -59
  292. package/bundle/typescript/src/templates/react/component/component.js.tsx +65 -65
  293. package/bundle/typescript/src/templates/react/hook/hook.css +3 -3
  294. package/bundle/typescript/src/templates/react/hook/hook.css.scss +3 -3
  295. package/bundle/typescript/src/templates/react/hook/hook.js.jsx +19 -19
  296. package/bundle/typescript/src/templates/react/hook/hook.js.tsx +19 -19
  297. package/bundle/typescript/src/templates/react/kit/auth/provider/authentication-provider.js.jsx +306 -306
  298. package/bundle/typescript/src/templates/react/kit/auth/provider/authentication-provider.js.tsx +350 -350
  299. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.jsx +178 -178
  300. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.tsx +178 -178
  301. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.jsx +59 -59
  302. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.tsx +67 -67
  303. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.jsx +273 -273
  304. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.tsx +275 -275
  305. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.jsx +182 -182
  306. package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.tsx +199 -199
  307. package/bundle/typescript/src/templates/react/kit/auth/service/meteor-auth-service.js.jsx +138 -138
  308. package/bundle/typescript/src/templates/react/kit/auth/service/meteor-auth-service.js.tsx +153 -153
  309. package/bundle/typescript/src/templates/react/kit/esri/map/basic-map.js.jsx +29 -29
  310. package/bundle/typescript/src/templates/react/kit/esri/map/basic-map.js.tsx +32 -32
  311. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.css +63 -63
  312. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.jsx +153 -153
  313. package/bundle/typescript/src/templates/react/kit/frames/frames-demo.tsx +164 -164
  314. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.css +105 -105
  315. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.jsx +158 -158
  316. package/bundle/typescript/src/templates/react/kit/frames/workspace-manager.tsx +166 -166
  317. package/bundle/typescript/src/templates/react/kit/mui/auth/forgot-password/basic-forgot-password-page.js.jsx +34 -34
  318. package/bundle/typescript/src/templates/react/kit/mui/auth/forgot-password/basic-forgot-password-page.js.tsx +33 -33
  319. package/bundle/typescript/src/templates/react/kit/mui/auth/login/basic-login-page.js.jsx +53 -53
  320. package/bundle/typescript/src/templates/react/kit/mui/auth/login/basic-login-page.js.tsx +53 -53
  321. package/bundle/typescript/src/templates/react/kit/mui/auth/logout/basic-logout-page.js.jsx +25 -25
  322. package/bundle/typescript/src/templates/react/kit/mui/auth/logout/basic-logout-page.js.tsx +25 -25
  323. package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.jsx +41 -41
  324. package/bundle/typescript/src/templates/react/kit/mui/auth/password-reset/basic-password-reset-page.js.tsx +34 -34
  325. package/bundle/typescript/src/templates/react/kit/mui/auth/register/basic-register-page.js.jsx +33 -33
  326. package/bundle/typescript/src/templates/react/kit/mui/auth/register/basic-register-page.js.tsx +33 -33
  327. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.jsx +71 -71
  328. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.tsx +71 -71
  329. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.jsx +123 -123
  330. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.tsx +123 -123
  331. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.jsx +56 -56
  332. package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.tsx +74 -74
  333. package/bundle/typescript/src/templates/react/kit/mui/frame/app-bars/basic-app-bar.js.jsx +43 -43
  334. package/bundle/typescript/src/templates/react/kit/mui/frame/app-bars/basic-app-bar.js.tsx +43 -43
  335. package/bundle/typescript/src/templates/react/kit/mui/frame/drawers/basic-drawer.js.jsx +121 -121
  336. package/bundle/typescript/src/templates/react/kit/mui/frame/drawers/basic-drawer.js.tsx +117 -117
  337. package/bundle/typescript/src/templates/react/kit/mui/frame/footers/basic-sticky-footer.js.jsx +41 -41
  338. package/bundle/typescript/src/templates/react/kit/mui/frame/footers/basic-sticky-footer.js.tsx +41 -41
  339. package/bundle/typescript/src/templates/react/route/route.js +8 -8
  340. package/bundle/typescript/src/templates/react/route/route.js.jsx +5 -5
  341. package/bundle/typescript/src/templates/react/route/route.js.ts +8 -8
  342. package/bundle/typescript/src/templates/react/route/route.js.tsx +5 -5
  343. package/bundle/typescript/src/templates/react/test/test.js +1 -1
  344. package/bundle/typescript/src/templates/react/test/test.js.jsx +40 -40
  345. package/bundle/typescript/src/templates/react/test/test.js.ts +1 -1
  346. package/bundle/typescript/src/templates/react/test/test.js.tsx +40 -40
  347. package/bundle/typescript/src/templates/service/docker/Dockerfile +54 -54
  348. package/bundle/typescript/src/templates/service/eslint/babel.config.js +16 -16
  349. package/bundle/typescript/src/templates/service/eslint/config.eslint.js +69 -69
  350. package/bundle/typescript/src/templates/service/i18n/i18n.js +33 -33
  351. package/bundle/typescript/src/templates/service/i18n/i18n.js.ts +33 -33
  352. package/bundle/typescript/src/templates/service/i18n/locales.js +3 -3
  353. package/bundle/typescript/src/templates/service/i18n/locales.js.ts +3 -3
  354. package/bundle/typescript/src/templates/service/openssl/openssl.conf +22 -22
  355. package/bundle/typescript/src/templates/service/test/jest/jest.config.js +24 -24
  356. package/bundle/typescript/src/templates/service/test/jest/jest.setup.js +7 -7
  357. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.d.ts +59 -0
  358. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.js +120 -0
  359. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.js.map +1 -0
  360. package/bundle/typescript/src/tools/mpi/mpi.class.d.ts +16 -0
  361. package/bundle/typescript/src/tools/mpi/mpi.class.js +25 -7
  362. package/bundle/typescript/src/tools/mpi/mpi.class.js.map +1 -1
  363. package/bundle/typescript/src/tools/mpi/sdk-runtime.d.ts +1 -0
  364. package/bundle/typescript/src/tools/mpi/sdk-runtime.js +29 -0
  365. package/bundle/typescript/src/tools/mpi/sdk-runtime.js.map +1 -1
  366. package/package.json +120 -120
@@ -1,6 +1,11 @@
1
1
  import { getSoft, getProgram } from '../utilities/programs.js';
2
+ import { GEAR_LANES, CHROME_LANES, QUALITY_LANES, ART_LANES, ADEPT_LANES, scaleOf } from '../utilities/effects.js';
2
3
  import { catalogRowFor } from '../utilities/catalog.js';
3
4
  import { getQuality, resolveQualityKey } from '../qualities.js';
5
+ import { ADEPT_POWERS } from '../adept-powers.js';
6
+ import { skillsInGroup } from '../skill-groups.js';
7
+ import { spellFor } from '../utilities/catalog.js';
8
+ import { METAMAGICS_BY_KEY } from '../commands/initiate.js';
4
9
  import { isAspectKey } from '../aspects.js';
5
10
  import { hint } from '../utilities/hints.js';
6
11
  import { Category } from '../types/shared/item-enum.js';
@@ -48,28 +53,36 @@ export function sameHostSide(actor, other) {
48
53
  const sideOf = (p) => p.hostInside ?? p.insideHost;
49
54
  return sideOf(actor) === sideOf(other);
50
55
  }
51
- export const SKILL_NAMES = [
52
- 'blades', 'clubs', 'unarmed', 'archery', 'free-fall',
53
- 'gunnery', 'astral-combat', 'cybercombat',
54
- 'sneaking', 'perception',
55
- 'palming', 'escape-artist', 'disguise',
56
- 'hacking', 'locksmith', 'computer', 'electronic-warfare', 'hardware',
57
- 'negotiation', 'con', 'intimidation', 'etiquette', 'leadership',
58
- 'performance', 'instruction',
59
- 'first-aid', 'medicine',
60
- 'assensing', 'spellcasting', 'counterspelling',
61
- 'piloting', 'conjuring', 'compiling', 'decompiling', 'registering', 'software',
62
- // Stage 2 additions -- see the union above for why these twenty-three.
63
- 'impersonation', 'ritual-spellcasting',
64
- 'gymnastics', 'running', 'swimming',
65
- 'automatics', 'pistols', 'longarms',
66
- 'banishing', 'summoning', 'binding',
67
- 'cybertechnology',
68
- 'alchemy', 'disenchanting', 'artificing',
69
- 'aeronautics-mechanic', 'industrial-mechanic', 'automotive-mechanic', 'nautical-mechanic',
70
- 'navigation', 'tracking', 'survival',
71
- 'heavy-weapons', 'throwing-weapons', 'exotic-melee', 'exotic-ranged',
72
- ];
56
+ /**
57
+ * The SR5e-condensed active skill list. Every pool that used to lean on
58
+ * the single generic `combatSkill` attribute now draws the SPECIFIC
59
+ * skill (firearms for guns, blades for melee weapons, unarmed for fists
60
+ * and subdual, hacking for cybercombat). Non-combat skills back their own
61
+ * mechanics as they land -- perception drives search rolls today; the
62
+ * rest are rated on the sheet and waiting for their verbs.
63
+ */
64
+ /**
65
+ * The active-skill sheet, rounded out from the SR5e canon list (p.90-91,
66
+ * p.151) at this engine's granularity. CONDENSATIONS (canon -> here):
67
+ * Automatics/Longarms/Pistols/Heavy Weapons/Archery -> firearms;
68
+ * Gymnastics/Running/Swimming -> athletics; the Pilot family ->
69
+ * piloting; Summoning/Binding/Banishing -> conjuring; Impersonation
70
+ * rides disguise (its voice half). Every skill listed HOOKS a live
71
+ * system -- see the site named beside it. Canon skills acknowledged but
72
+ * NOT yet carried (no system to hook): Throwing Weapons, Exotic
73
+ * Weapons, Diving, Free-Fall, Survival, Tracking, Navigation, Animal
74
+ * Handling, Demolitions, Forgery, Chemistry, Armorer, the Mechanic
75
+ * family, Biotechnology, Cybertechnology, Arcana, Alchemy, Artificing,
76
+ * Ritual Spellcasting, Disenchanting, and Knowledge/Language skills.
77
+ * (The Resonance trio -- Compiling, Decompiling, Registering -- WAS on
78
+ * this not-yet-carried list; all three are now real and hooked, one per
79
+ * command file beside them.)
80
+ */
81
+ // The skill vocabulary now lives in skills.ts, a module with no imports
82
+ // of its own, so skill-groups.ts can read it without a cycle back
83
+ // through this file. Re-exported so nothing that imports SkillName or
84
+ // SKILL_NAMES from here has to change.
85
+ export { SKILL_NAMES } from '../skills.js';
73
86
  /**
74
87
  * Skills that fall back to the legacy `combatSkill` attribute when no
75
88
  * explicit rating is given -- this is what keeps every existing NPC seed
@@ -824,8 +837,8 @@ export class Player extends AbstractPlayer {
824
837
  const silent = this.runningSilent ? 2 : 0;
825
838
  // What the running programs add (catalog v13): Signal Scrub always,
826
839
  // Sneak only while silent -- the rows carry both, and the `only`.
827
- const scrub = this.gearBonus('pan-defense');
828
- const sneak = this.runningSilent ? this.gearBonus('pan-defense', { only: 'silent' }) - scrub : 0;
840
+ const scrub = this.bonus('pan-defense');
841
+ const sneak = this.runningSilent ? this.bonus('pan-defense', { only: 'silent' }) - scrub : 0;
829
842
  const attributeName = against === 'attack' ? 'Willpower' : 'Intuition';
830
843
  const attribute = against === 'attack' ? this.willpower : this.intuition;
831
844
  const firewall = this.isTechnomancer()
@@ -962,6 +975,11 @@ export class Player extends AbstractPlayer {
962
975
  * check lives here and not at either call site.
963
976
  */
964
977
  smartlinkTier() {
978
+ // CHROME SPECIFICALLY, not every source of firearms dice. This is
979
+ // asking which TIER of smartlink you have -- the implant behind
980
+ // your eye (p.453) or the unit clipped to the gun (p.432) -- and
981
+ // `bonus` would answer true for the gear one and collapse the very
982
+ // distinction the method exists to draw.
965
983
  if (this.augBonus('firearms') > 0)
966
984
  return 'augmentation';
967
985
  // ON YOUR FACE, NOT IN YOUR BAG (fxPHSuE4nrH3gAGiy, rejected:
@@ -1295,19 +1313,39 @@ export class Player extends AbstractPlayer {
1295
1313
  if (getAug(aug.key)?.implantsCarriedDeck)
1296
1314
  this.getAnyCyberdeck()?.implantIntoBody();
1297
1315
  }
1298
- // ==================== Gear effects (catalog v13) ====================
1299
- // A catalog row says what it does (`effects`), and this is the one
1300
- // place the engine reads that: what is worn (and fitted into it), what
1301
- // is drawn, what is carried, what runs on the deck, what is stored on
1302
- // a commlink. Same shape as augBonus / qualityBonus so a pool adds the
1303
- // three the same way.
1304
- /** Every effect that applies right now, with the rating it scales by. */
1316
+ // ================= THE EFFECTS LANE (one resolver) =================
1317
+ // A catalog row says what it DOES (`effects`), and this is the one
1318
+ // place the engine reads that -- for every kind of row, not just
1319
+ // gear. What is worn (and fitted into it), what is drawn, what is
1320
+ // carried, what runs on the deck, what is stored on a commlink, the
1321
+ // chrome that is installed, the qualities held, the adept powers and
1322
+ // metamagics a character simply has, and the spells being sustained.
1323
+ //
1324
+ // WHY ONE RESOLVER (user, 2026-09-04: "no matter if it's a quality,
1325
+ // augmentation, metatype ability, it has a lane to follow to apply
1326
+ // stats to the character"). Before this there were six shapes and six
1327
+ // readers, and every pool in the game hand-added whichever ones its
1328
+ // author remembered -- which is why a rigger's chrome worked on a
1329
+ // drone and silently did nothing on a bench repair two files away.
1330
+ // A term that has to be remembered is a term that gets forgotten.
1331
+ /**
1332
+ * Which SOURCE an effect came from. The single accessor `bonus()`
1333
+ * sums them all; the older per-source getters each sum their own,
1334
+ * which is what lets the call sites migrate one at a time instead of
1335
+ * in one unreviewable commit.
1336
+ */
1337
+ /** Every effect applying right now, with its source and its scale. */
1305
1338
  applyingEffects() {
1306
1339
  const out = [];
1307
- const take = (effects, defaultWhen, state, rating) => {
1340
+ // `slug` is which ROW the effect came from, and it is what a
1341
+ // `chosen` property resolves through. Recorded here rather than
1342
+ // searched for later: the first version of pickFor walked the
1343
+ // qualities looking for an array that CONTAINED the effect object,
1344
+ // which worked only for qualities and only by object identity.
1345
+ const take = (effects, defaultWhen, state, rating, lane = 'gear', slug = '') => {
1308
1346
  for (const e of effects)
1309
1347
  if ((e.when ?? defaultWhen) === state)
1310
- out.push({ effect: e, rating: rating ?? 1 });
1348
+ out.push({ effect: e, rating: rating ?? 1, lane, slug });
1311
1349
  };
1312
1350
  for (const [groupName, group] of Object.entries(this._equipment)) {
1313
1351
  if (!group)
@@ -1317,14 +1355,14 @@ export class Player extends AbstractPlayer {
1317
1355
  continue;
1318
1356
  if (Player.isWeaponSlot(groupName, slot)) {
1319
1357
  if (this.weaponDrawn)
1320
- take(item.effects, item.defaultWhen, 'held', item.ratingValue);
1358
+ take(item.effects, item.defaultWhen, 'held', item.ratingValue, 'gear', item.row?.slug ?? item.name);
1321
1359
  continue;
1322
1360
  }
1323
- take(item.effects, item.defaultWhen, 'worn', item.ratingValue);
1361
+ take(item.effects, item.defaultWhen, 'worn', item.ratingValue, 'gear', item.row?.slug ?? item.name);
1324
1362
  }
1325
1363
  }
1326
1364
  for (const item of this._inventory.getAllItems())
1327
- take(item.effects, item.defaultWhen, 'carried', item.ratingValue);
1365
+ take(item.effects, item.defaultWhen, 'carried', item.ratingValue, 'gear', item.row?.slug ?? item.name);
1328
1366
  const runningOn = new Set();
1329
1367
  const deck = this.activeDeck ?? this.getCyberdeck();
1330
1368
  if (deck)
@@ -1337,7 +1375,7 @@ export class Player extends AbstractPlayer {
1337
1375
  const p = getProgram(key);
1338
1376
  const row = p ? catalogRowFor(`${p.name} Program`) ?? catalogRowFor(p.name) : undefined;
1339
1377
  if (row?.effects)
1340
- take(row.effects, 'running', 'running', row.ratingValue);
1378
+ take(row.effects, 'running', 'running', row.ratingValue, 'gear', row.slug);
1341
1379
  }
1342
1380
  }
1343
1381
  for (const device of [...this.commlinks(), ...this._inventory.getAllItems().filter(i => i.isCyberdeck())]) {
@@ -1345,62 +1383,265 @@ export class Player extends AbstractPlayer {
1345
1383
  const soft = getSoft(key);
1346
1384
  const row = soft ? catalogRowFor(soft.name) : undefined;
1347
1385
  if (row?.effects)
1348
- take(row.effects, 'stored', 'stored', row.ratingValue);
1386
+ take(row.effects, 'stored', 'stored', row.ratingValue, 'gear', row.slug);
1349
1387
  }
1350
1388
  }
1389
+ // ---- The CHROME lane. Installed 'ware, with ONE relationship the
1390
+ // rows cannot state themselves: the wired tiers SUPERSEDE each other
1391
+ // (III beats II beats I) rather than stacking, and a synaptic booster
1392
+ // rides on top as bioware. Resolved by dropping the superseded rows
1393
+ // here, once, so every reader sees the same surviving set -- rather
1394
+ // than in a getter, where only that getter would have been right.
1395
+ for (const key of this.chromeThatCounts()) {
1396
+ const a = getAug(key);
1397
+ if (a?.effects)
1398
+ take(a.effects, 'installed', 'installed', undefined, 'chrome', key);
1399
+ }
1400
+ // ---- The ADEPT lane. A power is 'innate': there is no object to
1401
+ // carry and no state to enter, and its LEVEL is what "per level"
1402
+ // scales by. A character with no `adeptPowers` at all is on the
1403
+ // compressed legacy model and contributes nothing here -- that shape
1404
+ // is resolved in adeptAttackBonus/adeptDefenseBonus, which is where
1405
+ // it has always lived and where the tests that pin it can see it.
1406
+ for (const [key, level] of Object.entries(this.adeptPowers ?? {})) {
1407
+ if (level <= 0)
1408
+ continue;
1409
+ const power = ADEPT_POWERS[key];
1410
+ if (power?.effects)
1411
+ take(power.effects, 'innate', 'innate', level, 'adept', key);
1412
+ }
1413
+ // ---- The INITIATE lane. A metamagic applies from the moment it is
1414
+ // learned, and 'grade' is what Centering multiplies by.
1415
+ for (const key of this.metamagics) {
1416
+ const m = METAMAGICS_BY_KEY[key];
1417
+ if (m?.effects)
1418
+ take(m.effects, 'initiated', 'initiated', this.initiationGrade, 'initiate', key);
1419
+ }
1420
+ // ---- The SUSTAINED lane. A spell held up applies until it is
1421
+ // dropped, and the magnitude it scales by is the hits it landed --
1422
+ // read from the state cast.ts already keeps rather than duplicated
1423
+ // into a second map. Barriers are absent on purpose: they raise a
1424
+ // wall in a room (models/scene.ts reads them), and that is world
1425
+ // state rather than a modifier on anybody.
1426
+ for (const [key, hits] of this.sustainedMagnitudes()) {
1427
+ const sp = spellFor(key);
1428
+ if (sp?.effects)
1429
+ take(sp.effects, 'sustained', 'sustained', hits, 'sustained', key);
1430
+ }
1431
+ // ---- The QUALITY lane. A quality is "held" in the same sense a
1432
+ // coat is worn: it is on the sheet, so it applies. A RETIRED one is
1433
+ // dormant rather than deleted (qualities.ts) -- its holder keeps it
1434
+ // and it rolls nothing, which is enforced here, once, rather than
1435
+ // at each reader.
1436
+ for (const key of this.qualities) {
1437
+ const q = getQuality(key);
1438
+ if (!q?.effects || q.retired)
1439
+ continue;
1440
+ take(q.effects, 'held-quality', 'held-quality', this.qualityRating(key), 'quality', key);
1441
+ }
1442
+ return out;
1443
+ }
1444
+ /**
1445
+ * Sums one kind of effect. `lanes` narrows to particular sources --
1446
+ * omitted means EVERY source, which is what the unified `bonus()`
1447
+ * wants; the legacy getters each pass their own single lane so that
1448
+ * a call site still adding two of them by hand cannot double.
1449
+ */
1450
+ /**
1451
+ * The spells being sustained, and what each scales by.
1452
+ *
1453
+ * READS THE STATE cast.ts ALREADY KEEPS rather than a second map:
1454
+ * the armor spell stores its hits, and the other two are booleans
1455
+ * whose rows carry a flat value, so 1 is the right multiplier for
1456
+ * them. Adding a parallel store would be two places to forget to
1457
+ * update, which is the shape this whole pass has been removing.
1458
+ */
1459
+ sustainedMagnitudes() {
1460
+ const out = [];
1461
+ if (this._sustainedArmor > 0)
1462
+ out.push(['armor', this._sustainedArmor]);
1463
+ if (this.sustainedInvisibility)
1464
+ out.push(['invisibility', 1]);
1465
+ if (this.sustainedReflexes)
1466
+ out.push(['reflexes', 1]);
1351
1467
  return out;
1352
1468
  }
1353
- sumEffects(type, property, only) {
1469
+ sumEffects(type, property, only, lanes) {
1354
1470
  let total = 0;
1355
- for (const { effect, rating } of this.applyingEffects()) {
1356
- if (effect.type !== type || effect.property !== property)
1471
+ for (const { effect, rating, lane, slug } of this.applyingEffects()) {
1472
+ if (lanes !== undefined && !lanes.includes(lane))
1473
+ continue;
1474
+ // FORBIDDEN never sums. It carries no value on purpose, and a
1475
+ // summing reader must not be able to fold it in as a zero --
1476
+ // "you cannot have this" is a different statement from "this
1477
+ // adds nothing", and only the second one is arithmetic.
1478
+ if (effect.type === 'forbid')
1479
+ continue;
1480
+ if (effect.type !== type)
1481
+ continue;
1482
+ // A `chosen` property belongs to whatever the holder named, and
1483
+ // it resolves the same way for dice and flags as it already did
1484
+ // for ceilings. It did NOT until 2026-09-04: only capBonus knew
1485
+ // the word, so a quality like Codeslinger -- +2 to ONE Matrix
1486
+ // action you pick -- could not be written as an effect at all,
1487
+ // and baked its answer into the slug instead.
1488
+ const target = effect.property === 'chosen' ? this.picks.get(slug) : effect.property;
1489
+ if (target !== property)
1357
1490
  continue;
1358
1491
  if (effect.only !== undefined && effect.only !== only)
1359
1492
  continue;
1360
- total += effect.value * (effect.scaleBy === 'rating' ? rating : 1);
1493
+ total += effect.value * scaleOf(effect, rating);
1361
1494
  }
1362
1495
  return total;
1363
1496
  }
1364
1497
  /**
1365
- * Dice the gear adds to a pool or a skill's tests (cf. augBonus,
1366
- * qualityBonus): `kind` is a pool kind or a SkillName; `only` names a
1367
- * narrowing an effect may carry ('haggle', 'silent', 'matrix-search').
1498
+ * Dice the GEAR LANE adds, and nothing else.
1499
+ *
1500
+ * A LANE READER, NOT A POOL. Every pool in the game reads `bonus()`
1501
+ * now; this survives as the way to ask what one source contributes,
1502
+ * which is what the behaviour fixture interrogates and what a "where
1503
+ * did that die come from" answer would need. It has no production
1504
+ * caller of its own, and that is the correct state for it rather than
1505
+ * a sign it should go: deleting it would delete the fixture's ability
1506
+ * to see the lanes apart, which is the only thing that can catch one
1507
+ * lane being counted twice.
1368
1508
  */
1369
1509
  gearBonus(kind, opts = {}) {
1370
- return this.sumEffects('dice', kind, opts.only);
1510
+ return this.sumEffects('dice', kind, opts.only, GEAR_LANES);
1371
1511
  }
1372
1512
  /** What the gear adds to a LIMIT (a Shopsoft's +1 Social on a haggle). */
1373
1513
  gearLimit(kind, opts = {}) {
1514
+ return this.sumEffects('limit', kind, opts.only, GEAR_LANES);
1515
+ }
1516
+ // ---------------- THE UNIFIED ACCESSORS ----------------
1517
+ // Four calls, every source. A pool written against these cannot lose
1518
+ // a term to a new kind of catalog row, because there is nothing left
1519
+ // for it to forget to add.
1520
+ /**
1521
+ * EVERY die the character has for this pool or skill, from every
1522
+ * source -- gear, chrome, qualities, adept powers, metamagics, a
1523
+ * metatype trait, a sustained spell.
1524
+ *
1525
+ * `only` names a narrowing an effect may carry ('haggle', 'silent',
1526
+ * 'matrix-search'). `skill` lets a combat pool pick up dice that were
1527
+ * declared against the SKILL being rolled rather than the pool --
1528
+ * a smartlink says `firearms`, and the attack pool has to see it.
1529
+ *
1530
+ * The sources that have not yet moved onto the shared shape are added
1531
+ * explicitly below, each named, so that migrating one is a matter of
1532
+ * deleting its line here rather than hunting for what it fed.
1533
+ */
1534
+ bonus(kind, opts = {}) {
1535
+ let total = this.sumEffects('dice', kind, opts.only);
1536
+ if (opts.skill !== undefined && opts.skill !== kind) {
1537
+ total += this.sumEffects('dice', opts.skill, opts.only);
1538
+ }
1539
+ // Chrome moved onto the shared shape (2026-09-04), so it arrives
1540
+ // through the lane above and must NOT be added again here. Every
1541
+ // source that migrates loses its hand-added line at the same moment
1542
+ // -- the fixture is what makes forgetting that loud instead of a
1543
+ // quietly doubled pool.
1544
+ // THE FAN-IN. Hacking is Matrix work, so whatever taxes the Matrix
1545
+ // taxes hacking too (Simsense Vertigo, p.83). It is a relationship
1546
+ // between two POOLS, which no single row can declare, so it lives
1547
+ // here -- and it applies across every lane, not only qualities.
1548
+ //
1549
+ // That is a wider reading than the getter it replaces: today
1550
+ // `qualityBonus('hacking')` folds in only the QUALITY side, so a
1551
+ // datajack helps a Matrix action and does nothing for a hacking
1552
+ // test. Both readings cannot be right, and the canon sentence does
1553
+ // not care where the dice came from. The change is deliberate, and
1554
+ // lands where a call site adopts `bonus()` -- the fixture prints
1555
+ // the exact pools that move when it does.
1556
+ if (kind === 'hacking')
1557
+ total += this.bonus('matrix', opts);
1558
+ return total;
1559
+ }
1560
+ /**
1561
+ * What a source adds to the initiative SCORE -- the number you roll
1562
+ * against -- as opposed to `bonus('initiative')`, which is the DICE.
1563
+ *
1564
+ * The `initiative` effect arm has existed since the gear pass and
1565
+ * was read by nothing at all until the Increase Reflexes spell came
1566
+ * onto the lane (2026-09-04). Two different quantities sharing the
1567
+ * word "initiative" is exactly why the arm is separate.
1568
+ */
1569
+ initiativeBonus(of) {
1570
+ return this.sumEffects('initiative', of);
1571
+ }
1572
+ /** Every point on a LIMIT, from every source. */
1573
+ limitBonus(kind, opts = {}) {
1374
1574
  return this.sumEffects('limit', kind, opts.only);
1375
1575
  }
1376
- /** A capability the gear grants (dni, smartlink, no-fog, low-light...), by key. */
1377
- hasCapability(key) {
1576
+ /**
1577
+ * How much further this character may raise one skill or attribute
1578
+ * than anyone else -- Aptitude's chosen skill (p.72), Exceptional
1579
+ * Attribute's chosen attribute (p.73-74).
1580
+ *
1581
+ * The book states absolute numbers against ITS ceilings (a skill at 7
1582
+ * rather than 6 at creation, 13 rather than 12 after). This engine
1583
+ * caps lower than the book does, so what carries across is the DELTA
1584
+ * -- one step further than anyone else gets -- applied to whatever
1585
+ * ceiling the engine is enforcing. Writing the book's 13 over a house
1586
+ * cap of 7 would not be more faithful, only a bigger number in the
1587
+ * wrong system.
1588
+ *
1589
+ * A `chosen` ceiling resolves through the holder's own pick, so a
1590
+ * quality bought but never answered lifts nothing at all.
1591
+ */
1592
+ capBonus(of, name) {
1593
+ const category = of === 'skill' ? 'skills' : 'attributes';
1594
+ let total = 0;
1595
+ for (const { effect, rating, slug } of this.applyingEffects()) {
1596
+ if (effect.type !== 'cap' || effect.category !== category)
1597
+ continue;
1598
+ const target = effect.property === 'chosen' ? this.picks.get(slug) : effect.property;
1599
+ if (target !== name)
1600
+ continue;
1601
+ total += effect.value * (effect.scaleBy === 'rating' ? rating : 1);
1602
+ }
1603
+ return total;
1604
+ }
1605
+ /**
1606
+
1607
+
1608
+ /** A capability the character has, from any source, by key. */
1609
+ can(key) {
1378
1610
  return this.sumEffects('flag', key) > 0;
1379
1611
  }
1380
1612
  /**
1381
- * The single tap every pool reads chrome through. Wired Reflexes tiers
1382
- * don't stack -- the higher tier wins (II supersedes I) -- but the
1383
- * synaptic-vs-wired conflict is the clinic's job, not this getter's.
1613
+ * THE CHROME THAT ACTUALLY COUNTS: everything installed, minus the
1614
+ * wired-reflex tiers a higher tier supersedes.
1615
+ *
1616
+ * Wired Reflexes I, II and III do not stack -- the best one wins (a
1617
+ * synaptic booster is bioware and rides on top of whichever that is).
1618
+ * That is a relationship BETWEEN rows, so no row can declare it and it
1619
+ * has to live in code; it lives HERE, in the one place the resolver
1620
+ * reads, rather than inside a getter, where only that getter would
1621
+ * have been right and every other reader would have double-counted.
1622
+ *
1623
+ * The synaptic-vs-wired legality conflict remains the clinic's job.
1384
1624
  */
1385
- augBonus(kind) {
1625
+ chromeThatCounts() {
1386
1626
  if (this.augmentations.size === 0)
1387
- return 0;
1627
+ return [];
1388
1628
  const has = (k) => this.augmentations.has(k);
1389
- // The reflex family is the one hand-resolved case: the tiers
1390
- // SUPERSEDE each other (III beats II beats I) instead of stacking,
1391
- // with a synaptic booster riding on top as bioware. Everything else
1392
- // is data -- augmentations.ts declares its own dice (IAugmentation
1393
- // .bonuses), so new chrome never touches this method again.
1394
- const reflexFamily = new Set(['wired-reflexes-1', 'wired-reflexes-2', 'wired-reflexes-3', 'synaptic-booster']);
1395
- const reflexes = (has('wired-reflexes-3') ? 3 : has('wired-reflexes-2') ? 2 : has('wired-reflexes-1') ? 1 : 0)
1396
- + (has('synaptic-booster') ? 1 : 0);
1397
- let total = (kind === 'initiative' || kind === 'defense') ? reflexes : 0;
1398
- for (const key of this.augmentations) {
1399
- if (reflexFamily.has(key))
1400
- continue; // resolved above
1401
- total += getAug(key)?.bonuses?.[kind] ?? 0;
1402
- }
1403
- return total;
1629
+ const best = has('wired-reflexes-3') ? 'wired-reflexes-3'
1630
+ : has('wired-reflexes-2') ? 'wired-reflexes-2'
1631
+ : has('wired-reflexes-1') ? 'wired-reflexes-1' : undefined;
1632
+ const superseded = new Set(['wired-reflexes-1', 'wired-reflexes-2', 'wired-reflexes-3'].filter(k => k !== best));
1633
+ return [...this.augmentations].filter(k => !superseded.has(k));
1634
+ }
1635
+ /**
1636
+ * The chrome lane, by pool. A LANE READER, like gearBonus above.
1637
+ *
1638
+ * Every pool now reads `bonus()`, with exactly two deliberate
1639
+ * exceptions, both in smartlinkTier and both commented there: that
1640
+ * method asks which TIER of smartlink you have, implant or clip-on,
1641
+ * and only the chrome lane can answer it.
1642
+ */
1643
+ augBonus(kind) {
1644
+ return this.sumEffects('dice', kind, undefined, CHROME_LANES);
1404
1645
  }
1405
1646
  // ==================== Qualities (see qualities.ts) ====================
1406
1647
  // SR5e's traits (p.71-85), baked into the archetype at creation
@@ -1409,6 +1650,43 @@ export class Player extends AbstractPlayer {
1409
1650
  // qualityBonus below, the same single-switch pattern as augBonus.
1410
1651
  /** Installed qualities, by catalog key (see QUALITIES). */
1411
1652
  qualities = new Set();
1653
+ /**
1654
+ * IS THIS SKILL OR ATTRIBUTE FORBIDDEN OUTRIGHT to this character?
1655
+ *
1656
+ * Reads the same lane every other rule reads, so a second quality
1657
+ * (or a piece of chrome, or a metatype trait) that bars something
1658
+ * needs no new reader. A `chosen` forbid resolves through the
1659
+ * holder's pick, and where the pick names a skill GROUP the bar
1660
+ * falls on every member -- which is what Incompetent actually is
1661
+ * (p.81: "all skills in the skill group").
1662
+ */
1663
+ isForbidden(of, name) {
1664
+ const category = of === 'skill' ? 'skills' : 'attributes';
1665
+ for (const { effect, slug } of this.applyingEffects()) {
1666
+ if (effect.type !== 'forbid' || effect.category !== category)
1667
+ continue;
1668
+ const target = effect.property === 'chosen' ? this.picks.get(slug) : effect.property;
1669
+ if (target === undefined)
1670
+ continue;
1671
+ if (target === name)
1672
+ return true;
1673
+ if (of === 'skill' && skillsInGroup(target).includes(name))
1674
+ return true;
1675
+ }
1676
+ return false;
1677
+ }
1678
+ /**
1679
+ * Incompetent (p.81) names ONE skill group, and this is the only place
1680
+ * that knows how to ask which. It was three separate reads of
1681
+ * `hasQuality(`incompetent-${skill}`)` -- the slug convention -- and a
1682
+ * fourth that counted slugs by prefix for notoriety. The quality is
1683
+ * now 'incompetent' with a pick, and a legacy slug resolves to the
1684
+ * same thing on load (qualities.ts LEGACY_CHOICE_KEYS), so this
1685
+ * answers the same for an old save and a new one.
1686
+ */
1687
+ isIncompetentAt(skill) {
1688
+ return this.isForbidden('skill', skill);
1689
+ }
1412
1690
  hasQuality(key) {
1413
1691
  return this.qualities.has(key);
1414
1692
  }
@@ -1417,6 +1695,23 @@ export class Player extends AbstractPlayer {
1417
1695
  * the only quality that edits an attribute (RAW raises the Edge ceiling;
1418
1696
  * with no attribute point-buy here, the point lands directly).
1419
1697
  */
1698
+ /**
1699
+ * WHAT THE HOLDER PICKED, per ROW that demands a pick (ICatalogChoice):
1700
+ * slug -> the skill or attribute named.
1701
+ *
1702
+ * KEYED BY SLUG, NOT BY QUALITY (mjmcee, 2026-09-04). A Hardwire is an
1703
+ * augmentation that gives +2 to a chosen skill and asks exactly the
1704
+ * question Aptitude asks; a map that only qualities could reach would
1705
+ * mean a second map for chrome and a third for spells. One map, one
1706
+ * picker, one payload field.
1707
+ */
1708
+ picks = new Map();
1709
+ /**
1710
+ * The former name for `picks`, back when only qualities could ask.
1711
+ * Kept as the SAME Map -- not a copy -- so a save written or read
1712
+ * through the old name still lands in one place.
1713
+ */
1714
+ get qualityChoices() { return this.picks; }
1420
1715
  /** Rating per rated quality (qualities.ts maxRating); absent = 1. */
1421
1716
  qualityRatings = new Map();
1422
1717
  /** The rating held, 0 when the quality is not held; a flat quality reads 1. */
@@ -1429,6 +1724,13 @@ export class Player extends AbstractPlayer {
1429
1724
  // A legacy "<key>-1" resolves to the rated key at 1 (qualities.ts).
1430
1725
  const resolved = resolveQualityKey(rawKey);
1431
1726
  const key = resolved.key;
1727
+ // A legacy slug carries its pick with it (qualities.ts
1728
+ // LEGACY_CHOICE_KEYS): incompetent-hacking IS incompetent, having
1729
+ // named hacking. Recorded before anything reads it, and never
1730
+ // overwriting a pick the payload supplied explicitly.
1731
+ if (resolved.pick !== undefined && !this.picks.has(key)) {
1732
+ this.picks.set(key, resolved.pick);
1733
+ }
1432
1734
  const q = getQuality(key);
1433
1735
  if (q?.maxRating) {
1434
1736
  const r = Math.max(1, Math.min(q.maxRating, Math.floor(rating ?? resolved.rating)));
@@ -1437,16 +1739,78 @@ export class Player extends AbstractPlayer {
1437
1739
  if (this.qualities.has(key))
1438
1740
  return;
1439
1741
  this.qualities.add(key);
1440
- if (key === 'lucky') {
1441
- this.edge += 1;
1442
- this.edgeRemaining += 1;
1742
+ this.applyQualityAttributes(q);
1743
+ }
1744
+ /**
1745
+ * A quality that MOVES AN ATTRIBUTE does it once, here, at install.
1746
+ *
1747
+ * Unlike a dice effect -- which qualityBonus re-reads on every roll
1748
+ * -- an attribute is a stored number, so it is edited rather than
1749
+ * summed. Lucky (p.76) is the long-standing case: RAW it raises the
1750
+ * Edge CEILING, and with no attribute point-buy here the point lands
1751
+ * directly. It used to be an `if (key === 'lucky')` in addQuality;
1752
+ * it is data now, which is what let Ugly and Doesn't Care (Cutting
1753
+ * Aces p.152-153) arrive as data too instead of as a second `if`.
1754
+ *
1755
+ * The floor is 1: no quality reduces an attribute below the minimum
1756
+ * a metahuman can have, and Ugly and Doesn't Care says so in as many
1757
+ * words ("minimum 1").
1758
+ */
1759
+ applyQualityAttributes(q) {
1760
+ for (const e of q?.effects ?? []) {
1761
+ if (e.type !== 'attribute' || e.category !== 'attributes')
1762
+ continue;
1763
+ const by = e.value;
1764
+ switch (e.property) {
1765
+ case 'edge':
1766
+ // Edge is two numbers: the ceiling and what is left in the
1767
+ // pool tonight. A quality that raises one raises both, or the
1768
+ // point exists on the sheet and cannot be spent.
1769
+ this.edge = Math.max(1, this.edge + by);
1770
+ this.edgeRemaining = Math.max(0, this.edgeRemaining + by);
1771
+ break;
1772
+ case 'body':
1773
+ this.body = Math.max(1, this.body + by);
1774
+ break;
1775
+ case 'agility':
1776
+ this.agility = Math.max(1, this.agility + by);
1777
+ break;
1778
+ case 'reaction':
1779
+ this.reaction = Math.max(1, this.reaction + by);
1780
+ break;
1781
+ case 'strength':
1782
+ this.strength = Math.max(1, this.strength + by);
1783
+ break;
1784
+ case 'willpower':
1785
+ this.willpower = Math.max(1, this.willpower + by);
1786
+ break;
1787
+ case 'logic':
1788
+ this.logic = Math.max(1, this.logic + by);
1789
+ break;
1790
+ case 'intuition':
1791
+ this.intuition = Math.max(1, this.intuition + by);
1792
+ break;
1793
+ case 'charisma':
1794
+ this.charisma = Math.max(1, this.charisma + by);
1795
+ break;
1796
+ default: break; // magic, resonance, reach, armor-bonus: no quality moves them
1797
+ }
1443
1798
  }
1444
1799
  }
1445
1800
  /**
1446
1801
  * The single tap every pool reads qualities through (cf. augBonus).
1447
1802
  *
1803
+ * DATA, NOT A SWITCH (2026-09-04). Every number below used to be a
1804
+ * `case` here; each one now lives on its own quality in qualities.ts
1805
+ * as an `effects` entry, in the same vocabulary gear and chrome
1806
+ * already speak. A quality that adds dice therefore needs no edit to
1807
+ * this method -- which is the property that matters, because the
1808
+ * cost of the old shape was that the catalog could only ever print a
1809
+ * quality's PROSE while its actual numbers lived somewhere a reader
1810
+ * could not reach.
1811
+ *
1448
1812
  * `perception` and `social` mirror the augBonus kinds of the same
1449
- * names, and are read at the same call sites, so a quality and a piece
1813
+ * names and are read at the same call sites, so a quality and a piece
1450
1814
  * of chrome that do the same thing are added the same way.
1451
1815
  *
1452
1816
  * `sleaze` and `attack` are the two Matrix ACTIONS this engine names
@@ -1455,25 +1819,51 @@ export class Player extends AbstractPlayer {
1455
1819
  * a mark IS one of those two. They exist because Codeslinger and
1456
1820
  * Codeblock (p.72 / p.80) modify ONE chosen Matrix action rather than
1457
1821
  * Matrix work in general, so a `hacking`-wide bonus would be a
1458
- * different quality wearing the name.
1822
+ * different quality wearing the name. In the DATA they are spelled
1823
+ * `matrix-attack` / `matrix-sleaze`: EffectPool already spends the
1824
+ * bare word `attack` on the COMBAT attack pool, and a quality that
1825
+ * fired in a firefight because it shared a word with a Matrix action
1826
+ * would be a bug nobody would look for.
1459
1827
  */
1460
1828
  qualityBonus(kind) {
1461
1829
  if (this.qualities.size === 0)
1462
1830
  return 0;
1463
- const has = (k) => this.qualities.has(k);
1464
- switch (kind) {
1465
- case 'sneak': return (has('catlike') ? 2 : 0) + (has('distinctive-style') ? -2 : 0);
1466
- case 'athletics': return has('natural-athlete') ? 2 : 0;
1467
- case 'matrix': return has('simsense-vertigo') ? -2 : 0;
1468
- case 'hacking': return (has('analytical-mind') ? 2 : 0) + this.qualityBonus('matrix');
1469
- case 'piloting': return has('juryrigger') ? 2 : 0;
1470
- case 'soak': return has('toughness') ? 1 : 0;
1471
- case 'healing': return has('quick-healer') ? 2 : 0;
1472
- case 'perception': return has('perceptive') ? 1 : 0;
1473
- case 'social': return (has('trustworthy') ? 1 : 0) + (has('uncouth') ? -2 : 0);
1474
- case 'sleaze': return has('codeslinger-hack-on-the-fly') ? 2 : 0;
1475
- case 'attack': return has('codeblock-brute-force') ? -2 : 0;
1476
- }
1831
+ const property = kind === 'sleaze' ? 'matrix-sleaze' : kind === 'attack' ? 'matrix-attack' : kind;
1832
+ let total = this.sumQualityEffects(property);
1833
+ // The one FAN-IN, and it is a rule rather than a number: hacking is
1834
+ // Matrix work, so anything that taxes the Matrix taxes hacking too
1835
+ // (Simsense Vertigo, p.83). Kept here because it is a relationship
1836
+ // between two pools, which no single quality can declare.
1837
+ if (kind === 'hacking')
1838
+ total += this.qualityBonus('matrix');
1839
+ return total;
1840
+ }
1841
+ /**
1842
+ * Dice the QUALITIES add to one pool or skill. Deliberately its own
1843
+ * summation rather than a source inside applyingEffects(): the pools
1844
+ * are read as `augBonus(k) + qualityBonus(k)` at call sites that do
1845
+ * NOT also add gearBonus(k), and folding qualities into the shared
1846
+ * effect walk would have made every site that DOES add gearBonus
1847
+ * count a quality twice, silently, in a number nobody prints.
1848
+ */
1849
+ /**
1850
+ * How much further this character may raise one skill or attribute
1851
+ * than anyone else -- Aptitude's chosen skill (p.72), Exceptional
1852
+ * Attribute's chosen attribute (p.73-74).
1853
+ *
1854
+ * The book states absolute numbers against ITS ceilings (a skill at
1855
+ * 7 rather than 6 at creation, 13 rather than 12 after). This engine
1856
+ * caps lower than the book does, so what carries across is the DELTA
1857
+ * -- one step further than anyone else gets -- applied to whatever
1858
+ * ceiling the engine is enforcing. Writing the book's 13 over a house
1859
+ * cap of 7 would not be more faithful, it would just be a bigger
1860
+ * number in the wrong system.
1861
+ */
1862
+ qualityCap(of, name) {
1863
+ return this.capBonus(of, name);
1864
+ }
1865
+ sumQualityEffects(property) {
1866
+ return this.sumEffects('dice', property, undefined, QUALITY_LANES);
1477
1867
  }
1478
1868
  // First Impression (p.74): the first social play of each job rolls +2.
1479
1869
  // Consumed like the edge boost and the liquid-courage die; re-armed
@@ -1576,7 +1966,7 @@ export class Player extends AbstractPlayer {
1576
1966
  get qualityNotoriety() {
1577
1967
  const plus = ['bad-luck', 'combat-paralysis', 'sinner-criminal']
1578
1968
  .filter(k => this.qualities.has(k)).length
1579
- + [...this.qualities].filter(k => k.startsWith('incompetent-')).length;
1969
+ + (this.qualities.has('incompetent') ? 1 : 0);
1580
1970
  const minus = ['first-impression', 'lucky'].filter(k => this.qualities.has(k)).length;
1581
1971
  // BAD REP CARRIES ITS OWN NUMBER (p.79): "starts play with 3 points
1582
1972
  // of Notoriety" -- not one entry on p.368's list, which is why it is
@@ -1625,7 +2015,7 @@ export class Player extends AbstractPlayer {
1625
2015
  skillRating(name) {
1626
2016
  // Incompetent (p.81): the skill isn't low, it's ABSENT -- pinned to 0
1627
2017
  // no matter what the sheet or the fallback would say.
1628
- if (this.qualities.has(`incompetent-${name}`))
2018
+ if (this.isForbidden('skill', name))
1629
2019
  return 0;
1630
2020
  const explicit = this.skills[name];
1631
2021
  if (typeof explicit === 'number')
@@ -2368,7 +2758,7 @@ export class Player extends AbstractPlayer {
2368
2758
  // A damage compensator (p.461) ignores boxes outright -- chrome that
2369
2759
  // stacks with High Pain Tolerance, unlike the pain editor's tier skip.
2370
2760
  const ignore = (!painEditor && this.qualities.has('high-pain-tolerance') ? 1 : 0)
2371
- + this.augBonus('wound-ignore');
2761
+ + this.bonus('wound-ignore');
2372
2762
  const dmg = Math.max(0, this._damageTaken - ignore);
2373
2763
  const spill = Math.max(0, ignore - this._damageTaken);
2374
2764
  const stun = Math.max(0, this._stunTaken - spill);
@@ -2556,7 +2946,7 @@ export class Player extends AbstractPlayer {
2556
2946
  armorPen: implant.ap,
2557
2947
  };
2558
2948
  }
2559
- return { name: 'bare fists', damage: this.strength + this.augBonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
2949
+ return { name: 'bare fists', damage: this.strength + this.bonus('unarmed-damage'), poolBonus: 0, armorPen: 0 };
2560
2950
  }
2561
2951
  const base = weapon.weaponDv + (weapon.dvAttribute === 'strength' ? this.strength : 0);
2562
2952
  return { name: weapon.name, damage: base, poolBonus: 0, armorPen: weapon.armorPen };
@@ -2658,7 +3048,7 @@ export class Player extends AbstractPlayer {
2658
3048
  // GUNNERY (canon: the vehicle-weapon skill) -- best-of with
2659
3049
  // piloting so pre-gunnery riggers lose nothing.
2660
3050
  const droneSkill = Math.max(this.skillRating('piloting'), this.skillRating('gunnery'));
2661
- return Math.max(0, this.logic + droneSkill + this.augBonus('piloting') + this.qualityBonus('piloting') + hot + this.woundModifier - this.sustainingPenalty);
3051
+ return Math.max(0, this.logic + droneSkill + this.bonus('piloting') + hot + this.woundModifier - this.sustainingPenalty);
2662
3052
  }
2663
3053
  if (this.plane === 'matrix') {
2664
3054
  const hot = this.simMode === 'hot' ? 2 : 0;
@@ -2674,7 +3064,7 @@ export class Player extends AbstractPlayer {
2674
3064
  // for a technomancer, Resonance/2. A living persona's Attack is
2675
3065
  // Charisma (p.251) and rides in the bracket too. See
2676
3066
  // getAttackLimit, and utilities/dice.ts for what a limit does.
2677
- return Math.max(0, this.logic + matrixSkill + hot + this.augBonus('matrix') + this.qualityBonus('hacking') + (this.hasMetamagic('attack-upgrade') ? 1 : 0) + this.formMatrixBonus + this.woundModifier - this.sustainingPenalty);
3067
+ return Math.max(0, this.logic + matrixSkill + hot + this.bonus('hacking') + (this.hasMetamagic('attack-upgrade') ? 1 : 0) + this.formMatrixBonus + this.woundModifier - this.sustainingPenalty);
2678
3068
  }
2679
3069
  if (this.plane === 'astral') {
2680
3070
  // ASTRAL COMBAT IS `Astral Combat + Willpower [Astral]` (p.315),
@@ -2701,7 +3091,6 @@ export class Player extends AbstractPlayer {
2701
3091
  // Power Points ride every strike. Chosen powers: Improved Ability
2702
3092
  // (Combat) is the striking bonus. See adeptAttackBonus.
2703
3093
  // Power Point (initiation, adepts): one more point of magic-as-muscle.
2704
- const adeptBonus = this.adeptAttackBonus();
2705
3094
  // THE WIRELESS SMARTGUN BONUS IS DICE, NOT ACCURACY (p.433), and it
2706
3095
  // has TWO TIERS this engine only had one of: "+1 if you're using
2707
3096
  // gear with a smartlink, +2 if you're using an augmentation for
@@ -2721,6 +3110,8 @@ export class Player extends AbstractPlayer {
2721
3110
  // bonus is the smartgun system talking to the smartlink.
2722
3111
  const drawnGun = this.weaponDrawn ? this.getCarriedWeapon() : null;
2723
3112
  const smartlink = drawnGun?.isSmartgun && FIREARM_SKILLS.includes(this.activeCombatSkill())
3113
+ // Chrome again, and for the same reason: the tier is already
3114
+ // decided above, and this branch is the implant's own dice.
2724
3115
  ? (this.smartlinkTier() === 'gear' ? 1 : this.augBonus('firearms'))
2725
3116
  : 0;
2726
3117
  // RUNNING COSTS YOU (p.162 Running Modifiers): -2 dice on everything
@@ -2731,7 +3122,15 @@ export class Player extends AbstractPlayer {
2731
3122
  // sprinted across a room to reach someone arrived shooting exactly
2732
3123
  // as straight as one who strolled -- and the movement budget's whole
2733
3124
  // point is that ground has a price.
2734
- return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill()) + adeptBonus + this.getWeaponProfile().poolBonus + this.augBonus('attack') + this.gearBonus('attack') + this.gearBonus(this.activeCombatSkill()) + smartlink + this.woundModifier - this.sustainingPenalty - this.runningPenalty - this.encumbrancePenalty);
3125
+ // ONE accessor for every source of attack dice: chrome, gear, the
3126
+ // gear that names the SKILL rather than the pool (a smartlink says
3127
+ // `firearms`), qualities, adept powers and the Power Point metamagic.
3128
+ // This line used to add five of those by hand and a sixth was always
3129
+ // one refactor away from being forgotten.
3130
+ return Math.max(0, this.agility + this.skillRating(this.activeCombatSkill())
3131
+ + this.adeptCompressedBonus() + this.getWeaponProfile().poolBonus
3132
+ + this.bonus('attack', { skill: this.activeCombatSkill() })
3133
+ + smartlink + this.woundModifier - this.sustainingPenalty - this.runningPenalty - this.encumbrancePenalty);
2735
3134
  }
2736
3135
  /**
2737
3136
  * The subdual pool: bare-hands work regardless of what's in the other
@@ -2739,8 +3138,8 @@ export class Player extends AbstractPlayer {
2739
3138
  * See subdue.ts.
2740
3139
  */
2741
3140
  getUnarmedAttackPool() {
2742
- const adeptBonus = this.adeptAttackBonus();
2743
- return Math.max(0, this.agility + this.skillRating('unarmed') + adeptBonus + this.augBonus('attack') + this.woundModifier - this.sustainingPenalty);
3141
+ return Math.max(0, this.agility + this.skillRating('unarmed') + this.adeptCompressedBonus()
3142
+ + this.bonus('attack', { skill: 'unarmed' }) + this.woundModifier - this.sustainingPenalty);
2744
3143
  }
2745
3144
  getDefensePool() {
2746
3145
  // Reaction plus HALF intuition (rounded down) -- full SR
@@ -2750,7 +3149,7 @@ export class Player extends AbstractPlayer {
2750
3149
  // reflex attribute but keep the same shape. Jumped-in: the rigger's
2751
3150
  // own reflexes drive the airframe, control rig sharpening handling.
2752
3151
  if (this.plane === 'drone') {
2753
- return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + this.augBonus('piloting') + this.qualityBonus('piloting') + this.woundModifier - this.sustainingPenalty);
3152
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + this.bonus('piloting') + this.woundModifier - this.sustainingPenalty);
2754
3153
  }
2755
3154
  if (this.plane === 'matrix') {
2756
3155
  const hot = this.simMode === 'hot' ? 1 : 0;
@@ -2767,7 +3166,7 @@ export class Player extends AbstractPlayer {
2767
3166
  // matrixAttribute is the persona's Firewall WHOLE: the deck's for a
2768
3167
  // decker, Willpower for a living persona (p.251), and a
2769
3168
  // commlink's Device Rating for anyone else (p.222/p.234).
2770
- return Math.max(0, this.intuition + this.matrixAttribute('firewall') + hot + this.augBonus('matrix') + this.qualityBonus('matrix') + this.formDefenseBonus + this.woundModifier - this.sustainingPenalty);
3169
+ return Math.max(0, this.intuition + this.matrixAttribute('firewall') + hot + this.bonus('matrix') + this.formDefenseBonus + this.woundModifier - this.sustainingPenalty);
2771
3170
  }
2772
3171
  if (this.plane === 'astral') {
2773
3172
  return Math.max(0, this.intuition + Math.floor(this.magic / 2) + this.woundModifier - this.sustainingPenalty);
@@ -2776,9 +3175,9 @@ export class Player extends AbstractPlayer {
2776
3175
  // Wired reflexes read it faster still. Locked in someone's grapple
2777
3176
  // (p.195: a subdued character counts as PRONE against attacks): -2.
2778
3177
  // Improved Reflexes rides Reaction itself (p.309), so it lands here too.
2779
- const adeptBonus = this.adeptDefenseBonus() + this.adeptReactionBonus();
2780
3178
  const grappled = this.grappledBy ? 2 : 0;
2781
- return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + adeptBonus + this.augBonus('defense') + this.gearBonus('defense') + this.woundModifier - this.sustainingPenalty - grappled - this.encumbrancePenalty);
3179
+ return Math.max(0, this.reaction + Math.floor(this.intuition / 2) + this.adeptCompressedBonus(false)
3180
+ + this.bonus('defense') + this.woundModifier - this.sustainingPenalty - grappled - this.encumbrancePenalty);
2782
3181
  }
2783
3182
  getSoakPool() {
2784
3183
  // Matrix: firewall -- Logic plus the DECK's quality (your deck is your
@@ -2805,13 +3204,15 @@ export class Player extends AbstractPlayer {
2805
3204
  }
2806
3205
  // A loaded Armor program hardens it further (+2, p.245 cited).
2807
3206
  const deck = this.activeDeck ?? this.getCyberdeck();
2808
- return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + this.gearBonus('matrix-soak'));
3207
+ return Math.max(0, (deck?.deviceRating ?? 0) + this.matrixAttribute('firewall') + this.bonus('matrix-soak'));
2809
3208
  }
2810
3209
  if (this.plane === 'astral') {
2811
3210
  return Math.max(0, this.willpower);
2812
3211
  }
2813
3212
  // Mystic Armor (p.309): the adept power is armor that was never worn.
2814
- return Math.max(0, this.body + this.getArmorValue() + this._sustainedArmor + this.adeptArmorBonus() + this.augBonus('soak') + this.qualityBonus('soak') + this.gearBonus('soak'));
3213
+ // The armor spell arrives through the sustained lane now, in
3214
+ // bonus('soak'), rather than as a term of its own.
3215
+ return Math.max(0, this.body + this.getArmorValue() + this.adeptArmorBonus() + this.bonus('soak'));
2815
3216
  }
2816
3217
  /**
2817
3218
  * THE INITIATIVE ATTRIBUTE -- what the dice are added TO (p.159,
@@ -2929,7 +3330,7 @@ export class Player extends AbstractPlayer {
2929
3330
  // of bug to have reported: the archetype it works for never sees
2930
3331
  // it, and the one it fails for has nothing to compare against.
2931
3332
  // matrixAttribute answers both cases from one place.
2932
- return Math.max(0, this.matrixAttribute('dataProcessing') + this.intuition + this.augBonus('matrix') + this.qualityBonus('matrix') + this.woundModifier);
3333
+ return Math.max(0, this.matrixAttribute('dataProcessing') + this.intuition + this.bonus('matrix') + this.woundModifier);
2933
3334
  }
2934
3335
  if (this.plane === 'astral') {
2935
3336
  // Intuition x 2 (p.159 chart, p.314 astral attributes).
@@ -2950,7 +3351,9 @@ export class Player extends AbstractPlayer {
2950
3351
  // cannot be computed from what is stored. The flat +2 is kept as the
2951
3352
  // engine's standing two-hit stand-in rather than inventing a dice
2952
3353
  // grant the save file cannot justify. Reported, not guessed.
2953
- const spellReflexes = this.sustainedReflexes ? 2 : 0;
3354
+ // Increase Reflexes declares its own +2 on the score now
3355
+ // (spells.ts), read through the initiative arm.
3356
+ const spellReflexes = this.initiativeBonus('score');
2954
3357
  // Improved Reflexes (p.309): +level Reaction, and Reaction feeds the
2955
3358
  // initiative score (p.160) -- the adept's speed is body speed.
2956
3359
  return Math.max(0, this.reaction + this.adeptReactionBonus() + this.intuition + arBonus + spellReflexes + this.woundModifier);
@@ -2992,7 +3395,9 @@ export class Player extends AbstractPlayer {
2992
3395
  // Improved Reflexes (p.309): "+1 Initiative Die" per level -- body
2993
3396
  // speed, so it rides the meat line beside the chrome, and the 5D6
2994
3397
  // ceiling in rollInitiativeScore governs it like every other source.
2995
- return Math.max(1, 1 + this.augBonus('initiative') + this.adeptReactionBonus());
3398
+ // Chrome initiative and Improved Reflexes both declare `initiative`,
3399
+ // so the lane carries the pair.
3400
+ return Math.max(1, 1 + this.bonus('initiative'));
2996
3401
  }
2997
3402
  /**
2998
3403
  * If an edge boost is armed (see commands/edge.ts), consume it: returns
@@ -3140,30 +3545,47 @@ export class Player extends AbstractPlayer {
3140
3545
  * Combat Sense. The Power Point metamagic stays what it always was --
3141
3546
  * a point of magic-as-muscle on the STRIKING side only.
3142
3547
  */
3548
+ /**
3549
+ * The COMPRESSED adept's flat bonus, or zero for anyone who chose
3550
+ * powers. Split out of adeptAttackBonus so the combat pools can read
3551
+ * `bonus()` for every other source without counting the chosen powers
3552
+ * twice -- they arrive through the adept lane, and this is the only
3553
+ * part of the adept that never became data.
3554
+ */
3555
+ adeptCompressedBonus(withMetamagic = true) {
3556
+ return this.adeptPowers === undefined ? this.adeptPoolBonus(withMetamagic) : 0;
3557
+ }
3143
3558
  adeptAttackBonus() {
3559
+ // The COMPRESSED model is the one thing here that is not data: a save
3560
+ // written before powers could be chosen has no `adeptPowers` at all
3561
+ // and gets floor(pp/2) as a flat pool bonus. It is a legacy shape,
3562
+ // not a rule, and the two models never stack.
3144
3563
  if (this.adeptPowers === undefined)
3145
3564
  return this.adeptPoolBonus();
3146
- return (this.adeptPowers['improved-ability-combat'] ?? 0)
3147
- + (this.hasMetamagic('power-point') ? 1 : 0);
3565
+ // Improved Ability (Combat) from the adept lane, the Power Point
3566
+ // metamagic from the initiate lane -- both declare `attack`, and the
3567
+ // resolver adds them without either being named here.
3568
+ return this.sumEffects('dice', 'attack', undefined, ART_LANES);
3148
3569
  }
3149
3570
  adeptDefenseBonus() {
3150
3571
  if (this.adeptPowers === undefined)
3151
3572
  return this.adeptPoolBonus(false);
3152
- return this.adeptPowers['combat-sense'] ?? 0;
3573
+ // Combat Sense AND Improved Reflexes both land on defence (p.308-309).
3574
+ return this.sumEffects('dice', 'defense', undefined, ADEPT_LANES);
3153
3575
  }
3154
3576
  /** Mystic Armor (p.309): +1 Armor per level. Zero under the compressed
3155
3577
  * model -- the flat pool bonus never granted armor, so none is owed. */
3156
3578
  adeptArmorBonus() {
3157
- return this.adeptPowers?.['mystic-armor'] ?? 0;
3579
+ return this.sumEffects('attribute', 'armor-bonus', undefined, ADEPT_LANES);
3158
3580
  }
3159
- /** Improved Reflexes (p.309): +level Reaction, read by the defence and
3160
- * initiative math. Zero under the compressed model. */
3581
+ /** Improved Reflexes (p.309): the initiative dice it adds. Zero under
3582
+ * the compressed model. */
3161
3583
  adeptReactionBonus() {
3162
- return this.adeptPowers?.['improved-reflexes'] ?? 0;
3584
+ return this.sumEffects('dice', 'initiative', undefined, ADEPT_LANES);
3163
3585
  }
3164
3586
  /** Killing Hands (p.309): unarmed strikes deal PHYSICAL damage. */
3165
3587
  hasKillingHands() {
3166
- return (this.adeptPowers?.['killing-hands'] ?? 0) > 0;
3588
+ return this.sumEffects('flag', 'killing-hands', undefined, ADEPT_LANES) > 0;
3167
3589
  }
3168
3590
  isAwakened() {
3169
3591
  return this.magic > 0;
@@ -3443,7 +3865,7 @@ export class Player extends AbstractPlayer {
3443
3865
  getMagicPool() {
3444
3866
  // A focus adds its Force to the tests it is bonded for (p.318): the
3445
3867
  // row's effects, scaled by Force (catalog v13).
3446
- const focusBonus = this.gearBonus('spellcasting');
3868
+ const focusBonus = this.bonus('spellcasting');
3447
3869
  // SR-proper when the SPELLCASTING skill is rated: Magic + Spellcasting
3448
3870
  // for the Awakened, base + skill for a book-caster. Unrated falls back
3449
3871
  // to the legacy shapes (talent max(5, magic+2); scholar's flat 5), so
@@ -3470,9 +3892,18 @@ export class Player extends AbstractPlayer {
3470
3892
  // meat-plane combat pools.
3471
3893
  if (this.adept)
3472
3894
  return false;
3473
- const wanted = spellName.toLowerCase();
3895
+ // THE FORMULA NAMES ITS SPELL, and this reads that name.
3896
+ //
3897
+ // It used to look for the spell name INSIDE the item name, which
3898
+ // could only ever have worked if a formula were called after its
3899
+ // spell. The five that shipped were named after their CATEGORY --
3900
+ // "Combat Spell Formula" -- so the test never once returned true,
3901
+ // and a mage could buy every formula in the shop and learn nothing.
3902
+ // Formulae are one row per spell now, each declaring `teaches`.
3903
+ //
3904
+ // A Grimoire is still the blanket case: a whole book, any spell.
3474
3905
  const carried = this._inventory.getAllItems().some(i => i.category === Category.Grimoire ||
3475
- (i.category === Category.SpellFormula && i.name.toLowerCase().includes(wanted)));
3906
+ (i.category === Category.SpellFormula && i.row?.teaches === spellName));
3476
3907
  if (this.isAwakened()) {
3477
3908
  return this.knownSpells === undefined
3478
3909
  || this.knownSpells.includes(spellName)