Skip to content

Commit eb24a6b

Browse files
committed
port data pipeline from content/*-crafting.json to data/ schema
main's data-consolidation commit (ff6b76f) moved raw weapon/armor/ monster/decoration records out of content/ into data/{weapons,armor, monsters,decorations}.json with JSON Schema validation, dropping the old *-crafting.json files this Next.js branch's build pipeline read from. Merging that in without updating the readers would have built successfully against stale in-memory state but broken from a clean checkout (generate-data.ts, generate-search-index.ts, generate-weapon-data.ts, validate.ts, and the src/lib/{weapons,armor, monsters,decorations}.ts route/metadata loaders all pointed at the deleted paths). Update every one of those readers to load from data/ instead: - weapons/monsters/decorations: same shape, just a new path (and a "decorations" key instead of "weapons" for decorations). - armor: data/armor/<slot>.json now bundles every rank in one file with an explicit "rank" field per piece, replacing the old low-rank/high-rank/g-rank subdirectories and hr/elder-based computeRank() guesswork. Crafting-tree files (content/**/map.json) are untouched by main and still live under content/, so tree loading is unchanged. Verified: generate-data still produces the same 3832 items across 37 bundles, `next build` exports all 3860 static pages, and `tsc --noEmit` is clean.
1 parent 120b0f0 commit eb24a6b

8 files changed

Lines changed: 135 additions & 123 deletions

File tree

‎scripts/generate-data.ts‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/**
2-
* Transforms content/*.json into compact per-category bundles in public/data:
2+
* Transforms data/*.json (raw items) and content/*.json (crafting trees) into
3+
* compact per-category bundles in public/data:
34
* - dictionaries.json (shared materials/enums/strings/melodies)
45
* - weapon-<type>.json (11)
56
* - armor-<slot>-<rank>.json
@@ -44,6 +45,7 @@ import type {
4445

4546
const root = process.cwd();
4647
const contentDir = path.join(root, "content");
48+
const dataDir = path.join(root, "data");
4749
const outDir = path.join(root, "public", "data");
4850

4951
const RANKS = ["low-rank", "high-rank", "g-rank"];
@@ -291,7 +293,7 @@ const melodies = readJson<Record<string, Melody[]>>(
291293

292294
for (const type of WEAPON_TYPES) {
293295
const crafting = readJson<{ weapons: Weapon[] }>(
294-
path.join(contentDir, "blacksmith", type, `${type}-crafting.json`)
296+
path.join(dataDir, "weapons", `${type}.json`)
295297
);
296298
const tree = readJson<{ map: WeaponTreeNode[] }>(
297299
path.join(contentDir, "blacksmith", type, "map.json")
@@ -312,19 +314,23 @@ for (const type of WEAPON_TYPES) {
312314
for (const w of items) pageManifest.push(`/blacksmith/${type}/${w.s}/`);
313315
}
314316

315-
// Armor
317+
// Armor — data/armor/<slot>.json holds every rank in one file (each piece
318+
// carries its own "rank"), while the crafting tree is still split per rank
319+
// under content/armorsmith/<slot>/<rank>/map.json.
316320
for (const slot of ARMOR_SLOTS) {
321+
const allPieces = readJson<{ armor: ArmorPiece[] }>(
322+
path.join(dataDir, "armor", `${slot}.json`)
323+
).armor;
317324
for (const rank of ARMOR_RANKS) {
318-
const craftingPath = path.join(contentDir, "armorsmith", slot, rank, `${slot}-crafting.json`);
319-
if (!fs.existsSync(craftingPath)) continue;
320-
const crafting = readJson<{ weapons: ArmorPiece[] }>(craftingPath);
325+
const pieces = allPieces.filter((p) => (p.rank ?? computeRank(p.hr, p.elder)) === rank);
326+
if (pieces.length === 0) continue;
321327
const treePath = path.join(contentDir, "armorsmith", slot, rank, "map.json");
322328
const tree = fs.existsSync(treePath)
323329
? readJson<{ map: WeaponTreeNode[] }>(treePath)
324330
: { map: [] };
325-
const items = crafting.weapons
331+
const items = pieces
326332
.filter((p) => !("donotrender" in p))
327-
.map((p) => encodeArmor({ ...p, slug: slugify(p.name), rank: computeRank(p.hr, p.elder) }));
333+
.map((p) => encodeArmor({ ...p, slug: slugify(p.name), rank: p.rank ?? computeRank(p.hr, p.elder) }));
328334
totalItems += items.length;
329335
push(`armor-${slot}-${rank}.json`, {
330336
kind: "armor",
@@ -340,7 +346,7 @@ for (const slot of ARMOR_SLOTS) {
340346

341347
// Monsters
342348
for (const category of MONSTER_CATEGORIES) {
343-
const p = path.join(contentDir, "monsters", category, `${category}.json`);
349+
const p = path.join(dataDir, "monsters", `${category}.json`);
344350
if (!fs.existsSync(p)) continue;
345351
const data = readJson<{ monsters: Monster[] }>(p);
346352
const items = data.monsters.map((m) => encodeMonster({ ...m, slug: slugify(m.name) }));
@@ -352,13 +358,13 @@ for (const category of MONSTER_CATEGORIES) {
352358

353359
// Decorations
354360
{
355-
const crafting = readJson<{ weapons: Decoration[] }>(
356-
path.join(contentDir, "decorations", "decorations-crafting.json")
361+
const crafting = readJson<{ decorations: Decoration[] }>(
362+
path.join(dataDir, "decorations.json")
357363
);
358364
const tree = readJson<{ map: WeaponTreeNode[] }>(
359365
path.join(contentDir, "decorations", "map.json")
360366
);
361-
const items = crafting.weapons
367+
const items = crafting.decorations
362368
.filter((d) => !("donotrender" in d))
363369
.map((d) => encodeDecoration({ ...d, slug: slugify(d.name) }));
364370
totalItems += items.length;

‎scripts/generate-search-index.ts‎

Lines changed: 46 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from "fs";
22
import path from "path";
33

44
const root = process.cwd();
5-
const contentDir = path.join(root, "content");
5+
const dataDir = path.join(root, "data");
66

77
function slugify(name: string): string {
88
return name
@@ -27,10 +27,10 @@ interface SearchEntry {
2727
const entries: SearchEntry[] = [];
2828

2929
// Weapons
30-
const weaponDir = path.join(contentDir, "blacksmith");
31-
for (const type of fs.readdirSync(weaponDir)) {
32-
const craftingFile = path.join(weaponDir, type, `${type}-crafting.json`);
33-
if (!fs.existsSync(craftingFile)) continue;
30+
const weaponDir = path.join(dataDir, "weapons");
31+
for (const file of fs.readdirSync(weaponDir)) {
32+
const type = file.replace(/\.json$/, "");
33+
const craftingFile = path.join(weaponDir, file);
3434

3535
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
3636
weapons: Record<string, unknown>[];
@@ -73,54 +73,51 @@ for (const type of fs.readdirSync(weaponDir)) {
7373
}
7474
}
7575

76-
// Armor
77-
const armorDir = path.join(contentDir, "armorsmith");
78-
for (const slot of fs.readdirSync(armorDir)) {
79-
const slotDir = path.join(armorDir, slot);
80-
if (!fs.statSync(slotDir).isDirectory()) continue;
81-
82-
for (const rank of fs.readdirSync(slotDir)) {
83-
const craftingFile = path.join(slotDir, rank, `${slot}-crafting.json`);
84-
if (!fs.existsSync(craftingFile)) continue;
85-
86-
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
87-
weapons: Record<string, unknown>[];
88-
};
89-
90-
for (const a of data.weapons) {
91-
if ("donotrender" in a || !a.name) continue;
92-
93-
const name = a.name as string;
94-
const slug = slugify(name);
95-
const skillNames = Array.isArray(a.skills)
96-
? (a.skills as ({ name: string } | string)[])
97-
.map((s) => (typeof s === "object" ? s.name : s))
98-
.join(" ")
99-
: "";
100-
101-
entries.push({
102-
name,
103-
slug,
104-
url: `/armorsmith/${slot}/${rank}/${slug}`,
105-
category: "armor",
106-
type: (a.type as string) || slot,
107-
rank,
108-
rarity: Number(a.rarity ?? 0),
109-
elements: "",
110-
skills: skillNames,
111-
});
112-
}
76+
// Armor — data/armor/<slot>.json holds every rank in one file; each piece
77+
// carries its own "rank" field.
78+
const armorDir = path.join(dataDir, "armor");
79+
for (const file of fs.readdirSync(armorDir)) {
80+
const slot = file.replace(/\.json$/, "");
81+
const craftingFile = path.join(armorDir, file);
82+
83+
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
84+
armor: Record<string, unknown>[];
85+
};
86+
87+
for (const a of data.armor) {
88+
if ("donotrender" in a || !a.name) continue;
89+
90+
const name = a.name as string;
91+
const slug = slugify(name);
92+
const rank = (a.rank as string) || "low-rank";
93+
const skillNames = Array.isArray(a.skills)
94+
? (a.skills as ({ name: string } | string)[])
95+
.map((s) => (typeof s === "object" ? s.name : s))
96+
.join(" ")
97+
: "";
98+
99+
entries.push({
100+
name,
101+
slug,
102+
url: `/armorsmith/${slot}/${rank}/${slug}`,
103+
category: "armor",
104+
type: (a.type as string) || slot,
105+
rank,
106+
rarity: Number(a.rarity ?? 0),
107+
elements: "",
108+
skills: skillNames,
109+
});
113110
}
114111
}
115112

116113
// Decorations
117-
const decoFile = path.join(contentDir, "decorations", "decorations-crafting.json");
114+
const decoFile = path.join(dataDir, "decorations.json");
118115
if (fs.existsSync(decoFile)) {
119116
const data = JSON.parse(fs.readFileSync(decoFile, "utf-8")) as {
120-
weapons: Record<string, unknown>[];
117+
decorations: Record<string, unknown>[];
121118
};
122119

123-
for (const d of data.weapons) {
120+
for (const d of data.decorations) {
124121
if ("donotrender" in d || !d.name) continue;
125122

126123
const name = d.name as string;
@@ -144,10 +141,10 @@ if (fs.existsSync(decoFile)) {
144141
}
145142

146143
// Monsters
147-
const monsterDir = path.join(contentDir, "monsters");
148-
for (const category of fs.readdirSync(monsterDir)) {
149-
const jsonFile = path.join(monsterDir, category, `${category}.json`);
150-
if (!fs.existsSync(jsonFile)) continue;
144+
const monsterDir = path.join(dataDir, "monsters");
145+
for (const file of fs.readdirSync(monsterDir)) {
146+
const category = file.replace(/\.json$/, "");
147+
const jsonFile = path.join(monsterDir, file);
151148

152149
const data = JSON.parse(fs.readFileSync(jsonFile, "utf-8")) as {
153150
monsters: Record<string, unknown>[];

‎scripts/generate-weapon-data.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import fs from "fs";
66
import path from "path";
77

88
const root = process.cwd();
9-
const contentDir = path.join(root, "content");
9+
const dataDir = path.join(root, "data");
1010

1111
function slugify(name: string): string {
1212
return name
@@ -47,10 +47,10 @@ interface WeaponEntry {
4747

4848
const weapons: WeaponEntry[] = [];
4949

50-
const weaponDir = path.join(contentDir, "blacksmith");
51-
for (const type of fs.readdirSync(weaponDir)) {
52-
const craftingFile = path.join(weaponDir, type, `${type}-crafting.json`);
53-
if (!fs.existsSync(craftingFile)) continue;
50+
const weaponDir = path.join(dataDir, "weapons");
51+
for (const file of fs.readdirSync(weaponDir)) {
52+
const type = file.replace(/\.json$/, "");
53+
const craftingFile = path.join(weaponDir, file);
5454

5555
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
5656
weapons: Record<string, unknown>[];

‎scripts/validate.ts‎

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@ import { slugify } from "../src/lib/slug";
88

99
const root = process.cwd();
1010
const contentDir = path.join(root, "content");
11-
12-
function isDir(p: string): boolean {
13-
return fs.statSync(p).isDirectory();
14-
}
11+
const dataDir = path.join(root, "data");
1512

1613
let errors = 0;
1714
let warnings = 0;
@@ -34,13 +31,18 @@ function section(title: string) {
3431

3532
section("Validating weapons…");
3633

37-
const weaponDir = path.join(contentDir, "blacksmith");
38-
const weaponTypes = fs.readdirSync(weaponDir).sort().filter((e) => isDir(path.join(weaponDir, e)));
34+
const weaponDataDir = path.join(dataDir, "weapons");
35+
const weaponContentDir = path.join(contentDir, "blacksmith");
36+
const weaponTypes = fs
37+
.readdirSync(weaponDataDir)
38+
.filter((e) => e.endsWith(".json"))
39+
.map((e) => e.replace(/\.json$/, ""))
40+
.sort();
3941

4042
// First pass: collect all slugs across all weapon types
4143
const allWeaponSlugs = new Map<string, Set<string>>(); // type → slugs
4244
for (const type of weaponTypes) {
43-
const craftingFile = path.join(weaponDir, type, `${type}-crafting.json`);
45+
const craftingFile = path.join(weaponDataDir, `${type}.json`);
4446
if (!fs.existsSync(craftingFile)) continue;
4547
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as { weapons: Record<string, unknown>[] };
4648
const slugs = new Set<string>();
@@ -53,11 +55,11 @@ for (const type of weaponTypes) {
5355

5456
// Second pass: validate each type
5557
for (const type of weaponTypes) {
56-
const craftingFile = path.join(weaponDir, type, `${type}-crafting.json`);
57-
const mapFile = path.join(weaponDir, type, "map.json");
58+
const craftingFile = path.join(weaponDataDir, `${type}.json`);
59+
const mapFile = path.join(weaponContentDir, type, "map.json");
5860

5961
if (!fs.existsSync(craftingFile)) {
60-
error(`${type}: missing ${type}-crafting.json`);
62+
error(`${type}: missing data/weapons/${type}.json`);
6163
continue;
6264
}
6365

@@ -143,51 +145,52 @@ for (const type of weaponTypes) {
143145

144146
section("Validating armor…");
145147

146-
const armorDir = path.join(contentDir, "armorsmith");
147-
for (const slot of fs.readdirSync(armorDir).sort().filter((e) => isDir(path.join(armorDir, e)))) {
148-
const slotDir = path.join(armorDir, slot);
148+
const armorDataDir = path.join(dataDir, "armor");
149+
for (const file of fs.readdirSync(armorDataDir).sort().filter((e) => e.endsWith(".json"))) {
150+
const slot = file.replace(/\.json$/, "");
151+
const craftingFile = path.join(armorDataDir, file);
149152

150-
for (const rank of fs.readdirSync(slotDir).sort().filter((e) => isDir(path.join(slotDir, e)))) {
151-
const craftingFile = path.join(slotDir, rank, `${slot}-crafting.json`);
152-
if (!fs.existsSync(craftingFile)) continue;
153-
154-
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
155-
weapons: Record<string, unknown>[];
156-
};
153+
const data = JSON.parse(fs.readFileSync(craftingFile, "utf-8")) as {
154+
armor: Record<string, unknown>[];
155+
};
157156

158-
let count = 0;
159-
const slugs = new Set<string>();
157+
const countByRank = new Map<string, number>();
158+
const slugsByRank = new Map<string, Set<string>>();
160159

161-
for (const a of data.weapons) {
162-
if ("donotrender" in a) continue;
163-
const name = a.name as string | undefined;
164-
if (!name) { error(`${slot}/${rank}: armor missing name`); continue; }
160+
for (const a of data.armor) {
161+
if ("donotrender" in a) continue;
162+
const name = a.name as string | undefined;
163+
const rank = (a.rank as string) || "low-rank";
164+
if (!name) { error(`${slot}/${rank}: armor missing name`); continue; }
165165

166-
const slug = slugify(name);
167-
if (slugs.has(slug)) error(`${slot}/${rank}: duplicate slug "${slug}"`);
168-
slugs.add(slug);
169-
count++;
170-
}
166+
const slug = slugify(name);
167+
const slugs = slugsByRank.get(rank) ?? new Set<string>();
168+
if (slugs.has(slug)) error(`${slot}/${rank}: duplicate slug "${slug}"`);
169+
slugs.add(slug);
170+
slugsByRank.set(rank, slugs);
171+
countByRank.set(rank, (countByRank.get(rank) ?? 0) + 1);
172+
}
171173

172-
console.log(` ${slot}/${rank}: ${count} pieces`);
174+
for (const rank of ["low-rank", "high-rank", "g-rank"]) {
175+
console.log(` ${slot}/${rank}: ${countByRank.get(rank) ?? 0} pieces`);
173176
}
174177
}
175178

176179
// ─── Decorations ──────────────────────────────────────────────────────────────
177180

178181
section("Validating decorations…");
179182

180-
const decoFile = path.join(contentDir, "decorations", "decorations-crafting.json");
183+
const decoFile = path.join(dataDir, "decorations.json");
181184
if (!fs.existsSync(decoFile)) {
182-
error("Missing decorations/decorations-crafting.json");
185+
error("Missing data/decorations.json");
183186
} else {
184187
const data = JSON.parse(fs.readFileSync(decoFile, "utf-8")) as {
185-
weapons: Record<string, unknown>[];
188+
decorations: Record<string, unknown>[];
186189
};
187190
const slugs = new Set<string>();
188191
let count = 0;
189192

190-
for (const d of data.weapons) {
193+
for (const d of data.decorations) {
191194
if ("donotrender" in d) continue;
192195
const name = d.name as string | undefined;
193196
if (!name) { error("decoration missing name"); continue; }
@@ -205,13 +208,10 @@ if (!fs.existsSync(decoFile)) {
205208

206209
section("Validating monsters…");
207210

208-
const monsterDir = path.join(contentDir, "monsters");
209-
for (const category of fs.readdirSync(monsterDir).sort().filter((e) => isDir(path.join(monsterDir, e)))) {
210-
const jsonFile = path.join(monsterDir, category, `${category}.json`);
211-
if (!fs.existsSync(jsonFile)) {
212-
warn(`monsters/${category}: missing ${category}.json`);
213-
continue;
214-
}
211+
const monsterDataDir = path.join(dataDir, "monsters");
212+
for (const file of fs.readdirSync(monsterDataDir).sort().filter((e) => e.endsWith(".json"))) {
213+
const category = file.replace(/\.json$/, "");
214+
const jsonFile = path.join(monsterDataDir, file);
215215

216216
const data = JSON.parse(fs.readFileSync(jsonFile, "utf-8")) as {
217217
monsters: Record<string, unknown>[];

0 commit comments

Comments
 (0)