@maka/maka-cli 5.122.0 → 5.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/bundle/typescript/package.json +1 -1
  2. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js +12 -8
  3. package/bundle/typescript/src/commands/deploy/ubuntu.sub.cmd.js.map +1 -1
  4. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.d.ts +15 -0
  5. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.js +12 -0
  6. package/bundle/typescript/src/commands/game/sideQuest/adept-powers.js.map +1 -1
  7. package/bundle/typescript/src/commands/game/sideQuest/archetypes.d.ts +10 -0
  8. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js +40 -14
  9. package/bundle/typescript/src/commands/game/sideQuest/archetypes.js.map +1 -1
  10. package/bundle/typescript/src/commands/game/sideQuest/augmentations.d.ts +33 -10
  11. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js +155 -72
  12. package/bundle/typescript/src/commands/game/sideQuest/augmentations.js.map +1 -1
  13. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js +15 -6
  14. package/bundle/typescript/src/commands/game/sideQuest/commands/advance.js.map +1 -1
  15. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js +1 -1
  16. package/bundle/typescript/src/commands/game/sideQuest/commands/ask.js.map +1 -1
  17. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js +1 -1
  18. package/bundle/typescript/src/commands/game/sideQuest/commands/bluff.js.map +1 -1
  19. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js +33 -33
  20. package/bundle/typescript/src/commands/game/sideQuest/commands/cast.js.map +1 -1
  21. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js +1 -1
  22. package/bundle/typescript/src/commands/game/sideQuest/commands/drone.js.map +1 -1
  23. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js +1 -1
  24. package/bundle/typescript/src/commands/game/sideQuest/commands/go.js.map +1 -1
  25. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js +5 -1
  26. package/bundle/typescript/src/commands/game/sideQuest/commands/grapple.js.map +1 -1
  27. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js +6 -6
  28. package/bundle/typescript/src/commands/game/sideQuest/commands/hack.js.map +1 -1
  29. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js +4 -4
  30. package/bundle/typescript/src/commands/game/sideQuest/commands/haggle.js.map +1 -1
  31. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js +1 -1
  32. package/bundle/typescript/src/commands/game/sideQuest/commands/hop.js.map +1 -1
  33. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.d.ts +20 -0
  34. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js +9 -0
  35. package/bundle/typescript/src/commands/game/sideQuest/commands/initiate.js.map +1 -1
  36. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js +7 -3
  37. package/bundle/typescript/src/commands/game/sideQuest/commands/lead.js.map +1 -1
  38. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js +3 -3
  39. package/bundle/typescript/src/commands/game/sideQuest/commands/move.js.map +1 -1
  40. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js +1 -1
  41. package/bundle/typescript/src/commands/game/sideQuest/commands/persuade.js.map +1 -1
  42. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js +3 -3
  43. package/bundle/typescript/src/commands/game/sideQuest/commands/rest.js.map +1 -1
  44. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js +10 -10
  45. package/bundle/typescript/src/commands/game/sideQuest/commands/sheet.js.map +1 -1
  46. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js +1 -1
  47. package/bundle/typescript/src/commands/game/sideQuest/commands/tap.js.map +1 -1
  48. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.d.ts +24 -0
  49. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js +95 -0
  50. package/bundle/typescript/src/commands/game/sideQuest/factories/chargen-factory.js.map +1 -1
  51. package/bundle/typescript/src/commands/game/sideQuest/game.d.ts +45 -1
  52. package/bundle/typescript/src/commands/game/sideQuest/game.js +119 -8
  53. package/bundle/typescript/src/commands/game/sideQuest/game.js.map +1 -1
  54. package/bundle/typescript/src/commands/game/sideQuest/metatypes.d.ts +53 -0
  55. package/bundle/typescript/src/commands/game/sideQuest/metatypes.js +86 -0
  56. package/bundle/typescript/src/commands/game/sideQuest/metatypes.js.map +1 -0
  57. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js +8 -2
  58. package/bundle/typescript/src/commands/game/sideQuest/models/npc.js.map +1 -1
  59. package/bundle/typescript/src/commands/game/sideQuest/models/player.d.ts +215 -18
  60. package/bundle/typescript/src/commands/game/sideQuest/models/player.js +548 -117
  61. package/bundle/typescript/src/commands/game/sideQuest/models/player.js.map +1 -1
  62. package/bundle/typescript/src/commands/game/sideQuest/models/scene.d.ts +2 -1
  63. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js +3 -2
  64. package/bundle/typescript/src/commands/game/sideQuest/models/scene.js.map +1 -1
  65. package/bundle/typescript/src/commands/game/sideQuest/qualities.d.ts +96 -4
  66. package/bundle/typescript/src/commands/game/sideQuest/qualities.js +290 -11
  67. package/bundle/typescript/src/commands/game/sideQuest/qualities.js.map +1 -1
  68. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.d.ts +11 -1
  69. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js +13 -1
  70. package/bundle/typescript/src/commands/game/sideQuest/skill-groups.js.map +1 -1
  71. package/bundle/typescript/src/commands/game/sideQuest/skills.d.ts +17 -0
  72. package/bundle/typescript/src/commands/game/sideQuest/skills.js +23 -0
  73. package/bundle/typescript/src/commands/game/sideQuest/skills.js.map +1 -0
  74. package/bundle/typescript/src/commands/game/sideQuest/spells.d.ts +76 -3
  75. package/bundle/typescript/src/commands/game/sideQuest/spells.js +48 -21
  76. package/bundle/typescript/src/commands/game/sideQuest/spells.js.map +1 -1
  77. package/bundle/typescript/src/commands/game/sideQuest/types/save-file.d.ts +8 -0
  78. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.d.ts +90 -82
  79. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js +277 -10
  80. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog-data.js.map +1 -1
  81. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.d.ts +121 -2
  82. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js +342 -18
  83. package/bundle/typescript/src/commands/game/sideQuest/utilities/catalog.js.map +1 -1
  84. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.d.ts +65 -2
  85. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js +74 -7
  86. package/bundle/typescript/src/commands/game/sideQuest/utilities/cloud-saves.js.map +1 -1
  87. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js +1 -1
  88. package/bundle/typescript/src/commands/game/sideQuest/utilities/combat-exchange.js.map +1 -1
  89. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.d.ts +248 -0
  90. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.js +38 -0
  91. package/bundle/typescript/src/commands/game/sideQuest/utilities/effects.js.map +1 -0
  92. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js +1 -2
  93. package/bundle/typescript/src/commands/game/sideQuest/utilities/first-aid.js.map +1 -1
  94. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js +1 -2
  95. package/bundle/typescript/src/commands/game/sideQuest/utilities/perception.js.map +1 -1
  96. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js +5 -0
  97. package/bundle/typescript/src/commands/game/sideQuest/utilities/persistence.js.map +1 -1
  98. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js +5 -1
  99. package/bundle/typescript/src/commands/game/sideQuest/utilities/planes.js.map +1 -1
  100. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js +22 -12
  101. package/bundle/typescript/src/commands/game/sideQuest/utilities/pregen-worksheet.js.map +1 -1
  102. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js +1 -2
  103. package/bundle/typescript/src/commands/game/sideQuest/utilities/spots.js.map +1 -1
  104. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.d.ts +1 -0
  105. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js +20 -0
  106. package/bundle/typescript/src/commands/game/sideQuest/utilities/web-character-generator.js.map +1 -1
  107. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js +31 -0
  108. package/bundle/typescript/src/commands/game/sideQuest.sub.cmd.js.map +1 -1
  109. package/bundle/typescript/src/commands/login.js +19 -1
  110. package/bundle/typescript/src/commands/login.js.map +1 -1
  111. package/bundle/typescript/src/commands/logout.js +16 -1
  112. package/bundle/typescript/src/commands/logout.js.map +1 -1
  113. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.d.ts +59 -0
  114. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.js +120 -0
  115. package/bundle/typescript/src/tools/guarded-file/guarded-file.class.js.map +1 -0
  116. package/bundle/typescript/src/tools/mpi/mpi.class.d.ts +16 -0
  117. package/bundle/typescript/src/tools/mpi/mpi.class.js +25 -7
  118. package/bundle/typescript/src/tools/mpi/mpi.class.js.map +1 -1
  119. package/package.json +1 -1
@@ -19,17 +19,22 @@ export const QUALITIES = [
19
19
  // ------------------------------ Positive ------------------------------
20
20
  {
21
21
  key: 'toughness', name: 'Toughness', kind: 'positive', karma: 9, page: 'p.77',
22
+ family: 'pool',
22
23
  effect: '+1 die on every damage soak roll',
24
+ effects: [{ type: 'dice', category: 'pools', property: 'soak', value: 1 }],
23
25
  },
24
26
  {
25
27
  // RAW: ignore one box per rating when counting wound modifiers, and it
26
28
  // cannot combine with a pain editor (woundModifier enforces that).
27
29
  key: 'high-pain-tolerance', name: 'High Pain Tolerance', kind: 'positive', karma: 7, page: 'p.74',
30
+ family: 'engine-rule',
28
31
  effect: 'ignore 1 box of damage when counting wound modifiers',
29
32
  },
30
33
  {
31
34
  key: 'catlike', name: 'Catlike', kind: 'positive', karma: 7, page: 'p.72',
35
+ family: 'pool',
32
36
  effect: '+2 dice on sneaking tests',
37
+ effects: [{ type: 'dice', category: 'pools', property: 'sneak', value: 2 }],
33
38
  },
34
39
  {
35
40
  // THE INVITE (Data Trails p.45-46, Prime Datahaven Membership). You
@@ -46,6 +51,7 @@ export const QUALITIES = [
46
51
  // named people you call, and inventing a group entry to carry two
47
52
  // numbers nothing reads would be a worse lie than the omission.
48
53
  key: 'prime-datahaven-membership', name: 'Prime Datahaven Membership',
54
+ family: 'engine-rule',
49
55
  kind: 'positive', karma: 7, page: 'Data Trails p.45-46',
50
56
  effect: 'JackPoint knows your handle -- the haven answers when you call it',
51
57
  },
@@ -53,38 +59,71 @@ export const QUALITIES = [
53
59
  // RAW: +2 on Running/Gymnastics -- here, the athletics sprint that
54
60
  // breaks off a fight (go.ts) is where that muscle shows.
55
61
  key: 'natural-athlete', name: 'Natural Athlete', kind: 'positive', karma: 7, page: 'p.76',
62
+ family: 'pool',
56
63
  effect: '+2 dice on athletics tests (including the sprint out of a fight)',
64
+ effects: [{ type: 'dice', category: 'pools', property: 'athletics', value: 2 }],
57
65
  },
58
66
  {
59
- // RAW raises the Edge CEILING without granting the point; with no
60
- // point-buy attributes here, the adaptation grants it outright.
67
+ // A CEILING, NOT A POINT -- RAG-checked p.76, which is explicit
68
+ // about the difference: "Lucky allows a character to possess an Edge
69
+ // attribute one point higher than his metatype maximum ... Note that
70
+ // taking this quality does not actually increase the character's
71
+ // current Edge rating, it just allows her the opportunity to do so;
72
+ // the Karma cost for gaining the extra point must still be paid."
73
+ // A human's Edge maximum goes 7 -> 8, and the eighth point is bought
74
+ // at "advance" like any other.
75
+ //
76
+ // THE COMMENT THAT USED TO SIT HERE said "RAW raises the Edge CEILING
77
+ // without granting the point; with no point-buy attributes here, the
78
+ // adaptation grants it outright" -- an honest approximation from the
79
+ // days when nothing in this file could express a ceiling. That excuse
80
+ // expired when `cap` effects arrived, and by then the approximation
81
+ // was costing something real: the web chargen applies the book's
82
+ // rule, so the same archetype built in the wizard and in the CLI
83
+ // disagreed about Edge by one. Reported by the user on 2026-09-04 --
84
+ // "I'm adding Lucky ... I don't see this updating in the chargen".
85
+ // The wizard was right.
86
+ //
87
+ // It is also WHY this quality and Exceptional Attribute exclude each
88
+ // other (p.65-66): they are the same mechanic, and Edge is the one
89
+ // attribute Exceptional Attribute may not name.
61
90
  key: 'lucky', name: 'Lucky', kind: 'positive', karma: 12, page: 'p.76',
62
- effect: '+1 Edge -- luck past your natural ceiling',
91
+ family: 'ceiling',
92
+ excludesQuality: 'exceptional-attribute',
93
+ effect: 'your Edge may run 1 past its natural maximum -- buy the point at "advance"',
94
+ effects: [{ type: 'cap', category: 'attributes', property: 'edge', value: 1 }],
63
95
  },
64
96
  {
65
97
  // RAW: +2 social dice on a first meeting. Meetings aren't tracked
66
98
  // per-NPC, so the adaptation is per JOB: the first social play of
67
99
  // every job (persuade, bluff, haggle) gets the bonus.
68
100
  key: 'first-impression', name: 'First Impression', kind: 'positive', karma: 11, page: 'p.74',
101
+ family: 'engine-rule',
69
102
  effect: '+2 dice on the first social play of every job',
70
103
  },
71
104
  {
72
105
  // RAW: +2 on all Healing Tests -- exactly the first-aid roll (use.ts)
73
106
  // and the rest recovery roll (rest.ts).
74
107
  key: 'quick-healer', name: 'Quick Healer', kind: 'positive', karma: 3, page: 'p.77',
108
+ family: 'pool',
75
109
  effect: '+2 dice on every healing test made on or by you (first aid, rest)',
110
+ effects: [{ type: 'dice', category: 'pools', property: 'healing', value: 2 }],
76
111
  },
77
112
  {
78
113
  // RAW: +2 on Logic tests for pattern analysis / puzzle-cracking --
79
114
  // mapped to the hacking rolls, the game's logic-puzzle work.
80
115
  key: 'analytical-mind', name: 'Analytical Mind', kind: 'positive', karma: 5, page: 'p.72',
116
+ family: 'pool',
81
117
  effect: '+2 dice on hacking tests -- code is just a puzzle with rent',
118
+ effects: [{ type: 'dice', category: 'skills', property: 'hacking', value: 2 }],
82
119
  },
83
120
  {
84
121
  // RAW: +2 juryrigging mechanical/electronic gear -- mapped to the
85
122
  // piloting roll (drone work IS field rigging) and bench deck repair.
86
123
  key: 'juryrigger', name: 'Juryrigger', kind: 'positive', karma: 10, page: 'p.75',
124
+ family: 'pool',
87
125
  effect: '+2 dice on piloting and on bench-repairing a damaged deck',
126
+ effects: [{ type: 'dice', category: 'pools', property: 'piloting', value: 2 }],
88
127
  },
89
128
  {
90
129
  // RAW: +1 on ALL Perception Tests, and the book says "including
@@ -95,7 +134,9 @@ export const QUALITIES = [
95
134
  // and its shape is co-owned with the web chargen, so a second entry
96
135
  // would be inventing a rating system in the key names.
97
136
  key: 'perceptive', name: 'Perceptive', kind: 'positive', karma: 5, page: 'Run Faster p.148',
137
+ family: 'pool',
98
138
  effect: '+1 die on every perception test -- meat, astral or grid',
139
+ effects: [{ type: 'dice', category: 'pools', property: 'perception', value: 1 }],
99
140
  },
100
141
  {
101
142
  // RAW: +1 to every skill in the INFLUENCE group (Etiquette,
@@ -110,7 +151,9 @@ export const QUALITIES = [
110
151
  // social chrome does too -- a bigger change than this entry, made
111
152
  // sideways. The verbs named below are the ones that actually move.
112
153
  key: 'trustworthy', name: 'Trustworthy', kind: 'positive', karma: 15, page: 'Run Faster p.151',
154
+ family: 'pool',
113
155
  effect: '+1 die when you ask, bluff, haggle or persuade -- people believe you',
156
+ effects: [{ type: 'dice', category: 'pools', property: 'social', value: 1 }],
114
157
  },
115
158
  {
116
159
  // RAW: +2 to ONE Matrix action you choose when you take it (p.237's
@@ -123,9 +166,16 @@ export const QUALITIES = [
123
166
  // shipped because it is one the engine actually names: hack.ts logs
124
167
  // the sleaze route as "Hack on the Fly (sleaze)" per p.240, and
125
168
  // mark.ts states that placing a mark IS that same action.
126
- key: 'codeslinger-hack-on-the-fly', name: 'Codeslinger (Hack on the Fly)',
169
+ key: 'codeslinger', name: 'Codeslinger',
170
+ family: 'pool',
127
171
  kind: 'positive', karma: 10, page: 'p.72',
128
- effect: '+2 dice on the quiet route in -- sleaze hacks and sleaze marks',
172
+ effect: '+2 dice on ONE Matrix action you choose -- the route you made your own',
173
+ choice: {
174
+ of: 'matrix-action',
175
+ prompt: 'Which way in did you make your own -- the quiet one or the loud one?',
176
+ from: ['matrix-sleaze', 'matrix-attack'],
177
+ },
178
+ effects: [{ type: 'dice', category: 'pools', property: 'chosen', value: 2 }],
129
179
  },
130
180
  {
131
181
  // GOING FOR ALL THREE AT ONCE (Data Trails p.44, cited on
@@ -136,59 +186,94 @@ export const QUALITIES = [
136
186
  // pool bonus -- a -4 handed to every intrusion would be a different,
137
187
  // much stronger quality than the book's.
138
188
  key: 'go-big-or-go-home', name: 'Go Big or Go Home',
189
+ family: 'engine-rule',
139
190
  kind: 'positive', karma: 6, page: 'Data Trails p.44',
140
191
  effect: 'reaching for all three marks in one action costs -6 dice instead of -10',
141
192
  },
142
193
  // ------------------------------ Negative ------------------------------
143
194
  {
144
195
  key: 'sinner-national', name: 'SINner (National)', kind: 'negative', karma: 5, page: 'p.84',
196
+ family: 'engine-rule',
145
197
  effect: 'a legal SIN with taxes attached: 15% of every job and gig payout withheld',
146
198
  },
147
199
  {
148
200
  key: 'sinner-criminal', name: 'SINner (Criminal)', kind: 'negative', karma: 10, page: 'p.84',
201
+ family: 'engine-rule',
149
202
  effect: 'a felon\'s SIN, broadcast always: 15% of every payout withheld, and the law knows your face',
150
203
  },
151
204
  {
152
205
  key: 'sinner-corp', name: 'SINner (Corporate Limited)', kind: 'negative', karma: 15, page: 'p.84-85',
206
+ family: 'engine-rule',
153
207
  effect: 'a megacorp owns your file: 20% of every payout goes home to the company',
154
208
  },
155
209
  {
156
210
  key: 'low-pain-tolerance', name: 'Low Pain Tolerance', kind: 'negative', karma: 9, page: 'p.82',
211
+ family: 'engine-rule',
157
212
  effect: 'wound modifiers bite every 2 boxes instead of every 3',
158
213
  },
159
214
  {
160
215
  // RAW: first Initiative Score of a combat halved. Mapped to the
161
216
  // opening exchange of any fresh fight (combat-exchange.ts).
162
217
  key: 'combat-paralysis', name: 'Combat Paralysis', kind: 'negative', karma: 12, page: 'p.80',
218
+ family: 'engine-rule',
163
219
  effect: 'you freeze when it starts: initiative halved in a fight\'s opening exchange',
164
220
  },
165
221
  {
166
222
  // RAW: spent Edge can turn on you. Adaptation: burning edge rolls a
167
223
  // die -- on a 1 the point is gone and the boost never arms (edge.ts).
168
224
  key: 'bad-luck', name: 'Bad Luck', kind: 'negative', karma: 12, page: 'p.79',
225
+ family: 'engine-rule',
169
226
  effect: '1 in 6 edge burns curdle: the point is spent, the boost never comes',
170
227
  },
171
228
  {
172
229
  // RAW: a look people remember, easy to describe, easy to identify.
173
230
  // Adaptation: -2 on sneaking tests -- witnesses clock you.
174
231
  key: 'distinctive-style', name: 'Distinctive Style', kind: 'negative', karma: 5, page: 'p.80',
232
+ family: 'pool',
175
233
  effect: 'people remember you: -2 dice on sneaking tests',
234
+ effects: [{ type: 'dice', category: 'pools', property: 'sneak', value: -2 }],
176
235
  },
177
236
  {
178
237
  // RAW: rest may not restore you fully. Adaptation: rest recovery
179
238
  // halved (rest.ts) -- sleep never comes easy.
180
239
  key: 'insomnia', name: 'Insomnia', kind: 'negative', karma: 10, page: 'p.81',
240
+ family: 'engine-rule',
181
241
  effect: 'rest recovers half the stun it should -- sleep never comes easy',
182
242
  },
183
243
  {
184
244
  key: 'simsense-vertigo', name: 'Simsense Vertigo', kind: 'negative', karma: 5, page: 'p.83',
245
+ family: 'pool',
185
246
  effect: '-2 dice on Matrix actions -- the sim makes your inner ear lie',
247
+ effects: [{ type: 'dice', category: 'pools', property: 'matrix', value: -2 }],
186
248
  },
187
249
  {
188
250
  // RAW: an Active skill group you simply cannot have. skillRating
189
251
  // pins it to 0 and advance.ts refuses to train it.
190
- key: 'incompetent-hacking', name: 'Incompetent (Hacking)', kind: 'negative', karma: 5, page: 'p.81',
191
- effect: 'the Matrix bounces off you: hacking is 0 and can never be trained',
252
+ // PER SKILL GROUP, NOT PER SKILL, and RAG-checked after this entry
253
+ // first shipped as a single-skill pick: p.81 applies the quality to
254
+ // "a certain Active skill group" and treats the holder as unaware
255
+ // "for all skills in the skill group". Naming one skill was a
256
+ // deviation I introduced while retiring the old slug -- the slug
257
+ // said hacking, and I carried its narrowness across instead of
258
+ // reading the quality.
259
+ //
260
+ // FORBIDDEN, NOT ZEROED. Unaware (p.131) is "You cannot default the
261
+ // skill" -- so this is a `forbid` effect and not a ceiling, because
262
+ // no number says "absent". A -6 would read as 0 at creation and 6
263
+ // against the career ceiling, and even an absolute 0 would still
264
+ // default at -1 in this engine.
265
+ key: 'incompetent', name: 'Incompetent', kind: 'negative', karma: 5, page: 'p.81',
266
+ choice: {
267
+ of: 'skill-group',
268
+ prompt: 'Which whole set of skills has never taken, however hard anyone tried?',
269
+ },
270
+ effects: [{ type: 'forbid', category: 'skills', property: 'chosen' }],
271
+ // Half-live, and deliberately not tagged inert: advance.ts refuses
272
+ // to train the skill at any price (the "never trained" half of the
273
+ // rule). What is missing is the other half -- nothing forces the
274
+ // rating to 0 at creation, so an archetype could still hand one out.
275
+ family: 'engine-rule',
276
+ effect: 'one whole skill group is closed to you -- you cannot use it, train it, or even default on it',
192
277
  },
193
278
  {
194
279
  // RAW: you start play with 3 points of Notoriety. Read straight by
@@ -196,6 +281,7 @@ export const QUALITIES = [
196
281
  // awareness exactly as earned notoriety does -- the quality does not
197
282
  // need its own arithmetic, it needs the number the book gives it.
198
283
  key: 'bad-rep', name: 'Bad Rep', kind: 'negative', karma: 7, page: 'p.79',
284
+ family: 'engine-rule',
199
285
  effect: 'the street tells stories about you: 3 points of notoriety, before you have earned any',
200
286
  },
201
287
  {
@@ -206,16 +292,25 @@ export const QUALITIES = [
206
292
  // (skillRating already floors at 0 with an unskilled -1 default, so
207
293
  // there is no lower rung to drop to).
208
294
  key: 'uncouth', name: 'Uncouth', kind: 'negative', karma: 14, page: 'p.85-86',
295
+ family: 'pool',
209
296
  effect: '-2 dice when you ask, bluff, haggle or persuade -- you rub people wrong',
297
+ effects: [{ type: 'dice', category: 'pools', property: 'social', value: -2 }],
210
298
  },
211
299
  {
212
300
  // RAW: -2 on ONE Matrix action you choose. Same key-encoded choice
213
301
  // as Codeslinger above, and the same reason. Brute Force is the
214
302
  // action shipped: hack.ts logs the loud route as "Brute Force
215
303
  // (attack)" per p.238, so the engine names it.
216
- key: 'codeblock-brute-force', name: 'Codeblock (Brute Force)',
304
+ key: 'codeblock', name: 'Codeblock',
305
+ family: 'pool',
217
306
  kind: 'negative', karma: 10, page: 'p.80',
218
- effect: '-2 dice on the loud route in -- brute-force hacks and marks',
307
+ effect: '-2 dice on ONE Matrix action you choose -- the one that never sits right',
308
+ choice: {
309
+ of: 'matrix-action',
310
+ prompt: 'Which way in never sits right -- the quiet one or the loud one?',
311
+ from: ['matrix-sleaze', 'matrix-attack'],
312
+ },
313
+ effects: [{ type: 'dice', category: 'pools', property: 'chosen', value: -2 }],
219
314
  },
220
315
  // ================= THE FULL-CATALOG PASS (2026-09-01) =================
221
316
  // The user's report: "the qualities list is severely lacking" -- and it
@@ -230,145 +325,237 @@ export const QUALITIES = [
230
325
  // ------------------------------ Positive ------------------------------
231
326
  {
232
327
  key: 'ambidextrous', name: 'Ambidextrous', kind: 'positive', karma: 4, page: 'p.71',
328
+ inert: 'the engine applies no off-hand penalty (p.178), so there is none to waive',
329
+ family: 'engine-rule',
233
330
  effect: 'either hand works equally well -- no off-hand penalty (p.178)',
234
331
  },
235
332
  {
333
+ // RAG-checked p.72: the chosen skill may be rated 7 at creation
334
+ // (against the usual 6) and may eventually reach 13 (against 12).
335
+ // Both ends are the SAME delta -- one step further than anyone else
336
+ // gets -- so the engine applies +1 to its own ceiling at both ends
337
+ // rather than writing the book's absolute numbers over house ones.
338
+ // Taken once only.
236
339
  key: 'aptitude', name: 'Aptitude', kind: 'positive', karma: 14, page: 'p.72',
340
+ inert: 'the engine path is complete -- the web chargen does not yet ask which skill, and without an answer the ceiling lifts nothing',
341
+ family: 'ceiling',
237
342
  effect: 'one chosen skill may start at 7 and one day reach 13 -- past every limit',
343
+ choice: { of: 'skill', prompt: 'Which skill is the one you were born for?' },
344
+ effects: [{ type: 'cap', category: 'skills', property: 'chosen', value: 1 }],
238
345
  },
239
346
  {
240
347
  key: 'astral-chameleon', name: 'Astral Chameleon', kind: 'positive', karma: 10, page: 'p.72',
348
+ inert: 'no astral signature is recorded, so nothing can fade faster or read fainter',
349
+ family: 'engine-rule',
241
350
  requires: 'awakened',
242
351
  effect: 'your astral signature fades in half the time and reads at -2 to assensing',
243
352
  },
244
353
  {
245
354
  key: 'bilingual', name: 'Bilingual', kind: 'positive', karma: 5, page: 'p.72',
355
+ inert: 'the engine models no languages',
356
+ family: 'narrative',
246
357
  effect: 'a second language as a native tongue, free, from birth',
247
358
  },
248
359
  {
249
360
  key: 'blandness', name: 'Blandness', kind: 'positive', karma: 8, page: 'p.72',
361
+ inert: 'no test carries a threshold this could raise; recall and shadowing are not rolled',
362
+ family: 'threshold',
250
363
  effect: 'forgettable on purpose: +1 threshold to recall or shadow you (not vs magic or Matrix)',
251
364
  },
252
365
  {
253
366
  key: 'double-jointed', name: 'Double-Jointed', kind: 'positive', karma: 6, page: 'p.72',
367
+ family: 'pool',
254
368
  effect: '+2 dice on escape-artist work; squeeze where others cannot',
369
+ effects: [{ type: 'dice', category: 'skills', property: 'escape-artist', value: 2 }],
255
370
  },
256
371
  {
372
+ // RAG-checked p.73-74: +1 to the NATURAL MAXIMUM of one attribute,
373
+ // never Edge (p.65-66 -- Edge is Lucky's job, and the book says you
374
+ // may buy either quality, not both). It does not raise the
375
+ // attribute itself: it moves the ceiling and you still pay to climb.
257
376
  key: 'exceptional-attribute', name: 'Exceptional Attribute', kind: 'positive', karma: 14, page: 'p.72',
377
+ inert: 'the engine path is complete -- the web chargen does not yet ask which attribute, and without an answer the ceiling lifts nothing',
378
+ family: 'ceiling',
258
379
  effect: 'one attribute may run 1 above its natural maximum (never Edge -- that is Lucky)',
380
+ choice: {
381
+ of: 'attribute',
382
+ prompt: 'Which attribute runs past the human ceiling?',
383
+ excludes: ['edge'],
384
+ },
385
+ excludesQuality: 'lucky',
386
+ effects: [{ type: 'cap', category: 'attributes', property: 'chosen', value: 1 }],
259
387
  },
260
388
  {
261
389
  key: 'focused-concentration', name: 'Focused Concentration',
390
+ family: 'engine-rule',
262
391
  kind: 'positive', karma: 4, maxRating: 6, page: 'p.74', requires: 'caster-or-technomancer',
263
392
  effect: 'sustain one spell of Force up to your rating with no sustaining penalty',
264
393
  },
265
394
  {
266
395
  key: 'gearhead', name: 'Gearhead', kind: 'positive', karma: 11, page: 'p.74',
396
+ inert: 'the piloting roll has no notion of a hard maneuver, and no speed or Handling to boost',
397
+ family: 'pool',
267
398
  effect: 'behind any wheel or stick: +2 dice on hard maneuvers, +20% speed or +1 Handling for 1D6 minutes',
268
399
  },
269
400
  {
270
401
  key: 'guts', name: 'Guts', kind: 'positive', karma: 10, page: 'p.74',
402
+ inert: 'nothing rolls to resist fear -- brandish menaces, but the target never tests',
403
+ family: 'pool',
271
404
  effect: '+2 dice to resist fear and intimidation, mundane or magical',
272
405
  },
273
406
  {
274
407
  key: 'home-ground', name: 'Home Ground', kind: 'positive', karma: 10, page: 'p.74',
408
+ inert: 'needs a chosen domain (IQualityChoice) and a per-domain bonus seat',
409
+ family: 'pool',
275
410
  effect: 'your turf, one chosen edge on it: the streets, the people, the host, or the bolt-holes',
276
411
  },
277
412
  {
278
413
  key: 'magic-resistance', name: 'Magic Resistance',
414
+ family: 'engine-rule',
279
415
  kind: 'positive', karma: 6, maxRating: 4, page: 'p.76', requires: 'mundane',
280
416
  effect: '+1 die per rating when you resist a spell -- always on: friendly spells (Heal included) fail on you too',
281
417
  },
282
418
  {
283
419
  key: 'mentor-spirit', name: 'Mentor Spirit', kind: 'positive', karma: 5, page: 'p.76',
420
+ inert: 'needs a chosen patron and its per-mentor bonuses and demands (p.320)',
421
+ family: 'engine-rule',
284
422
  requires: 'awakened',
285
423
  effect: 'a patron spirit guides your magic -- its blessings and its demands (p.320)',
286
424
  },
287
425
  {
288
426
  key: 'natural-hardening', name: 'Natural Hardening', kind: 'positive', karma: 10, page: 'p.76',
427
+ family: 'pool',
289
428
  effect: '1 point of natural biofeedback filtering, cumulative with a filter or living firewall',
429
+ effects: [{ type: 'dice', category: 'pools', property: 'biofeedback', value: 1 }],
290
430
  },
291
431
  {
292
- key: 'natural-immunity-natural', name: 'Natural Immunity (Natural)',
432
+ key: 'natural-immunity', name: 'Natural Immunity',
433
+ choice: {
434
+ of: 'hazard-class',
435
+ prompt: 'Which does your body simply refuse -- something natural, or something built?',
436
+ from: ['natural', 'synthetic'],
437
+ },
438
+ inert: 'no disease or toxin track exists to be immune to',
439
+ family: 'engine-rule',
293
440
  kind: 'positive', karma: 4, page: 'p.77',
294
441
  effect: 'immune to ONE chosen natural disease or toxin (never magical ones)',
295
442
  },
296
443
  {
297
444
  key: 'natural-immunity-synthetic', name: 'Natural Immunity (Synthetic)',
445
+ inert: 'no disease or toxin track exists to be immune to',
446
+ family: 'engine-rule',
298
447
  kind: 'positive', karma: 10, page: 'p.77',
299
448
  effect: 'immune to ONE chosen synthetic disease or toxin (never magical ones)',
300
449
  },
301
450
  {
302
451
  key: 'photographic-memory', name: 'Photographic Memory', kind: 'positive', karma: 6, page: 'p.77',
452
+ inert: 'nothing rolls a Memory Test',
453
+ family: 'pool',
303
454
  effect: '+2 dice on every Memory Test -- what you saw stays seen',
304
455
  },
305
456
  {
306
- key: 'resistance-pathogens-or-toxins', name: 'Resistance to Pathogens OR Toxins',
457
+ key: 'resistance-one', name: 'Resistance (One Class)',
458
+ choice: {
459
+ of: 'hazard-class',
460
+ prompt: 'Which do you shrug off -- pathogens, or toxins?',
461
+ from: ['pathogens', 'toxins'],
462
+ },
463
+ inert: 'nothing rolls to resist a pathogen or a toxin',
464
+ family: 'pool',
307
465
  kind: 'positive', karma: 4, page: 'p.77',
308
466
  effect: '+1 die to resist one chosen class: pathogens, or toxins',
309
467
  },
310
468
  {
311
469
  key: 'resistance-pathogens-and-toxins', name: 'Resistance to Pathogens AND Toxins',
470
+ inert: 'nothing rolls to resist a pathogen or a toxin',
471
+ family: 'pool',
312
472
  kind: 'positive', karma: 8, page: 'p.77',
313
473
  effect: '+1 die to resist both pathogens and toxins',
314
474
  },
315
475
  {
316
476
  key: 'spirit-affinity', name: 'Spirit Affinity', kind: 'positive', karma: 7, page: 'p.77',
477
+ inert: 'needs a chosen spirit type; binding and services are not rolled per type',
478
+ family: 'engine-rule',
317
479
  requires: 'awakened',
318
480
  effect: 'one spirit type favors you: +1 service from them, +1 die to bind them',
319
481
  },
320
482
  // ------------------------------ Negative ------------------------------
321
483
  {
322
484
  key: 'addiction-mild', name: 'Addiction (Mild)', kind: 'negative', karma: 4, page: 'p.77-78',
485
+ inert: 'no craving clock and no withdrawal state',
486
+ family: 'condition',
323
487
  effect: 'hooked, mildly: cravings monthly; withdrawal bites your tests until fed',
324
488
  },
325
489
  {
326
490
  key: 'addiction-moderate', name: 'Addiction (Moderate)', kind: 'negative', karma: 9, page: 'p.77-78',
491
+ inert: 'no craving clock and no withdrawal state',
492
+ family: 'condition',
327
493
  effect: 'hooked: cravings every two weeks; withdrawal bites your tests until fed',
328
494
  },
329
495
  {
330
496
  key: 'allergy-common-mild', name: 'Allergy (Common, Mild)', kind: 'negative', karma: 10, page: 'p.78',
497
+ inert: 'no exposure state -- nothing knows you are standing in the sun',
498
+ family: 'condition',
331
499
  effect: 'allergic to something everywhere (sunlight, soy, pollutants): -1 die while exposed, -1 to resist it as a weapon',
332
500
  },
333
501
  {
334
502
  key: 'astral-beacon', name: 'Astral Beacon', kind: 'negative', karma: 10, page: 'p.78-79',
503
+ inert: 'no astral signature is recorded',
504
+ family: 'engine-rule',
335
505
  requires: 'awakened',
336
506
  effect: 'your astral signature burns bright and lasts twice as long -- easier to read, easier to trace',
337
507
  },
338
508
  {
339
509
  key: 'code-of-honor', name: 'Code of Honor', kind: 'negative', karma: 15, page: 'p.80',
510
+ inert: 'canon leaves this to the table; the engine adjudicates no vow',
511
+ family: 'narrative',
340
512
  effect: 'a group you will not kill, ever -- non-lethal only, whatever it costs the run',
341
513
  },
342
514
  {
343
515
  key: 'gremlins', name: 'Gremlins', kind: 'negative', karma: 4, maxRating: 4, page: 'p.81',
516
+ family: 'threshold',
344
517
  effect: 'machines hate you: on the deck, a glitch needs one fewer 1 per rating',
345
518
  },
346
519
  {
347
520
  key: 'loss-of-confidence', name: 'Loss of Confidence', kind: 'negative', karma: 10, page: 'p.82',
521
+ inert: 'specializations are not modelled, so none can be suspended',
522
+ family: 'condition',
348
523
  effect: 'one skill you were proud of, now doubted -- its specializations unusable while the doubt holds',
349
524
  },
350
525
  {
351
526
  key: 'scorched', name: 'Scorched', kind: 'negative', karma: 10, page: 'p.83',
527
+ inert: 'no neurological scar state, and nothing triggers on entering VR',
528
+ family: 'condition',
352
529
  effect: 'black IC or BTL left a mark: one chosen neurological scar that flares in VR',
353
530
  },
354
531
  {
355
532
  key: 'sensitive-system', name: 'Sensitive System', kind: 'negative', karma: 12, page: 'p.83',
533
+ inert: 'the clinic charges canon Essence; there is no doubling hook, and bioware is not rejected',
534
+ family: 'engine-rule',
356
535
  effect: 'implants hate you: DOUBLE Essence loss from cyberware; bioware is rejected outright',
357
536
  },
358
537
  {
359
538
  key: 'social-stress', name: 'Social Stress', kind: 'negative', karma: 8, page: 'p.85',
539
+ inert: 'no pressure state to read',
540
+ family: 'condition',
360
541
  effect: 'crowds and confrontation wear on you -- emotions interfere when the pressure is social',
361
542
  },
362
543
  {
363
544
  key: 'uneducated', name: 'Uneducated', kind: 'negative', karma: 8, page: 'p.87',
545
+ inert: 'knowledge skills are not modelled, and learning costs are flat',
546
+ family: 'engine-rule',
364
547
  effect: 'no schooling: unaware in Technical/Academic/Professional knowledge you do not hold; learning them costs double',
365
548
  },
366
549
  {
367
550
  key: 'unsteady-hands', name: 'Unsteady Hands', kind: 'negative', karma: 7, page: 'p.87',
551
+ inert: 'no symptom state, and no fine-handwork test to penalise',
552
+ family: 'condition',
368
553
  effect: 'the shakes: -2 dice on fine handwork when the symptoms surface',
369
554
  },
370
555
  {
371
556
  key: 'weak-immune-system', name: 'Weak Immune System', kind: 'negative', karma: 10, page: 'p.87',
557
+ inert: 'no disease track, so no Power to raise',
558
+ family: 'engine-rule',
372
559
  effect: 'disease hits harder: +2 Power on every disease you resist (excludes the immunity qualities)',
373
560
  },
374
561
  // ------------------------- Cutting Aces (2026-09-02) -------------------------
@@ -383,85 +570,133 @@ export const QUALITIES = [
383
570
  // ------------------------------ Positive ------------------------------
384
571
  {
385
572
  key: 'alibi', name: 'Alibi', kind: 'positive', karma: 4, page: 'Cutting Aces p.150',
573
+ inert: 'canon narrows it to plausible evidence, which nothing tracks',
574
+ family: 'pool',
386
575
  effect: 'always the +2 on Con tests for plausible-seeming evidence -- your excuses are pre-made',
387
576
  },
388
577
  {
389
578
  key: 'closer', name: 'Closer', kind: 'positive', karma: 4, page: 'Cutting Aces p.150',
579
+ inert: 'canon narrows it to life-or-death stakes, which nothing tracks',
580
+ family: 'pool',
390
581
  effect: '+2 dice on Negotiation when it is a matter of life or death',
391
582
  },
392
583
  {
393
584
  key: 'cynic', name: 'Cynic', kind: 'positive', karma: 6, page: 'Cutting Aces p.150',
585
+ inert: 'the engine rolls no opposed pool for an NPC acting socially ON you',
586
+ family: 'pool',
394
587
  effect: 'anyone making a Social test against you rolls at -1',
395
588
  },
396
589
  {
397
590
  key: 'empathic-listener', name: 'Empathic Listener', kind: 'positive', karma: 10, page: 'Cutting Aces p.150',
591
+ inert: 'no substitution seat: a pool is assembled from fixed attributes',
592
+ family: 'substitution',
398
593
  effect: 'Intuition stands in for Charisma on Etiquette tests',
399
594
  },
400
595
  {
401
596
  key: 'good-looking-and-knows-it', name: 'Good Looking and Knows It', kind: 'positive', karma: 10, page: 'Cutting Aces p.150',
597
+ inert: 'no modifier ledger to ignore two points of',
598
+ family: 'threshold',
402
599
  effect: 'ignore 2 points of negative modifiers on any Social test; +1 Notoriety, and people remember your face (+2 to recall you)',
403
600
  },
404
601
  {
405
602
  key: 'groupthink', name: 'Groupthink', kind: 'positive', karma: 5, page: 'Cutting Aces p.150-151',
603
+ inert: 'no social teamwork test, and the assistant/leader roles do not exist',
604
+ family: 'pool',
406
605
  effect: 'social teamwork: as an assistant +1 die and +1 Social limit; as the leader, take -2 to soften an assistant\'s glitch',
407
606
  },
408
607
  {
409
608
  key: 'honest-face', name: 'Honest Face', kind: 'positive', karma: 5, page: 'Cutting Aces p.151',
609
+ inert: 'nothing rolls Judge Intentions against you',
610
+ family: 'pool',
410
611
  effect: '+2 dice on your Charisma + Willpower when someone Judges your Intentions',
411
612
  },
412
613
  {
413
614
  key: 'innocuous', name: 'Innocuous', kind: 'positive', karma: 5, page: 'Cutting Aces p.151',
615
+ inert: 'canon narrows it to hiding in a crowd, which the room does not model',
616
+ family: 'pool',
414
617
  effect: '+2 on Sneaking tests to hide in a crowd',
415
618
  },
416
619
  {
417
620
  key: 'master-debater', name: 'Master Debater', kind: 'positive', karma: 10, page: 'Cutting Aces p.151',
621
+ inert: 'no substitution seat: a pool is assembled from fixed attributes',
622
+ family: 'substitution',
418
623
  effect: 'Logic stands in for Charisma on Diplomacy tests -- you win by crushing their logic',
419
624
  },
420
625
  {
421
626
  key: 'memory-palace', name: 'Memory Palace', kind: 'positive', karma: 6, page: 'Cutting Aces p.151',
627
+ inert: 'nothing rolls a Memory Test, and interrogation is not a test',
628
+ family: 'pool',
422
629
  effect: '+1 die on Memory tests and to resist Intimidation (Interrogation)',
423
630
  },
424
631
  {
425
632
  key: 'method-actor', name: 'Method Actor', kind: 'positive', karma: 7, page: 'Cutting Aces p.151',
633
+ inert: 'no immersion clock, and Impersonation is not rolled',
634
+ family: 'pool',
426
635
  effect: 'each full day immersed in a role: +1 die and +1 Social limit on Impersonation as that role, up to your Willpower',
427
636
  },
428
637
  {
429
638
  key: 'mnemonic-vault', name: 'Mnemonic Vault', kind: 'positive', karma: 8, page: 'Cutting Aces p.151',
639
+ inert: 'interrogation is not a test',
640
+ family: 'pool',
430
641
  requiresQuality: 'memory-palace',
431
642
  effect: 'lock memories away: +2 dice to resist Interrogation and anything that would compel you to talk; resist, and they believe that was all you knew',
432
643
  },
433
644
  {
434
645
  key: 'watch-the-suit', name: 'Watch the Suit', kind: 'positive', karma: 3, page: 'Cutting Aces p.151',
646
+ inert: 'appearance carries no Etiquette penalty to be spared',
647
+ family: 'engine-rule',
435
648
  effect: 'Stun never marks your appearance: no Etiquette penalty for looking roughed up after a fight you took no Physical damage in',
436
649
  },
437
650
  // ------------------------------ Negative ------------------------------
438
651
  {
439
652
  key: 'alpha-junkie', name: 'Alpha Junkie', kind: 'negative', karma: 12, page: 'Cutting Aces p.151-152',
653
+ inert: 'no leadership-over-you state to trigger the Composure test',
654
+ family: 'condition',
440
655
  effect: 'when someone takes charge over you (or wins a Leadership test against you): Charisma + Willpower (3) or you fight for control -- argue, intimidate, or attack',
441
656
  },
442
657
  {
443
658
  key: 'designated-omega', name: 'Designated Omega', kind: 'negative', karma: 5, page: 'Cutting Aces p.152',
659
+ family: 'pool',
444
660
  effect: '-2 dice on every Leadership test, initiating or resisting',
661
+ effects: [{ type: 'dice', category: 'skills', property: 'leadership', value: -2 }],
445
662
  },
446
663
  {
447
664
  key: 'disheveled', name: 'Disheveled', kind: 'negative', karma: 5, page: 'Cutting Aces p.152',
665
+ inert: 'no positive social modifier ledger to cap',
666
+ family: 'threshold',
448
667
  effect: 'clothing never gives you a positive Social dice pool or limit modifier',
449
668
  },
450
669
  {
451
670
  key: 'favored', name: 'Favored', kind: 'negative', karma: 5, page: 'Cutting Aces p.152',
671
+ family: 'pool',
452
672
  effect: 'a group you cannot see straight (common group, biased): -2 dice on Social tests with them, and they negotiate against you at +2',
673
+ effects: [{ type: 'dice', category: 'pools', property: 'social', value: -2 }],
453
674
  },
454
675
  {
455
676
  key: 'one-born-every-minute', name: 'One Born Every Minute', kind: 'negative', karma: 5, page: 'Cutting Aces p.152',
677
+ inert: 'nothing rolls for you to resist a Con made against you',
678
+ family: 'pool',
456
679
  effect: '-2 dice to resist every Con test made against you',
457
680
  },
458
681
  {
459
682
  key: 'the-goat', name: 'The Goat', kind: 'negative', karma: 8, page: 'Cutting Aces p.152',
683
+ family: 'pool',
460
684
  effect: 'you just look guilty: everyone meeting you for the first time is suspicious (-1 Social), and security watches you first',
685
+ effects: [{ type: 'dice', category: 'pools', property: 'social', value: -1 }],
461
686
  },
462
687
  {
463
688
  key: 'ugly-and-doesnt-care', name: "Ugly and Doesn't Care", kind: 'negative', karma: 10, page: 'Cutting Aces p.152-153',
689
+ family: 'attribute',
464
690
  effect: 'Charisma maximum -1 and starting Charisma -1 (minimum 1)',
691
+ effects: [
692
+ // BOTH HALVES of the printed line. The attribute effect is the
693
+ // "starting Charisma -1"; the cap is the "Charisma maximum -1",
694
+ // which nothing enforced until ceilings existed -- the same gap
695
+ // Lucky had, found by sweeping the family rather than by a second
696
+ // report.
697
+ { type: 'attribute', category: 'attributes', property: 'charisma', value: -1 },
698
+ { type: 'cap', category: 'attributes', property: 'charisma', value: -1 },
699
+ ],
465
700
  },
466
701
  ];
467
702
  /**
@@ -472,16 +707,22 @@ export const QUALITIES = [
472
707
  export const RETIRED_QUALITIES = [
473
708
  {
474
709
  key: 'indomitable', name: 'Indomitable', kind: 'positive', karma: 8, maxRating: 3, page: 'p.75',
710
+ inert: 'the engine models only the Social limit, and no limit may be chosen',
711
+ family: 'limit',
475
712
  effect: '+1 to one Inherent limit of your choice per rating',
476
713
  retired: 'the engine models only the Social limit, and the choice of limit has no seat yet',
477
714
  },
478
715
  {
479
716
  key: 'will-to-live', name: 'Will to Live', kind: 'positive', karma: 3, maxRating: 3, page: 'p.77',
717
+ inert: 'there is no overflow track',
718
+ family: 'engine-rule',
480
719
  effect: '+1 Damage Overflow box per rating -- more room between down and dead, no easier to knock out',
481
720
  retired: 'the engine has no overflow track: a full Physical track is death',
482
721
  },
483
722
  {
484
723
  key: 'social-appearance-anxiety', name: 'Social Appearance Anxiety', kind: 'negative', karma: 3, maxRating: 3, page: 'Cutting Aces p.152',
724
+ inert: 'there is no grooming state',
725
+ family: 'condition',
485
726
  effect: '-1 die per rating on all Social rolls whenever you are not clean, coiffed, and cutting',
486
727
  retired: 'the engine has no grooming state to judge "clean, coiffed, and cutting"',
487
728
  },
@@ -490,7 +731,43 @@ export const RETIRED_QUALITIES = [
490
731
  * A saved or offered key, resolved: the old fixed "<key>-1" entries
491
732
  * (every rated quality shipped as Rating 1 only) read as key + rating 1.
492
733
  */
734
+ /**
735
+ * THE KEYS THAT BAKED A PLAYER-S CHOICE INTO THE SLUG, and what each
736
+ * one resolves to now.
737
+ *
738
+ * Before the shared choice lane existed, a quality whose effect named
739
+ * something the player picked had nowhere to put the answer, so the
740
+ * answer went into the key: incompetent-hacking, codeslinger-hack-on-
741
+ * the-fly, and five more. Seven slugs for what is really two qualities
742
+ * and a pick.
743
+ *
744
+ * These keys are written into every save that holds one, so they are
745
+ * not renamed -- they are RESOLVED, here, into the key plus the pick
746
+ * they always meant. An old save loads, the pick lands in Player.picks,
747
+ * and the character plays identically. Nothing has to be migrated and
748
+ * nothing can be missed, which is the same shape as the legacy
749
+ * "<key>-1" rated form below.
750
+ *
751
+ * A pick's VALUE is the engine's own word, never a label: Codeslinger's
752
+ * is the POOL it modifies (matrix-sleaze), so `property: 'chosen'`
753
+ * resolves with no translation table in between.
754
+ */
755
+ const LEGACY_CHOICE_KEYS = {
756
+ // The old slug named a SKILL; the quality is per GROUP (p.81), so it
757
+ // resolves to the group hacking belongs to. A character who held
758
+ // incompetent-hacking was, in canon terms, always Incompetent
759
+ // (Cracking) -- the slug simply could not say so.
760
+ 'incompetent-hacking': { key: 'incompetent', pick: 'cracking' },
761
+ 'codeslinger-hack-on-the-fly': { key: 'codeslinger', pick: 'matrix-sleaze' },
762
+ 'codeblock-brute-force': { key: 'codeblock', pick: 'matrix-attack' },
763
+ 'natural-immunity-natural': { key: 'natural-immunity', pick: 'natural' },
764
+ 'natural-immunity-synthetic': { key: 'natural-immunity', pick: 'synthetic' },
765
+ 'resistance-pathogens-or-toxins': { key: 'resistance-one', pick: 'toxins' },
766
+ };
493
767
  export function resolveQualityKey(key) {
768
+ const legacy = LEGACY_CHOICE_KEYS[key];
769
+ if (legacy)
770
+ return { key: legacy.key, rating: 1, pick: legacy.pick };
494
771
  const m = /^(.*)-1$/.exec(key);
495
772
  if (m) {
496
773
  const base = m[1];
@@ -499,6 +776,8 @@ export function resolveQualityKey(key) {
499
776
  }
500
777
  return { key, rating: 1 };
501
778
  }
779
+ /** Every legacy slug that resolves to a key plus a pick, for tests and tooling. */
780
+ export const LEGACY_CHOICE_SLUGS = Object.keys(LEGACY_CHOICE_KEYS);
502
781
  export function getQuality(key) {
503
782
  if (!key)
504
783
  return undefined;