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
package/race.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  import alea from "seedrandom";
2
2
  import { AVATAR_LIST } from "./avatars";
3
3
  import { AVATAR_DEFAULT_LIST } from "./avatarsDefault";
4
+ import { findSeasonAvatar, getSeasonAvatarPath } from "./avatarsSeason";
4
5
  import { BADGE_NAMES, hasBadge } from "./badge";
5
- import { AVATAR, CAREER_ID, getCareers, RACE } from "./career";
6
+ import {
7
+ AVATAR,
8
+ CAREER_ID_ALL,
9
+ getAvailableCareers,
10
+ getCareers,
11
+ RACE,
12
+ } from "./career";
6
13
  import { getRandomNumber } from "./random";
7
14
  import { isSpecialSkill, SKILL_NAMES } from "./skill";
8
15
  import { canBeSelectedByUser } from "./state";
@@ -91,19 +98,152 @@ const hasUnlockCareer = (user: User, careerId: string) => {
91
98
  if (!user.badges) {
92
99
  throw Error("no badges for player");
93
100
  }
94
- const race = getRaceFromCareer(careerId as CAREER_ID);
101
+ const race = getRaceFromCareer(careerId as CAREER_ID_ALL);
95
102
 
96
- return hasUnlockRace(user, race);
103
+ return hasUnlockRace(user, race as RACE);
97
104
  };
98
105
 
99
- const getCareerFromKey = (key: CAREER_ID) => {
106
+ const getCareerFromKey = (key: CAREER_ID_ALL) => {
100
107
  const careers = getCareers();
101
108
  return careers[key];
102
109
  };
103
110
 
104
111
  const getCareerKeyFromPlayer = (player: Player) => {
105
112
  // Attention la career est le race_id dans la table player
106
- return player.race_id as CAREER_ID;
113
+ return player.race_id as CAREER_ID_ALL;
114
+ };
115
+
116
+ /**
117
+ * Maps legacy (pre-2025) career IDs to their 2025 equivalents.
118
+ * Derived by inverting the ICON_ADAPTER from convert-csv-to-career.ts:
119
+ * old icon name (strip "1") → new 2025 career ID
120
+ */
121
+ const LEGACY_TO_2025_CAREER_ID: Record<string, string> = {
122
+ // Amazon
123
+ "amazon-linewomen": "amazon-linewoman-2025",
124
+ "amazon-blocker": "amazon-blocker-2025",
125
+ "amazon-blitzer": "amazon-blitzer-2025",
126
+ "amazon-catcher": "amazon-catcher-2025",
127
+ "amazon-thrower": "amazon-thrower-2025",
128
+ // Chaos Chosen
129
+ "chaos-beastman": "chaos-chosen-beastman-2025",
130
+ "chaos-warrior": "chaos-chosen-chosen-2025",
131
+ "chaos-minotaur": "chaos-chosen-minotaur-2025",
132
+ // Chaos Dwarf
133
+ "chaos-dwarf-blocker": "chaos-dwarf-blocker-2025",
134
+ "chaos-dwarf-bull-centaur": "chaos-dwarf-bull-centaur-2025",
135
+ "chaos-dwarf-hobgobelin": "chaos-dwarf-hobgoblin-2025",
136
+ // Dark Elf
137
+ "dark-elfe-blitzer": "dark-elf-blitzer-2025",
138
+ "dark-elfe-lineman": "dark-elf-lineman-2025",
139
+ "dark-elfe-runner": "dark-elf-runner-2025",
140
+ "dark-elfe-witch": "dark-elf-witch-elf-2025",
141
+ // Dwarf
142
+ "dwarf": "dwarf-lineman-2025",
143
+ "dwarf-blocker": "dwarf-lineman-2025",
144
+ "dwarf-blitzer": "dwarf-blitzer-2025",
145
+ "dwarf-deathroller": "dwarf-deathroller-2025",
146
+ "dwarf-runner": "dwarf-runner-2025",
147
+ "dwarf-troll-slayer": "dwarf-troll-slayer-2025",
148
+ // Elven Union
149
+ "elven-union-blitzer": "elven-union-blitzer-2025",
150
+ "elven-union-catcher": "elven-union-catcher-2025",
151
+ "elven-union-lineman": "elven-union-lineman-2025",
152
+ "elven-union-thrower": "elven-union-thrower-2025",
153
+ // Goblin
154
+ "gobelin-lineman": "goblin-lineman-2025",
155
+ "gobelin-looney": "goblin-loony-2025",
156
+ "gobelin-pogoer": "goblin-pogoer-2025",
157
+ "gobelin-troll": "goblin-troll-2025",
158
+ // Halfling
159
+ "halfling-lineman": "halfling-lineman-2025",
160
+ "halfling-hefty": "halfling-hefty-2025",
161
+ "halfling-catcher": "halfling-catcher-2025",
162
+ "halfling-treeman": "halfling-treeman-2025",
163
+ // Human
164
+ "human": "human-lineman-2025",
165
+ "human-lineman": "human-lineman-2025",
166
+ "human-blitzer": "human-blitzer-2025",
167
+ "human-catcher": "human-catcher-2025",
168
+ "human-ogre": "human-ogre-2025",
169
+ "human-thrower": "human-thrower-2025",
170
+ // Imperial Nobility
171
+ "imperial-lineman": "imperial-nobility-retainer-2025",
172
+ "imperial-blitzer": "imperial-nobility-blitzer-2025",
173
+ "imperial-bodyguard": "imperial-nobility-bodyguard-2025",
174
+ "imperial-ogre": "imperial-nobility-ogre-2025",
175
+ "imperial-thrower": "imperial-nobility-thrower-2025",
176
+ // Khorne
177
+ "khorne-lineman": "khorne-marauder-2025",
178
+ "khorne-khorngor": "khorne-khorngor-2025",
179
+ "khorne-bloodseeker": "khorne-bloodseeker-2025",
180
+ "khorne-bloodspawn": "khorne-bloodspawn-2025",
181
+ // Lizardmen
182
+ "lizardmen-chameleon": "lizardmen-chameleon-2025",
183
+ "lizardmen-kroxigor": "lizardmen-kroxigor-2025",
184
+ "lizardmen-saurus": "lizardmen-saurus-2025",
185
+ "lizardmen-skink": "lizardmen-skink-2025",
186
+ // Necromantic
187
+ "necromantic-zombie": "necromantic-horror-zombie-2025",
188
+ "necromantic-ghoul": "necromantic-horror-ghoul-2025",
189
+ "necromantic-wraith": "necromantic-horror-wraith-2025",
190
+ "necromantic-werewolf": "necromantic-horror-werewolf-2025",
191
+ "necromantic-golem": "necromantic-horror-flesh-golem-2025",
192
+ // Norse
193
+ "norse-lineman": "norse-raider-2025",
194
+ "norse-berserker": "norse-berserker-2025",
195
+ "norse-catcher": "norse-catcher-2025",
196
+ "norse-thrower": "norse-thrower-2025",
197
+ "norse-ulfwereners": "norse-ulfwerner-2025",
198
+ "norse-yhetee": "norse-yhetee-2025",
199
+ // Nurgle
200
+ "nurgle-rotter": "nurgle-rotter-2025",
201
+ "nurgle-pestigor": "nurgle-pestigor-2025",
202
+ "nurgle-bloater": "nurgle-bloater-2025",
203
+ "nurgle-rotspawn": "nurgle-rotspawn-2025",
204
+ // Orc
205
+ "orc": "orc-lineman-2025",
206
+ "orc-lineman": "orc-lineman-2025",
207
+ "orc-black-orc": "orc-big-un-2025",
208
+ "orc-blitzer": "orc-blitzer-2025",
209
+ "orc-thrower": "orc-thrower-2025",
210
+ "orc-troll": "orc-troll-2025",
211
+ // Skaven (anciens "rat")
212
+ "rat": "skaven-clanrat-2025",
213
+ "rat-lineman": "skaven-clanrat-2025",
214
+ "rat-runner": "skaven-gutter-2025",
215
+ "rat-blitzer": "skaven-blitzer-2025",
216
+ "rat-thrower": "skaven-thrower-2025",
217
+ "rat-ogre": "skaven-rat-ogre-2025",
218
+ // Shambling Undead (anciens "undead")
219
+ "undead-skeleton": "shambling-undead-skeleton-2025",
220
+ "undead-zombie": "shambling-undead-zombie-2025",
221
+ "undead-ghoul": "shambling-undead-ghoul-2025",
222
+ "undead-wight": "shambling-undead-wight-2025",
223
+ "undead-mummies": "shambling-undead-mummy-2025",
224
+ // Snotling
225
+ "snotling-lineman": "snotling-lineman-2025",
226
+ "snotling-runner": "snotling-runna-2025",
227
+ "snotling-wagon": "snotling-pump-wagon-2025",
228
+ "snotling-troll": "snotling-troll-2025",
229
+ // Vampire
230
+ "vampire-thrall": "vampire-thrall-2025",
231
+ "vampire-vampire": "vampire-blitzer-2025",
232
+ // Wood Elf (anciens "silvan")
233
+ "silvan": "wood-elf-lineman-2025",
234
+ "silvan-lineman": "wood-elf-lineman-2025",
235
+ "silvan-thrower": "wood-elf-thrower-2025",
236
+ "silvan-catcher": "wood-elf-catcher-2025",
237
+ "silvan-wardancer": "wood-elf-wardancer-2025",
238
+ };
239
+
240
+ /**
241
+ * Returns the 2025 career ID for a given career ID (legacy or already 2025).
242
+ * Returns undefined for careers without a 2025 equivalent (e.g. Slann).
243
+ */
244
+ const getCareerId2025 = (careerId: string): string | undefined => {
245
+ if (careerId.endsWith("-2025")) return careerId;
246
+ return LEGACY_TO_2025_CAREER_ID[careerId];
107
247
  };
108
248
 
109
249
  const getCareerFromPlayer = (player: Player) => {
@@ -114,7 +254,7 @@ const getCareerFromPlayer = (player: Player) => {
114
254
  const getCareerFromIcon = (nameIcon: string) => {
115
255
  for (const [key, values] of Object.entries(getCareers())) {
116
256
  if (values.icons.includes(nameIcon)) {
117
- return getCareerFromKey(key as CAREER_ID);
257
+ return getCareerFromKey(key as CAREER_ID_ALL);
118
258
  }
119
259
  }
120
260
  return null;
@@ -155,11 +295,26 @@ const getPlayerAvatar = (player: Player) => {
155
295
  if (!av) {
156
296
  return null;
157
297
  }
298
+ // Les avatars saisonniers (format "s17/1.png" ou trouvés dans la liste) gardent leur extension
299
+ // Normalise .jpeg → .png car les fichiers sont PNG (anciens enregistrements DB peuvent avoir .jpeg)
300
+ if (av.includes("/") || findSeasonAvatar(av)) {
301
+ return av.replace(".jpeg", ".png");
302
+ }
158
303
  return av.replace(".png", ".jpeg");
159
304
  };
160
305
 
161
306
  const getPlayerAvatarPath = (player: Player) => {
162
307
  if (player.avatar) {
308
+ // Nouveau format "s17/1.png"
309
+ if (player.avatar.includes("/")) {
310
+ const slashIdx = player.avatar.indexOf("/");
311
+ const season = parseInt(player.avatar.slice(1, slashIdx), 10);
312
+ return getSeasonAvatarPath(season);
313
+ }
314
+ const seasonAvatar = findSeasonAvatar(player.avatar);
315
+ if (seasonAvatar) {
316
+ return getSeasonAvatarPath(seasonAvatar.season);
317
+ }
163
318
  return "tile/shop/avatar";
164
319
  } else if (player.avatarDefault) {
165
320
  return "tile/shop/avatar-default";
@@ -167,9 +322,9 @@ const getPlayerAvatarPath = (player: Player) => {
167
322
  return null;
168
323
  };
169
324
 
170
- const getRaceFromCareer = (careerId: CAREER_ID) => {
325
+ const getRaceFromCareer = (careerId: CAREER_ID_ALL): RACE | undefined => {
171
326
  const career = getCareerFromKey(careerId);
172
- return career?.badge;
327
+ return career?.badge as RACE | undefined;
173
328
  };
174
329
 
175
330
  const getAvatarTypeFromPlayer = (player: Player, avatarFile: string) => {
@@ -199,6 +354,39 @@ const getAvatarTypeFromPlayer = (player: Player, avatarFile: string) => {
199
354
  return null;
200
355
  };
201
356
 
357
+ /** Returns the correct base path for an avatar file based on which list it belongs to,
358
+ * independent of the player's currently active avatar. */
359
+ const getAvatarBasePathFromFile = (player: Player, avatarFile: string): string | null => {
360
+ const career = getCareerFromPlayer(player);
361
+ if (!avatarFile) return null;
362
+ for (const avatarType of career.avatars) {
363
+ const avatars = AVATAR_LIST[avatarType as keyof typeof AVATAR_LIST];
364
+ if (avatars?.find((al) => al.file === avatarFile)) {
365
+ return "tile/shop/avatar";
366
+ }
367
+ const avatarsDefault = AVATAR_DEFAULT_LIST[avatarType as keyof typeof AVATAR_DEFAULT_LIST];
368
+ if (avatarsDefault?.find((al) => al.file === avatarFile)) {
369
+ return "tile/shop/avatar-default";
370
+ }
371
+ }
372
+ return null;
373
+ };
374
+
375
+ /** Returns the rarity string of a standard avatar file for the given player's career. */
376
+ const getAvatarRarityFromFile = (player: Player, avatarFile: string): string | undefined => {
377
+ const career = getCareerFromPlayer(player);
378
+ if (!avatarFile) return undefined;
379
+ for (const avatarType of career.avatars) {
380
+ const avatars = AVATAR_LIST[avatarType as keyof typeof AVATAR_LIST];
381
+ const found = avatars?.find((al) => al.file === avatarFile);
382
+ if (found) return found.rarity;
383
+ const avatarsDefault = AVATAR_DEFAULT_LIST[avatarType as keyof typeof AVATAR_DEFAULT_LIST];
384
+ const foundDefault = avatarsDefault?.find((al) => al.file === avatarFile);
385
+ if (foundDefault) return foundDefault.rarity;
386
+ }
387
+ return undefined;
388
+ };
389
+
202
390
  const getRaceFromPlayer = (player: Player) => {
203
391
  return getRaceFromCareer(getCareerKeyFromPlayer(player));
204
392
  };
@@ -218,6 +406,21 @@ const getCareersByRace = () => {
218
406
  return carrers;
219
407
  };
220
408
 
409
+ const getAvailableCareersByRace = () => {
410
+ const carrers: any = {};
411
+ for (const [name, values] of Object.entries(getAvailableCareers())) {
412
+ const badge = values.badge;
413
+ if (!carrers[badge]) {
414
+ carrers[badge] = [];
415
+ }
416
+ if (!carrers[badge][values.range]) {
417
+ carrers[badge][values.range] = {};
418
+ }
419
+ carrers[badge][values.range][name] = values;
420
+ }
421
+ return carrers;
422
+ };
423
+
221
424
  const GAIN_CAREER_POINT = 2;
222
425
  const GAIN_CAREER_POINT_FRIENDLY = 2;
223
426
 
@@ -255,7 +458,11 @@ const COMPATIBILITY: CompatibilityType = {
255
458
  } as const;
256
459
 
257
460
  const getPlayerGroup = (player: Player) => {
258
- return getRaceGroup(getRaceFromPlayer(player));
461
+ const race = getRaceFromPlayer(player);
462
+ if (!race) {
463
+ return undefined;
464
+ }
465
+ return getRaceGroup(race);
259
466
  };
260
467
 
261
468
  const getRaceGroup = (race: RACE): GROUP_NAME | undefined => {
@@ -283,6 +490,9 @@ const getRaceCompatibility = (race1: RACE, race2: RACE) => {
283
490
  const getPlayerCompatibility = (player1: Player, player2: Player) => {
284
491
  const race1 = getRaceFromPlayer(player1);
285
492
  const race2 = getRaceFromPlayer(player2);
493
+ if (!race1 || !race2) {
494
+ return;
495
+ }
286
496
  getRaceCompatibility(race1, race2);
287
497
  };
288
498
 
@@ -303,7 +513,7 @@ function getPlayersAvailableForPromo(players = []) {
303
513
  );
304
514
  }
305
515
 
306
- function getPromoCareer(careerId: CAREER_ID, userPlayers: Player[]) {
516
+ function getPromoCareer(careerId: CAREER_ID_ALL, userPlayers: Player[]) {
307
517
  const race = getRaceFromCareer(careerId);
308
518
  let promo = userPlayers
309
519
  .filter((p, i) => p && i < NB_MAX_PLAYERS && getRaceFromPlayer(p) === race)
@@ -315,7 +525,7 @@ function getPromoCareer(careerId: CAREER_ID, userPlayers: Player[]) {
315
525
  return promo;
316
526
  }
317
527
 
318
- function getPriceCareer(careerId: CAREER_ID) {
528
+ function getPriceCareer(careerId: CAREER_ID_ALL) {
319
529
  return getCareerRanges()[getCareerFromKey(careerId).range];
320
530
  }
321
531
 
@@ -377,7 +587,7 @@ const getPlayersByState = (players: Player[]) => {
377
587
  const getRaceSpecialSkills = (race: RACE) => {
378
588
  const careers = getCareers();
379
589
  const raceCareerIds = Object.keys(careers).filter(
380
- (careerId) => careers[careerId as CAREER_ID].badge === race
590
+ (careerId) => careers[careerId as CAREER_ID_ALL].badge === race
381
591
  );
382
592
 
383
593
  if (raceCareerIds.length === 0) {
@@ -388,7 +598,7 @@ const getRaceSpecialSkills = (race: RACE) => {
388
598
  const skillCount: Record<string, number> = {};
389
599
 
390
600
  raceCareerIds.forEach((careerId) => {
391
- const careerSkills = careers[careerId as CAREER_ID].skills;
601
+ const careerSkills = careers[careerId as CAREER_ID_ALL].skills;
392
602
  careerSkills.forEach((skill) => {
393
603
  const skillName = Array.isArray(skill) ? skill[0] : skill;
394
604
  // Ne compter que les compétences spéciales
@@ -418,7 +628,11 @@ export {
418
628
  COMPATIBILITY,
419
629
  GAIN_CAREER_POINT,
420
630
  GAIN_CAREER_POINT_FRIENDLY,
631
+ getAvailableCareersByRace,
632
+ getAvatarBasePathFromFile,
633
+ getAvatarRarityFromFile,
421
634
  getAvatarTypeFromPlayer,
635
+ getCareerId2025,
422
636
  getCareerFromIcon,
423
637
  getCareerFromKey,
424
638
  getCareerFromPlayer,