@rpgm-tools/neo-angband-content 0.11.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 (113) hide show
  1. package/LICENSE.md +43 -0
  2. package/README.md +65 -0
  3. package/dist/compile.d.ts +10 -0
  4. package/dist/compile.d.ts.map +1 -0
  5. package/dist/compile.js +42 -0
  6. package/dist/compile.js.map +1 -0
  7. package/dist/data-exactness.reader.d.ts +105 -0
  8. package/dist/data-exactness.reader.d.ts.map +1 -0
  9. package/dist/data-exactness.reader.js +547 -0
  10. package/dist/data-exactness.reader.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/parser.d.ts +70 -0
  16. package/dist/parser.d.ts.map +1 -0
  17. package/dist/parser.js +365 -0
  18. package/dist/parser.js.map +1 -0
  19. package/dist/records.d.ts +101 -0
  20. package/dist/records.d.ts.map +1 -0
  21. package/dist/records.js +244 -0
  22. package/dist/records.js.map +1 -0
  23. package/dist/specs/generate.d.ts +12 -0
  24. package/dist/specs/generate.d.ts.map +1 -0
  25. package/dist/specs/generate.js +57 -0
  26. package/dist/specs/generate.js.map +1 -0
  27. package/dist/specs/index.d.ts +9 -0
  28. package/dist/specs/index.d.ts.map +1 -0
  29. package/dist/specs/index.js +60 -0
  30. package/dist/specs/index.js.map +1 -0
  31. package/dist/specs/init.d.ts +23 -0
  32. package/dist/specs/init.d.ts.map +1 -0
  33. package/dist/specs/init.js +305 -0
  34. package/dist/specs/init.js.map +1 -0
  35. package/dist/specs/misc.d.ts +33 -0
  36. package/dist/specs/misc.d.ts.map +1 -0
  37. package/dist/specs/misc.js +146 -0
  38. package/dist/specs/misc.js.map +1 -0
  39. package/dist/specs/mon-init.d.ts +22 -0
  40. package/dist/specs/mon-init.d.ts.map +1 -0
  41. package/dist/specs/mon-init.js +157 -0
  42. package/dist/specs/mon-init.js.map +1 -0
  43. package/dist/specs/obj-init.d.ts +19 -0
  44. package/dist/specs/obj-init.d.ts.map +1 -0
  45. package/dist/specs/obj-init.js +219 -0
  46. package/dist/specs/obj-init.js.map +1 -0
  47. package/dist/specs/ui-entry.d.ts +22 -0
  48. package/dist/specs/ui-entry.d.ts.map +1 -0
  49. package/dist/specs/ui-entry.js +69 -0
  50. package/dist/specs/ui-entry.js.map +1 -0
  51. package/dist/specs/visuals.d.ts +26 -0
  52. package/dist/specs/visuals.d.ts.map +1 -0
  53. package/dist/specs/visuals.js +35 -0
  54. package/dist/specs/visuals.js.map +1 -0
  55. package/pack/activation.json +3340 -0
  56. package/pack/artifact.json +5146 -0
  57. package/pack/blow_effects.json +370 -0
  58. package/pack/blow_methods.json +286 -0
  59. package/pack/body.json +59 -0
  60. package/pack/brand.json +100 -0
  61. package/pack/chest_trap.json +117 -0
  62. package/pack/class.json +5501 -0
  63. package/pack/constants.json +470 -0
  64. package/pack/curse.json +580 -0
  65. package/pack/dungeon_profile.json +836 -0
  66. package/pack/ego_item.json +2900 -0
  67. package/pack/flavor.json +1541 -0
  68. package/pack/hints.json +303 -0
  69. package/pack/history.json +1656 -0
  70. package/pack/manifest.json +52 -0
  71. package/pack/monster.json +29392 -0
  72. package/pack/monster_base.json +688 -0
  73. package/pack/monster_spell.json +2735 -0
  74. package/pack/names.json +1204 -0
  75. package/pack/object.json +11877 -0
  76. package/pack/object_base.json +364 -0
  77. package/pack/object_property.json +2034 -0
  78. package/pack/p_race.json +447 -0
  79. package/pack/pain.json +150 -0
  80. package/pack/pit.json +617 -0
  81. package/pack/player_property.json +519 -0
  82. package/pack/player_timed.json +1321 -0
  83. package/pack/projection.json +664 -0
  84. package/pack/quest.json +18 -0
  85. package/pack/realm.json +34 -0
  86. package/pack/room_template.json +9571 -0
  87. package/pack/shape.json +263 -0
  88. package/pack/slay.json +116 -0
  89. package/pack/store.json +675 -0
  90. package/pack/summon.json +143 -0
  91. package/pack/terrain.json +511 -0
  92. package/pack/trap.json +1483 -0
  93. package/pack/ui_entry.json +382 -0
  94. package/pack/ui_entry_base.json +60 -0
  95. package/pack/ui_entry_renderer.json +43 -0
  96. package/pack/ui_knowledge.json +302 -0
  97. package/pack/vault.json +4355 -0
  98. package/pack/visuals.json +680 -0
  99. package/pack/world.json +1030 -0
  100. package/package.json +67 -0
  101. package/src/compile.ts +55 -0
  102. package/src/data-exactness.reader.ts +559 -0
  103. package/src/index.ts +31 -0
  104. package/src/parser.ts +406 -0
  105. package/src/records.ts +341 -0
  106. package/src/specs/generate.ts +61 -0
  107. package/src/specs/index.ts +103 -0
  108. package/src/specs/init.ts +322 -0
  109. package/src/specs/misc.ts +153 -0
  110. package/src/specs/mon-init.ts +165 -0
  111. package/src/specs/obj-init.ts +230 -0
  112. package/src/specs/ui-entry.ts +74 -0
  113. package/src/specs/visuals.ts +37 -0
package/src/index.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @rpgm-tools/neo-angband-content - the core content pack.
3
+ *
4
+ * Phase 1 builds a compiler that translates reference/lib/gamedata/*.txt
5
+ * (Angband 4.2.6) into the schema-validated pack format defined by
6
+ * @rpgm-tools/neo-angband-mod-sdk. The base game is itself "pack zero": it loads
7
+ * through the same pipeline as any mod, which is what makes total
8
+ * conversions possible by construction.
9
+ */
10
+
11
+ /** Namespace prefix reserved for the base game's content. */
12
+ export const CORE_NAMESPACE = "core";
13
+
14
+ export { ParseError, isValidRandom, parseLine, parseSignature } from "./parser.js";
15
+ export type {
16
+ DirectiveSignature,
17
+ FieldSpec,
18
+ FieldType,
19
+ ParseErrorCode,
20
+ ParsedLine,
21
+ } from "./parser.js";
22
+ export { compileGamedata } from "./records.js";
23
+ export type {
24
+ CompiledFile,
25
+ DirectiveDef,
26
+ FileSpec,
27
+ JsonObject,
28
+ JsonPrimitive,
29
+ JsonValue,
30
+ } from "./records.js";
31
+ export { gamedataSpecs } from "./specs/index.js";
package/src/parser.ts ADDED
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Generic parser for Angband's gamedata record format.
3
+ *
4
+ * Faithful port of the line-level semantics of reference/src/parser.c
5
+ * (Angband 4.2.6):
6
+ *
7
+ * - A line is `directive:field1:field2:...`. Leading whitespace is skipped;
8
+ * blank lines and lines starting with `#` are ignored.
9
+ * - Each directive is registered with a format string like upstream
10
+ * parser_reg(), e.g. "name str name" or "effect sym eff ?sym type
11
+ * ?int radius ?int other". A `?` prefix marks an optional field; a
12
+ * mandatory field may not follow an optional one, and no field may
13
+ * follow a `str` field.
14
+ * - int/uint/sym/rand fields are tokenized on `:` with strtok() semantics
15
+ * (consecutive colons collapse). `str` consumes the rest of the line,
16
+ * colons included. `char` consumes exactly one character (which may be
17
+ * a space or a colon) and then expects `:` or end of line.
18
+ * - int uses strtol(_, _, 0) semantics (hex/octal prefixes accepted,
19
+ * trailing garbage ignored); uint additionally rejects a leading `-`.
20
+ * - rand fields are validated against upstream parse_random() but the RAW
21
+ * dice string (e.g. "1+2d3M4") is preserved; it is never evaluated.
22
+ */
23
+
24
+ export type FieldType = "int" | "uint" | "sym" | "str" | "char" | "rand";
25
+
26
+ export interface FieldSpec {
27
+ readonly type: FieldType;
28
+ readonly name: string;
29
+ readonly optional: boolean;
30
+ }
31
+
32
+ export interface DirectiveSignature {
33
+ readonly directive: string;
34
+ readonly fields: readonly FieldSpec[];
35
+ }
36
+
37
+ export type ParseErrorCode =
38
+ | "MISSING_FIELD"
39
+ | "UNDEFINED_DIRECTIVE"
40
+ | "FIELD_TOO_LONG"
41
+ | "NOT_NUMBER"
42
+ | "NOT_RANDOM"
43
+ | "INVALID_SPEC";
44
+
45
+ /** Parse failure; `code` mirrors the upstream PARSE_ERROR_* it corresponds to. */
46
+ export class ParseError extends Error {
47
+ readonly code: ParseErrorCode;
48
+
49
+ constructor(code: ParseErrorCode, message: string) {
50
+ super(message);
51
+ this.name = "ParseError";
52
+ this.code = code;
53
+ }
54
+ }
55
+
56
+ /** A successfully parsed line: the directive plus its named field values. */
57
+ export interface ParsedLine {
58
+ readonly directive: string;
59
+ /** Field name -> value, in signature order. Absent optionals are omitted. */
60
+ readonly values: Readonly<Record<string, string | number>>;
61
+ }
62
+
63
+ const FIELD_TYPES: readonly string[] = ["int", "uint", "sym", "str", "char", "rand"];
64
+
65
+ function isFieldType(s: string): s is FieldType {
66
+ return FIELD_TYPES.includes(s);
67
+ }
68
+
69
+ /**
70
+ * Parse a registration format string exactly like upstream parse_specs():
71
+ * `fmt ::= directive [type name]*` where `type` may carry a `?` prefix.
72
+ */
73
+ export function parseSignature(fmt: string): DirectiveSignature {
74
+ const tokens = fmt.split(" ").filter((t) => t.length > 0);
75
+ const directive = tokens.shift();
76
+ if (directive === undefined) {
77
+ throw new ParseError("INVALID_SPEC", `empty format string`);
78
+ }
79
+ const fields: FieldSpec[] = [];
80
+ while (tokens.length > 0) {
81
+ const rawType = tokens.shift();
82
+ if (rawType === undefined) {
83
+ break;
84
+ }
85
+ const name = tokens.shift();
86
+ if (name === undefined) {
87
+ throw new ParseError("INVALID_SPEC", `type without name in "${fmt}"`);
88
+ }
89
+ const optional = rawType.startsWith("?");
90
+ const typeName = optional ? rawType.slice(1) : rawType;
91
+ if (!isFieldType(typeName)) {
92
+ throw new ParseError("INVALID_SPEC", `unknown field type "${rawType}" in "${fmt}"`);
93
+ }
94
+ const prev = fields[fields.length - 1];
95
+ if (prev !== undefined) {
96
+ if (!optional && prev.optional) {
97
+ throw new ParseError("INVALID_SPEC", `mandatory field after optional in "${fmt}"`);
98
+ }
99
+ if (prev.type === "str") {
100
+ throw new ParseError("INVALID_SPEC", `field after str field in "${fmt}"`);
101
+ }
102
+ }
103
+ fields.push({ type: typeName, name, optional });
104
+ }
105
+ return { directive, fields };
106
+ }
107
+
108
+ /** C isspace() for the default locale. */
109
+ function isCSpace(code: number): boolean {
110
+ return (
111
+ code === 0x20 || // space
112
+ code === 0x09 || // \t
113
+ code === 0x0a || // \n
114
+ code === 0x0b || // \v
115
+ code === 0x0c || // \f
116
+ code === 0x0d // \r
117
+ );
118
+ }
119
+
120
+ const INT_MAX = 2147483647;
121
+
122
+ interface NumberScan {
123
+ readonly value: number;
124
+ readonly end: number;
125
+ readonly negative: boolean;
126
+ }
127
+
128
+ function digitValue(ch: string, base: number): number | null {
129
+ let v: number;
130
+ const code = ch.charCodeAt(0);
131
+ if (code >= 0x30 && code <= 0x39) {
132
+ v = code - 0x30;
133
+ } else if (code >= 0x41 && code <= 0x5a) {
134
+ v = code - 0x41 + 10;
135
+ } else if (code >= 0x61 && code <= 0x7a) {
136
+ v = code - 0x61 + 10;
137
+ } else {
138
+ return null;
139
+ }
140
+ return v < base ? v : null;
141
+ }
142
+
143
+ /**
144
+ * C strtol/strtoul-style number scan. With `base0` the base is inferred
145
+ * from a 0x/0 prefix as strtol(_, _, 0) does; otherwise base 10.
146
+ * Returns null when no digits were consumed (endptr == nptr in C).
147
+ */
148
+ function scanNumber(s: string, start: number, base0: boolean): NumberScan | null {
149
+ let i = start;
150
+ while (i < s.length && isCSpace(s.charCodeAt(i))) {
151
+ i++;
152
+ }
153
+ let negative = false;
154
+ const sign = s[i];
155
+ if (sign === "+" || sign === "-") {
156
+ negative = sign === "-";
157
+ i++;
158
+ }
159
+ let base = 10;
160
+ if (base0 && s[i] === "0") {
161
+ const next = s[i + 1];
162
+ const afterPrefix = s[i + 2];
163
+ if (
164
+ (next === "x" || next === "X") &&
165
+ afterPrefix !== undefined &&
166
+ digitValue(afterPrefix, 16) !== null
167
+ ) {
168
+ base = 16;
169
+ i += 2;
170
+ } else {
171
+ base = 8;
172
+ }
173
+ }
174
+ const digitsStart = i;
175
+ let value = 0;
176
+ while (i < s.length) {
177
+ const ch = s[i];
178
+ if (ch === undefined) {
179
+ break;
180
+ }
181
+ const d = digitValue(ch, base);
182
+ if (d === null) {
183
+ break;
184
+ }
185
+ value = value * base + d;
186
+ i++;
187
+ }
188
+ if (i === digitsStart) {
189
+ return null;
190
+ }
191
+ return { value: negative ? -value : value, end: i, negative };
192
+ }
193
+
194
+ function containsOnlySpaces(s: string, from: number): boolean {
195
+ for (let i = from; i < s.length; i++) {
196
+ if (!isCSpace(s.charCodeAt(i))) {
197
+ return false;
198
+ }
199
+ }
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Validate a random-value (dice) string. Faithful port of parse_random()
205
+ * in reference/src/parser.c; only validity is reported, the raw string is
206
+ * what gets stored.
207
+ */
208
+ export function isValidRandom(s: string): boolean {
209
+ let pos = 0;
210
+ let i = 0;
211
+ let minI = 1;
212
+ if (s[0] === "-") {
213
+ pos++;
214
+ }
215
+ for (;;) {
216
+ const c = s[pos];
217
+ if (c === "d") {
218
+ if (i > 2) {
219
+ return false;
220
+ }
221
+ if (i < 2) {
222
+ /* 'd' with no preceding number implies one die. */
223
+ i = 2;
224
+ }
225
+ minI = 3;
226
+ pos++;
227
+ } else if (c === "M") {
228
+ if (i === 2) {
229
+ return false;
230
+ }
231
+ i = 3;
232
+ minI = 4;
233
+ pos++;
234
+ } else {
235
+ const scan = scanNumber(s, pos, false);
236
+ if (scan === null) {
237
+ /* Trailing garbage or not enough values are not accepted. */
238
+ if (!containsOnlySpaces(s, pos) || i < minI) {
239
+ return false;
240
+ }
241
+ break;
242
+ }
243
+ if (scan.value > INT_MAX || (scan.negative && scan.value !== 0) || s[pos] === "+") {
244
+ return false;
245
+ }
246
+ pos = scan.end;
247
+ if (i === 0) {
248
+ if (s[pos] === "d") {
249
+ i = 1;
250
+ } else if (s[pos] === "+") {
251
+ pos++;
252
+ minI = 3;
253
+ } else {
254
+ if (!containsOnlySpaces(s, scan.end)) {
255
+ return false;
256
+ }
257
+ break;
258
+ }
259
+ } else if (i === 4) {
260
+ return false;
261
+ }
262
+ i++;
263
+ }
264
+ }
265
+ return true;
266
+ }
267
+
268
+ /**
269
+ * Cursor over one line, replicating the strtok() usage in parser_parse().
270
+ */
271
+ class Tokenizer {
272
+ private readonly s: string;
273
+ private pos = 0;
274
+
275
+ constructor(s: string) {
276
+ this.s = s;
277
+ }
278
+
279
+ /** strtok(_, ":"): skip leading colons, take up to the next colon. */
280
+ nextToken(): string | null {
281
+ while (this.pos < this.s.length && this.s[this.pos] === ":") {
282
+ this.pos++;
283
+ }
284
+ if (this.pos >= this.s.length) {
285
+ return null;
286
+ }
287
+ const start = this.pos;
288
+ while (this.pos < this.s.length && this.s[this.pos] !== ":") {
289
+ this.pos++;
290
+ }
291
+ const tok = this.s.slice(start, this.pos);
292
+ if (this.pos < this.s.length) {
293
+ this.pos++; /* consume the delimiter, as strtok() does */
294
+ }
295
+ return tok;
296
+ }
297
+
298
+ /** strtok(_, ""): the untouched rest of the line, or null at the end. */
299
+ rest(): string | null {
300
+ if (this.pos >= this.s.length) {
301
+ return null;
302
+ }
303
+ const tok = this.s.slice(this.pos);
304
+ this.pos = this.s.length;
305
+ return tok;
306
+ }
307
+
308
+ /**
309
+ * A char field: exactly one character (possibly a space or a colon),
310
+ * then a `:` separator or end of line. Anything else is FIELD_TOO_LONG.
311
+ */
312
+ takeChar(fieldName: string): string | null {
313
+ if (this.pos >= this.s.length) {
314
+ return null;
315
+ }
316
+ const cp = this.s.codePointAt(this.pos);
317
+ /* pos is in bounds, so a code point exists. */
318
+ const ch = String.fromCodePoint(cp ?? 0);
319
+ this.pos += ch.length;
320
+ if (this.pos < this.s.length) {
321
+ if (this.s[this.pos] === ":") {
322
+ this.pos++;
323
+ } else {
324
+ throw new ParseError("FIELD_TOO_LONG", fieldName);
325
+ }
326
+ }
327
+ return ch;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Parse one line against a set of registered directives.
333
+ *
334
+ * Returns null for blank lines and comments. Throws ParseError for
335
+ * undefined directives, missing mandatory fields, malformed numbers and
336
+ * malformed dice strings, mirroring parser_parse().
337
+ *
338
+ * parser_new (parser.c:99) has no counterpart: upstream allocates a mutable
339
+ * `struct parser` that parser_reg fills with hooks and parser_setpriv threads
340
+ * state through. Here the directive table is the caller's own `lookup` and
341
+ * parsing is this pure function, so there is nothing to allocate or free
342
+ * (parser_destroy likewise).
343
+ */
344
+ export function parseLine(
345
+ line: string,
346
+ lookup: (directive: string) => DirectiveSignature | undefined,
347
+ ): ParsedLine | null {
348
+ let start = 0;
349
+ while (start < line.length && isCSpace(line.charCodeAt(start))) {
350
+ start++;
351
+ }
352
+ if (start >= line.length || line[start] === "#") {
353
+ return null;
354
+ }
355
+ const t = new Tokenizer(line.slice(start));
356
+ const directiveTok = t.nextToken();
357
+ if (directiveTok === null) {
358
+ throw new ParseError("MISSING_FIELD", "missing directive");
359
+ }
360
+ const sig = lookup(directiveTok);
361
+ if (sig === undefined) {
362
+ throw new ParseError("UNDEFINED_DIRECTIVE", directiveTok);
363
+ }
364
+ const values: Record<string, string | number> = {};
365
+ for (const field of sig.fields) {
366
+ let tok: string | null;
367
+ if (field.type === "char") {
368
+ tok = t.takeChar(field.name);
369
+ } else if (field.type === "str") {
370
+ tok = t.rest();
371
+ } else {
372
+ tok = t.nextToken();
373
+ }
374
+ if (tok === null) {
375
+ if (!field.optional) {
376
+ throw new ParseError("MISSING_FIELD", field.name);
377
+ }
378
+ break;
379
+ }
380
+ if (field.type === "int") {
381
+ const scan = scanNumber(tok, 0, true);
382
+ if (scan === null) {
383
+ throw new ParseError("NOT_NUMBER", field.name);
384
+ }
385
+ values[field.name] = scan.value;
386
+ } else if (field.type === "uint") {
387
+ if (tok[0] === "-") {
388
+ throw new ParseError("NOT_NUMBER", field.name);
389
+ }
390
+ const scan = scanNumber(tok, 0, true);
391
+ if (scan === null || scan.negative) {
392
+ throw new ParseError("NOT_NUMBER", field.name);
393
+ }
394
+ values[field.name] = scan.value;
395
+ } else if (field.type === "rand") {
396
+ if (!isValidRandom(tok)) {
397
+ throw new ParseError("NOT_RANDOM", field.name);
398
+ }
399
+ values[field.name] = tok;
400
+ } else {
401
+ /* sym, str, char: stored verbatim */
402
+ values[field.name] = tok;
403
+ }
404
+ }
405
+ return { directive: sig.directive, values };
406
+ }