enefel 2.9.1 → 2.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 (77) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/2025/newSkills.ts +769 -0
  3. package/2025/raceMigration.ts +66 -0
  4. package/2025/rawTeamsData.ts +1726 -0
  5. package/2025/skillMigration.ts +155 -0
  6. package/avatarsSeason.ts +61 -0
  7. package/career.ts +64 -8
  8. package/career2025.ts +2752 -0
  9. package/dice.ts +40 -8
  10. package/dist/2025/newSkills.d.ts +10 -0
  11. package/dist/2025/newSkills.js +663 -0
  12. package/dist/2025/raceMigration.d.ts +10 -0
  13. package/dist/2025/raceMigration.js +53 -0
  14. package/dist/2025/rawTeamsData.d.ts +12 -0
  15. package/dist/2025/rawTeamsData.js +1699 -0
  16. package/dist/2025/skillMigration.d.ts +31 -0
  17. package/dist/2025/skillMigration.js +95 -0
  18. package/dist/avatarsSeason.d.ts +22 -0
  19. package/dist/avatarsSeason.js +54 -0
  20. package/dist/career.d.ts +13 -4
  21. package/dist/career.js +36 -7
  22. package/dist/career2025.d.ts +192 -0
  23. package/dist/career2025.js +2611 -0
  24. package/dist/dice.d.ts +10 -2
  25. package/dist/dice.js +31 -8
  26. package/dist/index.d.ts +17 -2
  27. package/dist/index.js +44 -5
  28. package/dist/move.d.ts +6 -2
  29. package/dist/move.js +35 -0
  30. package/dist/package-lock.json +913 -7
  31. package/dist/package.json +6 -2
  32. package/dist/pitch.d.ts +10 -1
  33. package/dist/pitch.js +16 -0
  34. package/dist/player.d.ts +8 -4
  35. package/dist/player.js +104 -20
  36. package/dist/position.d.ts +4 -0
  37. package/dist/position.js +17 -1
  38. package/dist/race.d.ts +23 -12
  39. package/dist/race.js +210 -2
  40. package/dist/skill.d.ts +26 -3
  41. package/dist/skill.js +199 -81
  42. package/dist/skills/hitAndRun.d.ts +10 -0
  43. package/dist/skills/hitAndRun.js +12 -0
  44. package/dist/skills/safePairOfHands.d.ts +28 -0
  45. package/dist/skills/safePairOfHands.js +18 -0
  46. package/dist/skills/sidestep.d.ts +21 -0
  47. package/dist/skills/sidestep.js +20 -0
  48. package/dist/status.d.ts +4 -1
  49. package/dist/status.js +14 -4
  50. package/dist/store.d.ts +3 -0
  51. package/dist/store.js +3 -0
  52. package/dist/teams/career_v2025.d.ts +246 -0
  53. package/dist/teams/career_v2025.js +3512 -0
  54. package/dist/teams/convert-csv-to-career.d.ts +1 -0
  55. package/dist/teams/convert-csv-to-career.js +1085 -0
  56. package/dist/types/models.d.ts +4 -0
  57. package/index.ts +57 -4
  58. package/jest.config.js +3 -0
  59. package/move.ts +50 -2
  60. package/npm-login.sh +0 -0
  61. package/package.json +6 -2
  62. package/pitch.ts +22 -0
  63. package/player.ts +105 -22
  64. package/position.ts +16 -0
  65. package/race.ts +227 -13
  66. package/skill.ts +217 -83
  67. package/skills/hitAndRun.ts +14 -0
  68. package/skills/safePairOfHands.ts +33 -0
  69. package/skills/sidestep.ts +25 -0
  70. package/status.ts +15 -3
  71. package/store.ts +3 -0
  72. package/teams/README.md +53 -0
  73. package/teams/clean-stats.js +54 -0
  74. package/teams/convert-csv-to-career.ts +1209 -0
  75. package/teams/players_clean.csv +107 -0
  76. package/teams/players_next.csv +21 -0
  77. package/types/models.ts +4 -0
@@ -0,0 +1,1209 @@
1
+ import { parse } from "csv-parse/sync";
2
+ import fs from "fs";
3
+
4
+ const CSV_PATH = "./teams/players_clean.csv";
5
+ const OUTPUT_PATH = "./career2025.ts";
6
+ const SKILL_FILE_PATH = "./skill.ts";
7
+ const CAREER_FILE_PATH = "./career.ts";
8
+
9
+ // Icônes déjà présentes côté client (sans l'extension .gif)
10
+ const KNOWN_ICONS = new Set<string>([
11
+ "amazon-blitzer1",
12
+ "amazon-catcher1",
13
+ "amazon-linewomen1",
14
+ "amazon-thrower1",
15
+ "chaos-beastman1",
16
+ "chaos-dwarf-blocker1",
17
+ "chaos-dwarf-bull-centaur1",
18
+ "chaos-dwarf-hobgobelin1",
19
+ "chaos-minotaur1",
20
+ "chaos-warrior1",
21
+ "dark-elfe-blitzer1",
22
+ "dark-elfe-lineman1",
23
+ "dark-elfe-runner1",
24
+ "dark-elfe-witch1",
25
+ "dwarf-blitzer1",
26
+ "dwarf-deathroller1",
27
+ "dwarf-runner1",
28
+ "dwarf-troll-slayer1",
29
+ "dwarf1",
30
+ "elven-union-blitzer1",
31
+ "elven-union-catcher1",
32
+ "elven-union-lineman1",
33
+ "elven-union-thrower1",
34
+ "gobelin-lineman1",
35
+ "gobelin-looney1",
36
+ "gobelin-pogoer1",
37
+ "gobelin-troll1",
38
+ "halfling-treeman1",
39
+ "human-blitzer1",
40
+ "human-catcher1",
41
+ "human-ogre1",
42
+ "human-thrower1",
43
+ "human1",
44
+ "imperial-blitzer1",
45
+ "imperial-bodyguard1",
46
+ "imperial-lineman1",
47
+ "imperial-ogre1",
48
+ "imperial-thrower1",
49
+ "lizardmen-chameleon1",
50
+ "lizardmen-kroxigor1",
51
+ "lizardmen-saurus1",
52
+ "lizardmen-skink1",
53
+ "norse-berserker1",
54
+ "norse-catcher1",
55
+ "norse-lineman1",
56
+ "norse-thrower1",
57
+ "norse-ulfwereners1",
58
+ "norse-yhetee1",
59
+ "orc-black-orc1",
60
+ "orc-blitzer1",
61
+ "orc-thrower1",
62
+ "orc-troll1",
63
+ "orc1",
64
+ "rat-blitzer1",
65
+ "rat-ogre1",
66
+ "rat-runner1",
67
+ "rat-thrower1",
68
+ "rat1",
69
+ "silvan-catcher1",
70
+ "silvan-thrower1",
71
+ "silvan-wardancer1",
72
+ "silvan1",
73
+ "slann-blitzer1",
74
+ "slann-catcher1",
75
+ "slann-kroxigor1",
76
+ "slann-lineman1",
77
+ "snotling-lineman1",
78
+ "undead-ghoul1",
79
+ "undead-mummies1",
80
+ "undead-skeleton1",
81
+ "undead-wight1",
82
+ "undead-zombie1",
83
+ "vampire-thrall1",
84
+ "vampire-vampire1",
85
+ ]);
86
+
87
+ // Adapter icônes générées -> icônes existantes
88
+ const ICON_ADAPTER: Record<string, string> = {
89
+ // Amazon
90
+ "amazon-linewoman1": "amazon-linewomen1",
91
+ "amazon-blocker1": "amazon-blitzer1",
92
+
93
+ // Chaos Chosen
94
+ "chaos-chosen-beastman1": "chaos-beastman1",
95
+ "chaos-chosen-chosen1": "chaos-warrior1",
96
+ "chaos-chosen-minotaur1": "chaos-minotaur1",
97
+
98
+ // Chaos Dwarf
99
+ "chaos-dwarf-hobgoblin1": "chaos-dwarf-hobgobelin1",
100
+ "chaos-dwarf-stabba1": "chaos-dwarf-hobgobelin1",
101
+ "chaos-dwarf-flamer1": "chaos-dwarf-blocker1",
102
+ "chaos-dwarf-minotaur1": "chaos-minotaur1",
103
+
104
+ // Dark Elf
105
+ "dark-elf-lineman1": "dark-elfe-lineman1",
106
+ "dark-elf-runner1": "dark-elfe-runner1",
107
+ "dark-elf-assassin1": "dark-elfe-blitzer1",
108
+ "dark-elf-blitzer1": "dark-elfe-blitzer1",
109
+ "dark-elf-witch-elf1": "dark-elfe-witch1",
110
+
111
+ // Dwarf
112
+ "dwarf-lineman1": "dwarf1",
113
+
114
+ // Human
115
+ "human-lineman1": "human1",
116
+
117
+ // Orc
118
+ "orc-lineman1": "orc1",
119
+ "orc-big-un1": "orc-black-orc1",
120
+
121
+ // Goblin
122
+ "goblin-lineman1": "gobelin-lineman1",
123
+ "goblin-loony1": "gobelin-looney1",
124
+ "goblin-pogoer1": "gobelin-pogoer1",
125
+ "goblin-troll1": "gobelin-troll1",
126
+ "goblin-ooligan1": "gobelin-lineman1",
127
+ "goblin-doom-diver1": "gobelin-pogoer1",
128
+
129
+ // Halfling (fallback V6)
130
+ "halfling-lineman1": "halfling-lineman1",
131
+ "halfling-hefty1": "halfling-hefty1",
132
+ "halfling-catcher1": "halfling-catcher1",
133
+
134
+ // Imperial Nobility (fallback V6)
135
+ "imperial-nobility-blitzer1": "imperial-blitzer1",
136
+ "imperial-nobility-bodyguard1": "imperial-bodyguard1",
137
+ "imperial-nobility-ogre1": "imperial-ogre1",
138
+ "imperial-nobility-retainer1": "imperial-lineman1",
139
+ "imperial-nobility-thrower1": "imperial-thrower1",
140
+
141
+ // Khorne (fallback V6)
142
+ "khorne-marauder1": "khorne-lineman1",
143
+ "khorne-khorngor1": "khorne-khorngor1",
144
+ "khorne-bloodseeker1": "khorne-bloodseeker1",
145
+ "khorne-bloodspawn1": "khorne-bloodspawn1",
146
+
147
+ // Necromantic Horror (fallback V6 necromantic)
148
+ "necromantic-horror-zombie1": "necromantic-zombie1",
149
+ "necromantic-horror-ghoul1": "necromantic-ghoul1",
150
+ "necromantic-horror-wraith1": "necromantic-wraith1",
151
+ "necromantic-horror-werewolf1": "necromantic-werewolf1",
152
+ "necromantic-horror-flesh-golem1": "necromantic-golem1",
153
+
154
+ // Norse (fallback V6)
155
+ "norse-raider1": "norse-lineman1",
156
+ "norse-ulfwerner1": "norse-ulfwereners1",
157
+ "norse-valkyrie1": "norse-catcher1",
158
+
159
+ // Nurgle (fallback V6)
160
+ "nurgle-rotter1": "nurgle-rotter1",
161
+ "nurgle-pestigor1": "nurgle-pestigor1",
162
+ "nurgle-bloater1": "nurgle-bloater1",
163
+ "nurgle-rotspawn1": "nurgle-rotspawn1",
164
+
165
+ // Skaven (fallback V6)
166
+ "skaven-thrower1": "rat-thrower1",
167
+ "skaven-gutter1": "rat-runner1",
168
+ "skaven-blitzer1": "rat-blitzer1",
169
+ "skaven-rat-ogre1": "rat-ogre1",
170
+
171
+ // Snotling (fallback V6 gobelin/snotling)
172
+ "snotling-hoppa1": "snotling-runner1",
173
+ "snotling-runna1": "snotling-lineman1",
174
+ "snotling-pump-wagon1": "snotling-wagon1",
175
+ "snotling-troll1": "snotling-troll1",
176
+
177
+ // Vampire (fallback V6)
178
+ "vampire-blitzer1": "vampire-vampire1",
179
+ "vampire-runner1": "vampire-vampire1",
180
+ "vampire-thrower1": "vampire-vampire1",
181
+ "vampire-vargheist1": "vampire-vampire1",
182
+
183
+ // Shambling Undead
184
+ "shambling-undead-skeleton1": "undead-skeleton1",
185
+ "shambling-undead-zombie1": "undead-zombie1",
186
+ "shambling-undead-ghoul1": "undead-ghoul1",
187
+ "shambling-undead-wight1": "undead-wight1",
188
+ "shambling-undead-mummy1": "undead-mummies1",
189
+
190
+ // Skaven
191
+ "skaven-clanrat1": "rat1",
192
+
193
+ // Wood Elf
194
+ "wood-elf-lineman1": "silvan1",
195
+ "wood-elf-thrower1": "silvan-thrower1",
196
+ "wood-elf-catcher1": "silvan-catcher1",
197
+ "wood-elf-wardancer1": "silvan-wardancer1",
198
+ "wood-elf-treeman1": "halfling-treeman1",
199
+
200
+ // High Elf (fallback vers Elven Union)
201
+ "high-elf-lineman1": "elven-union-lineman1",
202
+ "high-elf-thrower1": "elven-union-thrower1",
203
+ "high-elf-catcher1": "elven-union-catcher1",
204
+ "high-elf-blitzer1": "elven-union-blitzer1",
205
+
206
+ // Tomb King (fallback vers Undead)
207
+ "tomb-king-lineman1": "undead-skeleton1",
208
+ "tomb-king-thrower1": "undead-ghoul1",
209
+ "tomb-king-blitzer1": "undead-wight1",
210
+ "tomb-king-tomb-guardian1": "undead-mummies1",
211
+ };
212
+ interface CSVRow {
213
+ Team: string;
214
+ Quantity: string;
215
+ Position: string;
216
+ Keywords: string;
217
+ Cost: string;
218
+ Skills: string;
219
+ Primary: string;
220
+ Secondary: string;
221
+ MA: string;
222
+ ST: string;
223
+ AG: string;
224
+ PA: string;
225
+ AR: string;
226
+ }
227
+
228
+ const SKILL_NAME_MAP: Record<string, string> = {
229
+ "Always Hungry": "always-hungry",
230
+ "Animal Savagery": "animal-salvagery",
231
+ Animosity: "animosity",
232
+ "Big Guy": "big-guy",
233
+ Block: "block",
234
+ "Bone Head": "bone-head",
235
+ Brawler: "brawler",
236
+ "Break Tackle": "break-tackle",
237
+ Catch: "catch",
238
+ Chainsaw: "chainsaw",
239
+ Claws: "claw",
240
+ "Cloud Burster": "cloud-burster",
241
+ Dauntless: "dauntless",
242
+ Decay: "decay",
243
+ Defensive: "defensive",
244
+ "Dirty Player": "dirty-player",
245
+ "Disturbing Presence": "disturbing-presence",
246
+ "Diving Catch": "diving-catch",
247
+ "Diving Tackle": "diving-tackle",
248
+ Dodge: "dodge",
249
+ Drunkard: "drunkard",
250
+ "Dump-Off": "dump-off",
251
+ Fend: "fend",
252
+ "Foul Appearance": "foul-appearance",
253
+ Frenzy: "frenzy",
254
+ Fumblerooski: "fumblerooskie",
255
+ "Give and Go": "running-pass", // Mapped to GIVE_AND_GO for DB compatibility
256
+ Grab: "grab",
257
+ "Hail Mary Pass": "hail-mary-pass",
258
+ Horns: "horns",
259
+ "Hypnotic Gaze": "hypnotic-gaze",
260
+ "Iron Hard Skin": "iron-hard-skin",
261
+ Juggernaut: "juggernaut",
262
+ "Jump Up": "jump-up",
263
+ Leap: "leap",
264
+ Loner: "loner",
265
+ "Mighty Blow": "mighty-blow",
266
+ "Nerves of Steel": "nerves-of-steel",
267
+ "No Ball": "no-hand",
268
+ "No Hands": "no-hand",
269
+ "On The Ball": "on-the-ball",
270
+ "On the Ball": "on-the-ball",
271
+ Pass: "pass",
272
+ "Pick-me-up": "pick-me-up",
273
+ "Plague Ridden": "plague-ridden",
274
+ "Pogo Stick": "pogo-stick",
275
+ Pogo: "pogo-stick",
276
+ "Prehensile Tail": "prehensile-tail",
277
+ "Projectile Vomit": "projectile-vomit",
278
+ Pro: "pro",
279
+ Punt: "punt",
280
+ "Really Stupid": "really-stupid",
281
+ Regeneration: "regeneration",
282
+ "Right Stuff": "right-stuff",
283
+ "Safe Pass": "safe-pass",
284
+ "Safe Pair of Hands": "safe-pair-of-hands",
285
+ "Secret Weapon": "secret-weapon",
286
+ Shadowing: "shadowing",
287
+ Sidestep: "sidestep",
288
+ Sprint: "sprint",
289
+ "Stand Firm": "stand-firm",
290
+ Star: "star",
291
+ "Strip Ball": "strip-ball",
292
+ "Strong Arm": "strong-arm",
293
+ Stunty: "stunty",
294
+ "Sure Feet": "sure-feet",
295
+ "Sure Hands": "sure-hands",
296
+ Stab: "stab",
297
+ Tackle: "tackle",
298
+ "Take Root": "take-root",
299
+ Taunt: "taunt",
300
+ Tentacles: "tentacles",
301
+ "Thick Skull": "thick-skull",
302
+ "Throw Team-mate": "throw-team-mate",
303
+ Titchy: "titchy",
304
+ "Unchannelled Fury": "unchannelled-fury",
305
+ Unsteady: "unsteady",
306
+ Wrestle: "wrestle",
307
+ "Eye Gouge": "eye-gouge",
308
+ "Hit and Run": "hit-and-run",
309
+ "Ball & Chain": "ball-and-chain",
310
+ Bombardier: "bombardier",
311
+ "Breathe Fire": "breathe-fire",
312
+ Insignificant: "insignificant",
313
+ "Kick Team-mate": "kick-team-mate",
314
+ "Bloodlust (2+)": "bloodlust",
315
+ "Bloodlust (3+)": "bloodlust",
316
+ "Hatred (Troll)": "hatred-troll",
317
+ "Timmm-ber!": "timmm-ber",
318
+ Swoop: "swoop",
319
+ "Steady Footing": "steady-footing",
320
+ "Arm Bar": "arm-bar",
321
+ };
322
+
323
+ const TEAM_TO_RACE_MAP: Record<string, string> = {
324
+ Amazon: "amazon-career",
325
+ "Black Orc": "black-orc-career",
326
+ Bretonnian: "bretonnian-career",
327
+ "Chaos Chosen": "chaos-career",
328
+ "Chaos Dwarf": "chaos-dwarf-career",
329
+ "Chaos Renegate": "chaos-renegate-career",
330
+ "Dark Elf": "dark-elfe-career",
331
+ Dwarf: "dwarf-career",
332
+ "Elven Union": "elven-union-career",
333
+ Goblin: "gobelin-career",
334
+ Halfling: "halfling-career",
335
+ "High Elf": "high-elf-career",
336
+ Human: "human-career",
337
+ "Imperial Nobility": "imperial-career",
338
+ Khorne: "khorne-career",
339
+ Lizardmen: "lizardmen-career",
340
+ "Necromantic Horror": "necromantic-career",
341
+ Norse: "norse-career",
342
+ Nurgle: "nurgle-career",
343
+ Ogre: "ogre-career",
344
+ "Old World Alliance": "old-world-alliance-career",
345
+ Orc: "orc-career",
346
+ "Shambling Undead": "undead-career",
347
+ Skaven: "rat-career",
348
+ Snotling: "snotling-career",
349
+ "Tomb King": "tomb-king-career",
350
+ "Underworld Denizens": "underworld-denizens-career",
351
+ Vampire: "vampire-career",
352
+ "Wood Elf": "silvan-career",
353
+ };
354
+
355
+ const KEYWORD_TO_AVATAR_MAP: Record<string, string> = {
356
+ Human: "human",
357
+ "Human, Lineman": "human",
358
+ "Human, Thrower": "human",
359
+ "Human, Thorwer": "human",
360
+ "Human, Skeleton": "undead",
361
+ "Human, Thrall": "human",
362
+ "Human Woman": "human-woman",
363
+ "Human, Blocker": "human",
364
+ "Human, Blitzer": "human",
365
+ "Human, Catcher": "human",
366
+ "Human, Runner": "human",
367
+ "Human, Special": "human",
368
+ Goblin: "gobelin",
369
+ "Goblin, Lineman": "gobelin",
370
+ "Goblin, Special": "gobelin",
371
+ Orc: "orc",
372
+ "Lineman, Orc": "orc",
373
+ "Orc, Thrower": "orc",
374
+ "Orc, Blitzer": "orc",
375
+ "Blocker, Orc": "black-orc",
376
+ Dwarf: "dwarf",
377
+ "Dwarf, Lineman": "dwarf",
378
+ "Dwarf, Runner": "dwarf",
379
+ "Dwarf, Blitzer": "dwarf",
380
+ "Dwarf, Special": "dwarf",
381
+ "Blocker, Dwarf": "dwarf",
382
+ Elf: "elf",
383
+ "Elf, Lineman": "elf",
384
+ "Elf, Thrower": "elf",
385
+ "Elf, Runner": "elf",
386
+ "Elf, Special": "elf",
387
+ "Blitzer, Elf": "elf",
388
+ "Catcher, Elf": "elf",
389
+ "Dark Elf": "dark_elf",
390
+ Halfling: "halfing",
391
+ "Halfling, Lineman": "halfing",
392
+ "Blocker, Halfling": "halfing",
393
+ "Catcher, Halfling": "halfing",
394
+ Skaven: "rat",
395
+ "Lineman, Skaven": "rat",
396
+ "Skaven, Thrower": "rat",
397
+ "Runner, Skaven": "rat",
398
+ "Blitzer, Skaven": "rat",
399
+ Lizardman: "lezard",
400
+ "Lineman, Lizardman": "lezard",
401
+ "Lizardman, Thrower": "lezard",
402
+ "Blocker, Lizardman": "lezard",
403
+ Beastman: "beastman",
404
+ "Beastman, Lineman": "beastman",
405
+ "Beastman, Runner": "beastman",
406
+ Undead: "undead",
407
+ "Undead, Zombie": "undead",
408
+ "Undead, Wraith": "undead",
409
+ "Undead, Werewolf": "werewolf",
410
+ "Undead, Vampire": "human",
411
+ Skeleton: "undead",
412
+ "Human, Lineman, Skeleton, Undead": "undead",
413
+ "Human, Skeleton, Thrower, Undead": "undead",
414
+ "Blitzer, Human, Skeleton, Undead": "undead",
415
+ "Big Guy, Blocker, Human, Undead": "mummie",
416
+ Zombie: "undead",
417
+ "Human, Lineman, Undead, Zombie": "undead",
418
+ Ghoul: "undead",
419
+ "Ghoul, Runner, Undead": "undead",
420
+ Wight: "undead",
421
+ Mummy: "mummie",
422
+ Troll: "troll",
423
+ "Big Guy, Troll": "troll",
424
+ Ogre: "ogre",
425
+ "Big Guy, Ogre": "ogre",
426
+ "Big Guy, Blocker, Ogre": "ogre",
427
+ "Big Guy, Ogre, Thrower": "ogre",
428
+ Minotaur: "minotaur",
429
+ "Big Guy, Minotaur": "minotaur",
430
+ "Rat Ogre": "rat_ogre",
431
+ "Big Guy, Skaven": "rat_ogre",
432
+ Treeman: "treeman",
433
+ "Big Guy, Treeman": "treeman",
434
+ Spawn: "rotspawn",
435
+ "Big Guy, Spawn": "rotspawn",
436
+ Yhetee: "yhetee",
437
+ "Big Guy, Yhetee": "yhetee",
438
+ Gnoblar: "gobelin",
439
+ "Gnoblar, Lineman": "gobelin",
440
+ Snotling: "gobelin",
441
+ "Lineman, Snotling": "gobelin",
442
+ "Snotling, Special": "gobelin",
443
+ "Runner, Snotling": "gobelin",
444
+ "Big Guy, Snotling, Special": "wagon",
445
+ Construct: "golem",
446
+ "Blocker, Construct, Undead": "golem",
447
+ Centaur: "centaur",
448
+ "Blitzer, Dwarf": "centaur",
449
+ Animal: "gobelin",
450
+ "Animal, Special": "gobelin",
451
+ };
452
+
453
+ function enforceSpecialSkills(
454
+ skills: (string | [string, number])[]
455
+ ): (string | [string, number])[] {
456
+ const specials = ["big-guy", "star", "specialist"];
457
+ const present = new Set<string>();
458
+ skills.forEach((skill) => {
459
+ if (typeof skill === "string" && specials.includes(skill)) {
460
+ present.add(skill);
461
+ }
462
+ });
463
+
464
+ const orderedKeep: string[] = [];
465
+ if (present.has("big-guy")) orderedKeep.push("big-guy");
466
+ else if (present.has("star")) orderedKeep.push("star");
467
+ else if (present.has("specialist")) orderedKeep.push("specialist");
468
+ const allowed = new Set(orderedKeep.slice(0, 1));
469
+
470
+ const filtered: (string | [string, number])[] = [];
471
+ for (const skill of skills) {
472
+ if (typeof skill === "string" && specials.includes(skill)) {
473
+ if (allowed.has(skill)) {
474
+ filtered.push(skill);
475
+ }
476
+ } else {
477
+ filtered.push(skill);
478
+ }
479
+ }
480
+ return filtered;
481
+ }
482
+
483
+ function parseStat(stat: string): number {
484
+ if (!stat || stat === "") return 0;
485
+ const match = stat.match(/^(\d+)/);
486
+ return match ? parseInt(match[1], 10) : 0;
487
+ }
488
+
489
+ function convertAGPAFrom2025To2020(value: number): number {
490
+ if (value <= 0) return 0;
491
+ return 7 - value;
492
+ }
493
+
494
+ function convertAVFrom2025To2020(value: number): number {
495
+ if (value <= 0) return 0;
496
+ return value - 1;
497
+ }
498
+
499
+ function parseCost(cost: string): number {
500
+ if (!cost || cost === "") return 0;
501
+ const match = cost.match(/^(\d+)/);
502
+ return match ? parseInt(match[1], 10) : 0;
503
+ }
504
+
505
+ function parseSkills(skillsStr: string): (string | [string, number])[] {
506
+ if (!skillsStr || skillsStr.trim() === "") return [];
507
+
508
+ const skills: (string | [string, number])[] = [];
509
+ const skillParts = skillsStr.split(",").map((s) => s.trim());
510
+
511
+ for (const skillPart of skillParts) {
512
+ const lonerMatch = skillPart.match(/^Loner \(([\d+]+)\)$/i);
513
+ if (lonerMatch) {
514
+ const value = parseInt(lonerMatch[1].replace("+", ""), 10);
515
+ skills.push(["loner", value]);
516
+ continue;
517
+ }
518
+
519
+ const mightyBlowMatch = skillPart.match(/^Mighty Blow \(\+?(\d+)\)$/i);
520
+ if (mightyBlowMatch) {
521
+ const value = parseInt(mightyBlowMatch[1], 10);
522
+ skills.push(["mighty-blow", value]);
523
+ continue;
524
+ }
525
+
526
+ const dirtyPlayerMatch = skillPart.match(/^Dirty Player \((\d+)\)$/i);
527
+ if (dirtyPlayerMatch) {
528
+ const value = parseInt(dirtyPlayerMatch[1], 10);
529
+ skills.push(["dirty-player", value]);
530
+ continue;
531
+ }
532
+
533
+ const bloodlustMatch = skillPart.match(/^Bloodlust \(([\d+]+)\)$/i);
534
+ if (bloodlustMatch) {
535
+ const value = parseInt(bloodlustMatch[1].replace("+", ""), 10);
536
+ skills.push(["bloodlust", value]);
537
+ continue;
538
+ }
539
+
540
+ const animosityMatch = skillPart.match(/^Animosity \(([^)]+)\)$/i);
541
+ if (animosityMatch) {
542
+ skills.push("animosity");
543
+ continue;
544
+ }
545
+
546
+ const hatredMatch = skillPart.match(/^Hatred \(([^)]+)\)$/i);
547
+ if (hatredMatch) {
548
+ skills.push("hatred-troll");
549
+ continue;
550
+ }
551
+
552
+ const normalizedSkill = skillPart
553
+ .replace(/^\(/, "")
554
+ .replace(/\)$/, "")
555
+ .trim();
556
+
557
+ const skillKey = Object.keys(SKILL_NAME_MAP).find(
558
+ (key) => key.toLowerCase() === normalizedSkill.toLowerCase()
559
+ );
560
+
561
+ if (skillKey) {
562
+ skills.push(SKILL_NAME_MAP[skillKey]);
563
+ } else {
564
+ console.warn(`⚠️ Skill non mappé: "${skillPart}"`);
565
+ }
566
+ }
567
+
568
+ return skills;
569
+ }
570
+
571
+ function toKebabCase(str: string): string {
572
+ return str
573
+ .toLowerCase()
574
+ .replace(/[^a-z0-9]+/g, "-")
575
+ .replace(/^-+|-+$/g, "");
576
+ }
577
+
578
+ function generateCareerId(team: string, position: string): string {
579
+ const teamKebab = toKebabCase(team);
580
+ const positionKebab = toKebabCase(position);
581
+ return `${teamKebab}-${positionKebab}-2025`;
582
+ }
583
+
584
+ function generateRaceEnum(team: string): string {
585
+ const race = TEAM_TO_RACE_MAP[team] || toKebabCase(team) + "-career";
586
+ return race.toUpperCase().replace(/-/g, "_");
587
+ }
588
+
589
+ function getExistingAvatars(): Set<string> {
590
+ const careerFileContent = fs.readFileSync(CAREER_FILE_PATH, "utf-8");
591
+ const existingAvatars = new Set<string>();
592
+
593
+ const enumMatch = careerFileContent.match(/enum AVATAR \{([^}]+)\}/s);
594
+ if (enumMatch) {
595
+ const enumContent = enumMatch[1];
596
+ const avatarMatches = enumContent.matchAll(/(\w+)\s*=\s*"([^"]+)"/g);
597
+ for (const match of avatarMatches) {
598
+ existingAvatars.add(match[2]);
599
+ }
600
+ }
601
+
602
+ const avatarMatches = careerFileContent.matchAll(/avatars:\s*\[([^\]]+)\]/g);
603
+ for (const match of avatarMatches) {
604
+ const avatarsStr = match[1];
605
+ const avatarMatches2 = avatarsStr.matchAll(/AVATAR\.(\w+)/g);
606
+ for (const avatarMatch of avatarMatches2) {
607
+ const avatarKey = avatarMatch[1];
608
+ const enumMatch2 = careerFileContent.match(
609
+ new RegExp(`${avatarKey}\\s*=\\s*"([^"]+)"`)
610
+ );
611
+ if (enumMatch2) {
612
+ existingAvatars.add(enumMatch2[1]);
613
+ }
614
+ }
615
+ }
616
+
617
+ return existingAvatars;
618
+ }
619
+
620
+ function getAvatarFromKeywords(
621
+ keywords: string,
622
+ position: string,
623
+ team: string
624
+ ): string {
625
+ const keywordParts = keywords.split(",").map((k) => k.trim());
626
+ let avatar: string | null = null;
627
+
628
+ for (const keyword of keywordParts) {
629
+ const mappedAvatar = KEYWORD_TO_AVATAR_MAP[keyword];
630
+ if (mappedAvatar) {
631
+ avatar = mappedAvatar;
632
+ break;
633
+ }
634
+ }
635
+
636
+ if (!avatar) {
637
+ const posLower = position.toLowerCase();
638
+ const teamLower = team.toLowerCase();
639
+
640
+ if (posLower.includes("troll")) {
641
+ avatar = "troll";
642
+ } else if (posLower.includes("ogre")) {
643
+ avatar = "ogre";
644
+ } else if (posLower.includes("treeman")) {
645
+ avatar = "treeman";
646
+ } else if (posLower.includes("yhetee")) {
647
+ avatar = "yhetee";
648
+ } else if (posLower.includes("minotaur")) {
649
+ avatar = "minotaur";
650
+ } else if (posLower.includes("rat ogre") || posLower.includes("ratogre")) {
651
+ avatar = "rat_ogre";
652
+ } else if (posLower.includes("werewolf")) {
653
+ avatar = "werewolf";
654
+ } else if (posLower.includes("vampire")) {
655
+ avatar = "vampire";
656
+ } else if (posLower.includes("golem")) {
657
+ avatar = "golem";
658
+ } else if (posLower.includes("centaur")) {
659
+ avatar = "centaur";
660
+ } else if (posLower.includes("wagon")) {
661
+ avatar = "wagon";
662
+ } else if (teamLower.includes("elf") || teamLower.includes("elven")) {
663
+ avatar = "elf";
664
+ } else if (teamLower.includes("dwarf")) {
665
+ avatar = "dwarf";
666
+ } else if (teamLower.includes("orc")) {
667
+ avatar = "orc";
668
+ } else if (teamLower.includes("goblin") || teamLower.includes("snotling")) {
669
+ avatar = "gobelin";
670
+ } else {
671
+ avatar = "human";
672
+ }
673
+ }
674
+
675
+ return avatar;
676
+ }
677
+
678
+ function determineRange(quantity: string): number {
679
+ if (quantity.includes("0-16") || quantity.includes("0-12")) return 0;
680
+ if (quantity.includes("0-6")) return 5;
681
+ if (quantity.includes("0-4")) return 4;
682
+ if (quantity.includes("0-3")) return 3;
683
+ if (quantity.includes("0-2")) return 2;
684
+ if (quantity.includes("0-1")) return 6;
685
+ return 0;
686
+ }
687
+
688
+ function getExistingSkills(): Set<string> {
689
+ const skillFileContent = fs.readFileSync(SKILL_FILE_PATH, "utf-8");
690
+ const existingSkills = new Set<string>();
691
+
692
+ const enumMatch = skillFileContent.match(/enum SKILL_NAMES \{([^}]+)\}/s);
693
+ if (enumMatch) {
694
+ const enumContent = enumMatch[1];
695
+ const skillMatches = enumContent.matchAll(/(\w+)\s*=\s*"([^"]+)"/g);
696
+ for (const match of skillMatches) {
697
+ existingSkills.add(match[2]);
698
+ }
699
+ }
700
+
701
+ // Alias pour la faute d'orthographe historique présente dans skill.ts
702
+ if (existingSkills.has("distrubing-presence")) {
703
+ existingSkills.add("disturbing-presence");
704
+ }
705
+
706
+ return existingSkills;
707
+ }
708
+
709
+ function getCommentedSkills(): Set<string> {
710
+ const skillFileContent = fs.readFileSync(SKILL_FILE_PATH, "utf-8");
711
+ const commentedSkills = new Set<string>();
712
+
713
+ // Chercher les compétences commentées dans les tableaux (format: // SKILL_NAMES.XXX)
714
+ const commentedMatches = skillFileContent.matchAll(
715
+ /\/\/\s*SKILL_NAMES\.(\w+)/g
716
+ );
717
+
718
+ // Lire l'enum SKILL_NAMES pour mapper les clés aux valeurs
719
+ const enumMatch = skillFileContent.match(/enum SKILL_NAMES \{([^}]+)\}/s);
720
+ const enumToValueMap: Record<string, string> = {};
721
+ if (enumMatch) {
722
+ const enumContent = enumMatch[1];
723
+ const skillMatches = enumContent.matchAll(/(\w+)\s*=\s*"([^"]+)"/g);
724
+ for (const match of skillMatches) {
725
+ enumToValueMap[match[1]] = match[2];
726
+ }
727
+ }
728
+
729
+ // Convertir les enum keys commentées en skill values
730
+ for (const match of commentedMatches) {
731
+ const enumKey = match[1];
732
+ if (enumToValueMap[enumKey]) {
733
+ commentedSkills.add(enumToValueMap[enumKey]);
734
+ }
735
+ }
736
+
737
+ return commentedSkills;
738
+ }
739
+
740
+ function toEnumKey(skillValue: string): string {
741
+ return skillValue
742
+ .split("-")
743
+ .map((word) => word.toUpperCase())
744
+ .join("_");
745
+ }
746
+
747
+ function skillValueToEnumKey(skillValue: string): string {
748
+ if (skillValue === "animal-salvagery") {
749
+ return "SKILL_NAMES.ANIMAL_SAVAGERY";
750
+ }
751
+ if (skillValue === "no-hand") {
752
+ return "SKILL_NAMES.NO_HANDS";
753
+ }
754
+ if (
755
+ skillValue === "disturbing-presence" ||
756
+ skillValue === "distrubing-presence"
757
+ ) {
758
+ return "SKILL_NAMES.DISTURBING_PRESENCE";
759
+ }
760
+ if (skillValue === "running-pass") {
761
+ return "SKILL_NAMES.GIVE_AND_GO";
762
+ }
763
+ const enumKey = toEnumKey(skillValue);
764
+ const existingSkills = getExistingSkills();
765
+ if (existingSkills.has(skillValue)) {
766
+ return `SKILL_NAMES.${enumKey}`;
767
+ }
768
+ return `SKILL_NAMES.${enumKey}`;
769
+ }
770
+
771
+ function checkAndReportMissingSkills(usedSkills: Set<string>): void {
772
+ const existingSkills = getExistingSkills();
773
+ const missingSkills = new Set<string>();
774
+
775
+ for (const skill of usedSkills) {
776
+ if (!existingSkills.has(skill)) {
777
+ missingSkills.add(skill);
778
+ }
779
+ }
780
+
781
+ if (missingSkills.size > 0) {
782
+ console.log("\n⚠️ Compétences manquantes dans skill.ts:");
783
+ const sortedMissing = Array.from(missingSkills).sort();
784
+ for (const skill of sortedMissing) {
785
+ const enumKey = toEnumKey(skill);
786
+ console.log(` // TODO: Ajouter ${enumKey} = "${skill}"`);
787
+ console.log(` ${enumKey} = "${skill}",`);
788
+ }
789
+ console.log("");
790
+ } else {
791
+ console.log("✅ Toutes les compétences existent déjà dans skill.ts");
792
+ }
793
+ }
794
+
795
+ function main() {
796
+ const csvContent = fs.readFileSync(CSV_PATH, "utf-8");
797
+ const rows = parse(csvContent, {
798
+ columns: true,
799
+ skip_empty_lines: true,
800
+ }) as CSVRow[];
801
+
802
+ const races = new Set<string>();
803
+ const careerIds: string[] = [];
804
+ const careers: Array<{
805
+ id: string;
806
+ team: string;
807
+ position: string;
808
+ data: any;
809
+ isBigGuy: boolean;
810
+ }> = [];
811
+ const usedSkills = new Set<string>();
812
+ const missingIcons = new Set<string>();
813
+
814
+ for (const row of rows) {
815
+ if (!row.Team || !row.Position) continue;
816
+
817
+ const careerId = generateCareerId(row.Team, row.Position);
818
+ const race =
819
+ TEAM_TO_RACE_MAP[row.Team] || toKebabCase(row.Team) + "-career";
820
+ races.add(race);
821
+
822
+ const skills = parseSkills(row.Skills || "");
823
+ const keywordsLower = (row.Keywords || "").toLowerCase();
824
+ const isBigGuy = keywordsLower.includes("big guy");
825
+ if (isBigGuy && !skills.includes("big-guy")) {
826
+ skills.push("big-guy");
827
+ }
828
+ if (!row.Quantity.includes("0-16") && !skills.includes("specialist")) {
829
+ skills.push("specialist");
830
+ }
831
+ const avatar = getAvatarFromKeywords(
832
+ row.Keywords || "",
833
+ row.Position,
834
+ row.Team
835
+ );
836
+
837
+ for (const skill of skills) {
838
+ if (typeof skill === "string") {
839
+ usedSkills.add(skill);
840
+ } else if (Array.isArray(skill) && skill.length > 0) {
841
+ usedSkills.add(skill[0]);
842
+ }
843
+ }
844
+
845
+ const rawIcon = `${toKebabCase(row.Team)}-${toKebabCase(row.Position)}1`;
846
+ const mappedIcon = ICON_ADAPTER[rawIcon] || rawIcon;
847
+ if (!KNOWN_ICONS.has(mappedIcon)) {
848
+ missingIcons.add(mappedIcon);
849
+ }
850
+
851
+ const rawAG = parseStat(row.AG);
852
+ const rawPA = parseStat(row.PA);
853
+ const rawAV = parseStat(row.AR);
854
+
855
+ const careerData = {
856
+ MA: parseStat(row.MA),
857
+ ST: parseStat(row.ST),
858
+ AG: convertAGPAFrom2025To2020(rawAG),
859
+ PA: convertAGPAFrom2025To2020(rawPA),
860
+ AV: convertAVFrom2025To2020(rawAV),
861
+ normal: row.Primary || "G",
862
+ double: row.Secondary || "",
863
+ normalCoach: "G",
864
+ icons: [mappedIcon],
865
+ skills,
866
+ avatar,
867
+ badge: race,
868
+ range: determineRange(row.Quantity),
869
+ cost: parseCost(row.Cost),
870
+ hasSprite: false,
871
+ version: "V2025" as const,
872
+ keywords: row.Keywords || "",
873
+ };
874
+
875
+ careerIds.push(careerId);
876
+ careers.push({
877
+ id: careerId,
878
+ team: row.Team,
879
+ position: row.Position,
880
+ data: careerData,
881
+ isBigGuy,
882
+ });
883
+ }
884
+
885
+ // Ajout de la compétence star pour le joueur non-Big Guy le plus cher (>100k) par équipe
886
+ const mostExpensiveByTeam = new Map<
887
+ string,
888
+ { index: number; cost: number }
889
+ >();
890
+ careers.forEach((career, index) => {
891
+ if (career.isBigGuy) return;
892
+ if (typeof career.data.cost !== "number" || career.data.cost <= 100) return;
893
+ const current = mostExpensiveByTeam.get(career.team);
894
+ if (!current || career.data.cost > current.cost) {
895
+ mostExpensiveByTeam.set(career.team, {
896
+ index,
897
+ cost: career.data.cost,
898
+ });
899
+ }
900
+ });
901
+ mostExpensiveByTeam.forEach(({ index }) => {
902
+ const target = careers[index];
903
+ if (
904
+ target &&
905
+ !target.data.skills.includes("star") &&
906
+ !target.isBigGuy &&
907
+ typeof target.data.cost === "number" &&
908
+ target.data.cost > 100
909
+ ) {
910
+ target.data.skills.push("star");
911
+ usedSkills.add("star");
912
+ }
913
+ });
914
+
915
+ // Répartition des ranges par équipe
916
+ const teams = new Map<string, number[]>();
917
+ careers.forEach((career, idx) => {
918
+ const list = teams.get(career.team) ?? [];
919
+ list.push(idx);
920
+ teams.set(career.team, list);
921
+ });
922
+
923
+ teams.forEach((indices) => {
924
+ const bigGuyIndices = indices.filter((i) => careers[i].isBigGuy);
925
+ const starIndices = indices.filter((i) =>
926
+ careers[i].data.skills.some((s: any) =>
927
+ Array.isArray(s) ? s[0] === "star" : s === "star"
928
+ )
929
+ );
930
+ const otherIndices = indices
931
+ .filter((i) => !bigGuyIndices.includes(i) && !starIndices.includes(i))
932
+ .sort((a, b) => {
933
+ const costA = careers[a].data.cost ?? 0;
934
+ const costB = careers[b].data.cost ?? 0;
935
+ return costA - costB;
936
+ });
937
+
938
+ // Assign ranges: big guy -> 6, star -> 5, autres 0..5 en boucle en évitant les slots réservés
939
+ bigGuyIndices.forEach((i) => {
940
+ careers[i].data.range = 6;
941
+ });
942
+ starIndices.forEach((i) => {
943
+ careers[i].data.range = 5;
944
+ });
945
+
946
+ const reserved = new Set<number>();
947
+ if (bigGuyIndices.length) reserved.add(6);
948
+ if (starIndices.length) reserved.add(5);
949
+ const baseRanges = [0, 1, 2, 3, 4, 5].filter((r) => !reserved.has(r));
950
+
951
+ if (baseRanges.length === 0) {
952
+ baseRanges.push(0); // fallback
953
+ }
954
+
955
+ otherIndices.forEach((i, idx) => {
956
+ const range = baseRanges[idx % baseRanges.length];
957
+ careers[i].data.range = range;
958
+ });
959
+ });
960
+
961
+ // Appliquer la limite de 2 compétences spéciales (big-guy, star, specialist) par priorité
962
+ careers.forEach((career) => {
963
+ career.data.skills = enforceSpecialSkills(career.data.skills);
964
+ });
965
+
966
+ const raceEnumEntries = Array.from(races)
967
+ .sort()
968
+ .map((race) => {
969
+ const enumName = race.toUpperCase().replace(/-/g, "_");
970
+ return ` ${enumName} = "${race}",`;
971
+ });
972
+
973
+ const careerIdEnumEntries = careerIds.sort().map((id) => {
974
+ const enumName = id.toUpperCase().replace(/-/g, "_");
975
+ return ` ${enumName} = "${id}",`;
976
+ });
977
+
978
+ const avatarValueToKey = (value: string): string => {
979
+ const mapping: Record<string, string> = {
980
+ beastman: "BEASTMAN",
981
+ "black-orc": "BLACK_ORC",
982
+ bloater: "BLOATER",
983
+ bloodspawn: "BLOODSPAWN",
984
+ centaur: "CENTAUR",
985
+ "chaos-dwarf": "CHAOS_DWARF",
986
+ "chaos-warrior": "CHAOS_WARRIOR",
987
+ dark_elf: "DARK_ELF",
988
+ dwarf: "DWARF",
989
+ elf: "ELF",
990
+ gobelin: "GOBELIN",
991
+ golem: "GOLEM",
992
+ halfing: "HALFING",
993
+ "human-woman": "HUMAN_WOMAN",
994
+ human: "HUMAN",
995
+ lezard: "LIZARDMEN",
996
+ minotaur: "MINOTAUR",
997
+ mummie: "MUMMIE",
998
+ ogre: "OGRE",
999
+ orc: "ORC",
1000
+ rat_ogre: "RAT_OGRE",
1001
+ rat: "RAT",
1002
+ rotspawn: "ROTSPAWN",
1003
+ slann: "SLANN",
1004
+ treeman: "TREEMAN",
1005
+ troll: "TROLL",
1006
+ undead: "UNDEAD",
1007
+ vampire: "VAMPIRE",
1008
+ wagon: "WAGON",
1009
+ werewolf: "WEREWOLF",
1010
+ yhetee: "YHETEE",
1011
+ };
1012
+ return mapping[value] || "HUMAN";
1013
+ };
1014
+
1015
+ const existingSkills = getExistingSkills();
1016
+ const existingAvatars = getExistingAvatars();
1017
+ const commentedSkills = getCommentedSkills();
1018
+
1019
+ const formatSkill = (
1020
+ skill: string | [string, number],
1021
+ existingSkills: Set<string>,
1022
+ commentedSkills: Set<string>
1023
+ ): string => {
1024
+ let skillValue: string;
1025
+ let skillValueWithParam: string | null = null;
1026
+
1027
+ if (typeof skill === "string") {
1028
+ skillValue = skill;
1029
+ } else if (Array.isArray(skill) && skill.length === 2) {
1030
+ skillValue = skill[0];
1031
+ skillValueWithParam = `[${skillValueToEnumKey(skill[0])}, ${skill[1]}]`;
1032
+ } else {
1033
+ return JSON.stringify(skill);
1034
+ }
1035
+
1036
+ const enumKey = skillValueToEnumKey(skillValue);
1037
+ const exists = existingSkills.has(skillValue);
1038
+ const isCommented = commentedSkills.has(skillValue);
1039
+
1040
+ if (skillValueWithParam) {
1041
+ if (exists && !isCommented) {
1042
+ return skillValueWithParam;
1043
+ } else if (isCommented) {
1044
+ return `// ${skillValueWithParam},`;
1045
+ } else {
1046
+ return `// ${skillValueWithParam}, // TODO: Ajouter ${toEnumKey(
1047
+ skillValue
1048
+ )} = "${skillValue}" dans skill.ts`;
1049
+ }
1050
+ } else {
1051
+ if (exists && !isCommented) {
1052
+ return enumKey;
1053
+ } else if (isCommented) {
1054
+ return `// ${enumKey},`;
1055
+ } else {
1056
+ return `// ${enumKey}, // TODO: Ajouter ${toEnumKey(
1057
+ skillValue
1058
+ )} = "${skillValue}" dans skill.ts`;
1059
+ }
1060
+ }
1061
+ };
1062
+
1063
+ const formatAvatar = (
1064
+ avatar: string,
1065
+ existingAvatars: Set<string>
1066
+ ): string => {
1067
+ const enumKey = avatarValueToKey(avatar);
1068
+ const exists = existingAvatars.has(avatar);
1069
+
1070
+ if (exists) {
1071
+ return `AVATAR.${enumKey}`;
1072
+ } else {
1073
+ return `// AVATAR.${enumKey}, // TODO: Ajouter ${enumKey} dans career.ts`;
1074
+ }
1075
+ };
1076
+
1077
+ const careerEntries = careers.map((career) => {
1078
+ const enumName = career.id.toUpperCase().replace(/-/g, "_");
1079
+ const skillsFormatted = career.data.skills.map(
1080
+ (skill: string | [string, number]) =>
1081
+ formatSkill(skill, existingSkills, commentedSkills)
1082
+ );
1083
+
1084
+ const skillsStr = skillsFormatted.join(",\n ");
1085
+ const avatarStr = formatAvatar(career.data.avatar, existingAvatars);
1086
+
1087
+ return ` [CAREER_ID.${enumName}]: {
1088
+ MA: ${career.data.MA},
1089
+ ST: ${career.data.ST},
1090
+ AG: ${career.data.AG},
1091
+ PA: ${career.data.PA},
1092
+ AV: ${career.data.AV},
1093
+ normal: "${career.data.normal}",
1094
+ double: "${career.data.double}",
1095
+ normalCoach: "${career.data.normalCoach}",
1096
+ icons: ${JSON.stringify(career.data.icons)},
1097
+ skills: [
1098
+ ${skillsStr}
1099
+ ],
1100
+ avatars: [${avatarStr}],
1101
+ badge: RACE.${career.data.badge.toUpperCase().replace(/-/g, "_")},
1102
+ range: ${career.data.range},
1103
+ cost: ${career.data.cost},
1104
+ hasSprite: ${career.data.hasSprite},
1105
+ version: CAREER_VERSION.V2025,
1106
+ },`;
1107
+ });
1108
+
1109
+ const output = `// MA: Movement Allowance
1110
+ // ST : Strength
1111
+ // AG: Agility
1112
+ // AV: Armour Value
1113
+
1114
+ import { SKILL_NAMES } from "./skill";
1115
+
1116
+ enum RACE {
1117
+ ${raceEnumEntries.join("\n")}
1118
+ }
1119
+
1120
+ enum AVATAR {
1121
+ BEASTMAN = "beastman",
1122
+ BLACK_ORC = "black-orc",
1123
+ BLOATER = "bloater",
1124
+ BLOODSPAWN = "bloodspawn",
1125
+ CENTAUR = "centaur",
1126
+ CHAOS_DWARF = "chaos-dwarf",
1127
+ CHAOS_WARRIOR = "chaos-warrior",
1128
+ DARK_ELF = "dark_elf",
1129
+ DWARF = "dwarf",
1130
+ ELF = "elf",
1131
+ GOBELIN = "gobelin",
1132
+ GOLEM = "golem",
1133
+ HALFING = "halfing",
1134
+ HUMAN_WOMAN = "human-woman",
1135
+ HUMAN = "human",
1136
+ LIZARDMEN = "lezard",
1137
+ MINOTAUR = "minotaur",
1138
+ MUMMIE = "mummie",
1139
+ OGRE = "ogre",
1140
+ ORC = "orc",
1141
+ RAT_OGRE = "rat_ogre",
1142
+ RAT = "rat",
1143
+ ROTSPAWN = "rotspawn",
1144
+ SLANN = "slann",
1145
+ TREEMAN = "treeman",
1146
+ TROLL = "troll",
1147
+ UNDEAD = "undead",
1148
+ VAMPIRE = "vampire",
1149
+ WAGON = "wagon",
1150
+ WEREWOLF = "werewolf",
1151
+ YHETEE = "yhetee",
1152
+ }
1153
+
1154
+ enum CAREER_ID {
1155
+ ${careerIdEnumEntries.join("\n")}
1156
+ }
1157
+
1158
+ enum CAREER_VERSION {
1159
+ V6 = "v6",
1160
+ V2020 = "v2020",
1161
+ V2025 = "v2025",
1162
+ }
1163
+
1164
+ export type Career = {
1165
+ MA: number;
1166
+ ST: number;
1167
+ AG: number;
1168
+ PA: number;
1169
+ AV: number;
1170
+ normal: string;
1171
+ double: string;
1172
+ normalCoach: string;
1173
+ icons: string[];
1174
+ skills: (SKILL_NAMES | [SKILL_NAMES, number])[];
1175
+ avatars: AVATAR[];
1176
+ badge: RACE;
1177
+ range: number;
1178
+ cost: number;
1179
+ hasSprite?: boolean;
1180
+ version: CAREER_VERSION;
1181
+ };
1182
+
1183
+ const CAREER: Record<CAREER_ID, Career> = {
1184
+ ${careerEntries.join("\n\n")}
1185
+ };
1186
+
1187
+ const getCareers = (): Record<CAREER_ID, Career> => {
1188
+ return CAREER;
1189
+ };
1190
+
1191
+ export { AVATAR, CAREER_ID, CAREER_VERSION, getCareers, RACE };
1192
+ `;
1193
+
1194
+ fs.writeFileSync(OUTPUT_PATH, output);
1195
+ console.log(`✅ Fichier généré: ${OUTPUT_PATH}`);
1196
+ console.log(`📊 ${careers.length} carrières générées`);
1197
+ console.log(`🏆 ${races.size} races trouvées`);
1198
+
1199
+ if (missingIcons.size > 0) {
1200
+ console.warn(
1201
+ "ICÔNES MANQUANTES (ajouter dans client/public/tile/player, sans .gif):",
1202
+ Array.from(missingIcons).sort()
1203
+ );
1204
+ }
1205
+
1206
+ checkAndReportMissingSkills(usedSkills);
1207
+ }
1208
+
1209
+ main();