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
@@ -77,10 +77,12 @@ export interface Player extends Point {
77
77
  MA: number;
78
78
  ST: number;
79
79
  AG: number;
80
+ PA?: number | null;
80
81
  AV: number;
81
82
  current_MA: number;
82
83
  current_ST: number;
83
84
  current_AG: number;
85
+ current_PA?: number | null;
84
86
  current_AV: number;
85
87
  current_gfi: number;
86
88
  date_next_action: Date | null;
@@ -99,6 +101,7 @@ export interface Player extends Point {
99
101
  can_reroll: boolean;
100
102
  can_reroll_rebound: boolean;
101
103
  can_reroll_no_to: boolean;
104
+ secure_ball: boolean;
102
105
  has_action: boolean;
103
106
  pc: number;
104
107
  aging: number;
@@ -106,6 +109,7 @@ export interface Player extends Point {
106
109
  tempCarac: string;
107
110
  userCreatedAt: Date;
108
111
  avatar: string;
112
+ dice_id: string | null;
109
113
  quote: string;
110
114
  speak: string;
111
115
  raz: boolean;
package/index.ts CHANGED
@@ -8,16 +8,18 @@ import {
8
8
  meteoMaxPassDistance,
9
9
  } from "./meteo";
10
10
  import { getMaxCarac, hasMove, PLAYER_CARAC } from "./player";
11
- import { getEnemyTackleZones, isAdjacent } from "./position";
11
+ import { getAllAdjacentSquares, getEnemyTackleZones, isAdjacent } from "./position";
12
12
  import { getCareerFromPlayer } from "./race";
13
13
  import { getDisturbingPresenceMalus, hasSkill, SKILL_NAMES } from "./skill";
14
14
  import {
15
+ hasStatusCanAssist,
15
16
  hasStatusCanFaceup,
16
17
  hasStatusCanStandup,
17
18
  hasStatusFall,
18
19
  hasStatusOnField,
19
20
  hasStatusStanding,
20
21
  hasStatusTacleZone,
22
+ PLAYER_STATUS,
21
23
  } from "./status";
22
24
  import version from "./version";
23
25
 
@@ -142,6 +144,7 @@ const GAME_HISTORY_INFO = {
142
144
  ARMOR: "ar",
143
145
  BALL: "ball",
144
146
  BLOCK: "b",
147
+ BREATHE_FIRE: "brf",
145
148
  BRIBE: "br",
146
149
  CARAC_CHANGE: "cc",
147
150
  CASUALTY: "ca",
@@ -166,17 +169,25 @@ const GAME_HISTORY_INFO = {
166
169
  METEO: "met",
167
170
  MOVE: "m",
168
171
  PASS: "p",
172
+ PUNT_DIRECTION: "puntd",
173
+ PUNT_DISTANCE: "puntdist",
174
+ PUNT_TO_CROWD: "puntc",
175
+ PUNT_ON_PLAYER: "puntp",
176
+ PUNT_SCATTER: "punts",
177
+ PRO: "pro",
169
178
  REJECTION: "r",
170
179
  REROLL_COACH: "rc",
171
180
  REROLL_INITIAL: "rei",
172
181
  REROLL_METEO: "rm",
173
182
  REROLL_RACE_MALUS: "rrm",
174
183
  REROLL: "re",
184
+ SECURE_BALL: "sb",
175
185
  SCATTER: "sc",
176
186
  SKILL_GAIN: "skg",
177
187
  SKILL_LOSS: "skl",
178
188
  SKILL: "sk",
179
189
  SPY: "spy",
190
+ STAB: "stab",
180
191
  STANDUP: "st",
181
192
  START_FAN: "fan",
182
193
  STATUS: "s",
@@ -184,6 +195,8 @@ const GAME_HISTORY_INFO = {
184
195
  TD: "td",
185
196
  THREATENING: "thr",
186
197
  THROW_TEAM_MATE: "ttm",
198
+ BLOODLUST_BITE: "blb",
199
+ BLOODLUST_NO_TARGET: "blnt",
187
200
  TURNOVER: "to",
188
201
  };
189
202
 
@@ -330,7 +343,8 @@ const getBlockAssistance = (
330
343
  attackerPlayer: PlayerWithSkill,
331
344
  defenderPlayer: PlayerWithSkill,
332
345
  players: PlayerWithSkill[],
333
- isAttacker = false
346
+ isAttacker = false,
347
+ isFoul = false
334
348
  ) => {
335
349
  const OtherPlayersThanInvolved = players.filter((player) => {
336
350
  return player.id !== attackerPlayer.id && player.id !== defenderPlayer.id;
@@ -346,15 +360,16 @@ const getBlockAssistance = (
346
360
 
347
361
  // 1. Must be adjacent to the enemy player involved in the block,
348
362
  // 3. Must be standing, and …
363
+ // 5. Must not be Eye Gouged (cannot provide assists)
349
364
  const adjacentEnemyPlayers = OtherPlayersThanInvolved.filter((player) => {
350
- return isValidAdjacentEnemy(player, defenderPlayer);
365
+ return isValidAdjacentEnemy(player, defenderPlayer) && hasStatusCanAssist(player);
351
366
  });
352
367
  // 2. Must not be in the tackle zone of any other player from the
353
368
  // opposing team, and ...
354
369
  const assistantPlayers = adjacentEnemyPlayers.filter(
355
370
  (adjacentEnemyPlayer) => {
356
371
  let hasGard = false;
357
- if (hasSkill(adjacentEnemyPlayer, SKILL_NAMES.GUARD)) {
372
+ if (!isFoul && hasSkill(adjacentEnemyPlayer, SKILL_NAMES.GUARD)) {
358
373
  hasGard = true;
359
374
  if (
360
375
  isAttacker && // player is the attacker player.
@@ -368,8 +383,15 @@ const getBlockAssistance = (
368
383
  hasGard = false;
369
384
  }
370
385
  }
386
+ // Put the Boot In: can provide offensive assist on fouls even when marked
387
+ const hasPutTheBootIn =
388
+ isFoul &&
389
+ isAttacker &&
390
+ hasSkill(adjacentEnemyPlayer, SKILL_NAMES.PUT_THE_BOOT_IN);
391
+
371
392
  return (
372
393
  hasGard ||
394
+ hasPutTheBootIn ||
373
395
  !OtherPlayersThanInvolved.some((player) => {
374
396
  return (
375
397
  // 4. Must have his tackle zones.
@@ -811,6 +833,7 @@ function canPass(
811
833
  playerIdWithBall: string
812
834
  ) {
813
835
  return (
836
+ player.status !== PLAYER_STATUS.BLOODLUST &&
814
837
  hasStatusTacleZone(player) &&
815
838
  player.has_action === true &&
816
839
  playerTeam.date_next_pass === null &&
@@ -862,9 +885,20 @@ function canTransmit(
862
885
  if (!player || !target || isSamePlayer(player, target)) {
863
886
  return false;
864
887
  }
888
+ // Cannot transmit from or to a player in Bloodlust state
889
+ if (player.status === PLAYER_STATUS.BLOODLUST) {
890
+ return false;
891
+ }
892
+ if (target.status === PLAYER_STATUS.BLOODLUST) {
893
+ return false;
894
+ }
865
895
  if (player.id !== playerIdWithBall && target.id !== playerIdWithBall) {
866
896
  return false;
867
897
  }
898
+ // Cannot transmit if player has already ended turn (e.g., after Bloodlust bite)
899
+ if (player.hasEndTurn === true) {
900
+ return false;
901
+ }
868
902
 
869
903
  return (
870
904
  player.teamId === target.teamId &&
@@ -1016,6 +1050,7 @@ export {
1016
1050
  FORMATION_DEFAULT_NAME,
1017
1051
  GAME_HISTORY_INFO,
1018
1052
  GAME_HISTORY_TYPE,
1053
+ getAllAdjacentSquares,
1019
1054
  getAttackerStrength,
1020
1055
  getBestInterceptor,
1021
1056
  getBlockAssistance,
@@ -1052,3 +1087,21 @@ export {
1052
1087
  TAKE_BALL_TYPE,
1053
1088
  version,
1054
1089
  };
1090
+
1091
+ export {
1092
+ SidestepMode,
1093
+ SidestepStrategy,
1094
+ SidestepPreference,
1095
+ } from "./skills/sidestep";
1096
+
1097
+ export {
1098
+ HitAndRunMode,
1099
+ HitAndRunStrategy,
1100
+ HitAndRunPreference,
1101
+ } from "./skills/hitAndRun";
1102
+
1103
+ export {
1104
+ SafePairOfHandsMode,
1105
+ SafePairOfHandsStrategy,
1106
+ SafePairOfHandsPreference,
1107
+ } from "./skills/safePairOfHands";
package/jest.config.js CHANGED
@@ -14,3 +14,6 @@ module.exports = {
14
14
 
15
15
 
16
16
 
17
+
18
+
19
+
package/move.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getEnemyTackleZones } from "./position";
2
2
  import { hasSkill, SKILL_NAMES } from "./skill";
3
- import { PlayerWithSkill, Point } from "./types/models";
3
+ import { hasStatusStanding } from "./status";
4
+ import { Game, PlayerWithSkill, Point, Team } from "./types/models";
4
5
 
5
6
  function bonusDodge(
6
7
  player: PlayerWithSkill,
@@ -94,4 +95,51 @@ function modifJumping(
94
95
  return { modif, enemies };
95
96
  }
96
97
 
97
- export { bonusDodge, modifJumping };
98
+ function hasOpponentStandingAroundBall(
99
+ players: Record<string, PlayerWithSkill> | PlayerWithSkill[],
100
+ ball: Point,
101
+ teamId: string,
102
+ distance = 2
103
+ ) {
104
+ const list = Array.isArray(players) ? players : Object.values(players);
105
+ return list.some(
106
+ (p) =>
107
+ p.teamId !== teamId &&
108
+ hasStatusStanding(p) &&
109
+ p.x !== null &&
110
+ p.y !== null &&
111
+ ball.x !== null &&
112
+ ball.y !== null &&
113
+ Math.max(Math.abs(p.x - ball.x), Math.abs(p.y - ball.y)) <= distance
114
+ );
115
+ }
116
+
117
+ function getGameBall(game: Game | null | undefined): Point | null {
118
+ if (!game) return null;
119
+ if (game.ball_player_id !== null) return null;
120
+ if (game.ball_x == null || game.ball_y == null) return null;
121
+ return { x: game.ball_x, y: game.ball_y };
122
+ }
123
+
124
+ function listGamePlayers(game: Game | null | undefined): PlayerWithSkill[] {
125
+ if (!game) return [];
126
+ const team1Players = (game.team1 as Team | undefined)?.players || [];
127
+ const team2Players = (game.team2 as Team | undefined)?.players || [];
128
+ return [...team1Players, ...team2Players];
129
+ }
130
+
131
+ function canUseSecureBall(player: PlayerWithSkill): boolean {
132
+ return (
133
+ !hasSkill(player, SKILL_NAMES.BIG_GUY) &&
134
+ !hasSkill(player, SKILL_NAMES.UNSTEADY)
135
+ );
136
+ }
137
+
138
+ export {
139
+ bonusDodge,
140
+ canUseSecureBall,
141
+ getGameBall,
142
+ hasOpponentStandingAroundBall,
143
+ listGamePlayers,
144
+ modifJumping,
145
+ };
package/npm-login.sh CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enefel",
3
- "version": "2.9.1",
3
+ "version": "2.11.0",
4
4
  "description": "Blood Bowl 3 game engine",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,12 +20,16 @@
20
20
  "watch": "tsc -w",
21
21
  "prepare": "yarn build",
22
22
  "test": "jest",
23
- "test:watch": "jest --watch"
23
+ "test:watch": "jest --watch",
24
+ "convert:career": "npx ts-node teams/convert-csv-to-career.ts"
24
25
  },
25
26
  "keywords": [],
26
27
  "author": "Manest",
27
28
  "license": "MIT",
28
29
  "dependencies": {
30
+ "csv-parse": "^6.1.0",
31
+ "csv-stringify": "^6.6.0",
32
+ "jsdom": "^27.2.0",
29
33
  "seedrandom": "3.0.5",
30
34
  "yup": "^0.29.3"
31
35
  },
package/pitch.ts CHANGED
@@ -115,6 +115,27 @@ function getNumberPlayerZone(
115
115
  return { playerScrimmage, playerLateralA, playerLateralB, playerCenter };
116
116
  }
117
117
 
118
+ /**
119
+ * Checks if a tile at position (x, y) is the enemy team's touchdown zone
120
+ * @param x - X coordinate
121
+ * @param y - Y coordinate
122
+ * @param teamId - The player's team ID
123
+ * @param game - Game object containing stadium_zone, stadium_width, team1, team2
124
+ * @returns true if the tile is the enemy TD zone
125
+ */
126
+ function isEnemyTDZone(
127
+ x: number,
128
+ y: number,
129
+ teamId: string,
130
+ game: Game
131
+ ): boolean {
132
+ const tile = getTile(game, x, y);
133
+ // Determine if player is team1 or team2, then get enemy TD zone
134
+ const enemyTDTeamNumber = teamId === game.team1?.id ? 2 : 1;
135
+ const enemyTD = getTdTile(enemyTDTeamNumber);
136
+ return tile === enemyTD;
137
+ }
138
+
118
139
  export {
119
140
  coordinatesConvertorX,
120
141
  coordinatesConvertorY,
@@ -127,5 +148,6 @@ export {
127
148
  getScrimmageTile,
128
149
  getTdTile,
129
150
  getTile,
151
+ isEnemyTDZone,
130
152
  TILES,
131
153
  };
package/player.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CAREER_VERSION } from "./career";
1
2
  import { getAgingMinExperience } from "./improvement";
2
3
  import { meteoMaxMA } from "./meteo";
3
4
  import { getCareerFromPlayer } from "./race";
@@ -20,6 +21,24 @@ enum PLAYER_CARAC {
20
21
  PA = "PA",
21
22
  }
22
23
 
24
+ // Valeurs maximales absolues des caractéristiques (Blood Bowl 2020 rulebook)
25
+ const CARAC_MAX: Record<PLAYER_CARAC, number> = {
26
+ [PLAYER_CARAC.MA]: 9,
27
+ [PLAYER_CARAC.ST]: 8,
28
+ [PLAYER_CARAC.AG]: 5, // interne 5 = affichage 1+
29
+ [PLAYER_CARAC.PA]: 5, // interne 5 = affichage 1+
30
+ [PLAYER_CARAC.AV]: 10, // interne 10 = affichage 11+
31
+ };
32
+
33
+ // Valeurs minimales absolues des caractéristiques
34
+ const CARAC_MIN: Record<PLAYER_CARAC, number> = {
35
+ [PLAYER_CARAC.MA]: 1,
36
+ [PLAYER_CARAC.ST]: 1,
37
+ [PLAYER_CARAC.AG]: 1, // interne 1 = affichage 6+
38
+ [PLAYER_CARAC.PA]: 0, // 0 = pas de PA
39
+ [PLAYER_CARAC.AV]: 2, // interne 2 = affichage 3+
40
+ };
41
+
23
42
  function hasMove(player: PlayerWithSkill, game: Game) {
24
43
  if (
25
44
  player.current_MA >= getMaxCarac(player, PLAYER_CARAC.MA, game) &&
@@ -95,24 +114,29 @@ function getCareerBaseCaracLimit(
95
114
  return value;
96
115
  }
97
116
 
98
- function getMaxCarac(player: Player, carac: PLAYER_CARAC, game: Game) {
117
+ function getMaxCarac(player: Player, carac: PLAYER_CARAC, game?: Game) {
99
118
  if (!player) {
100
119
  return -1;
101
120
  }
102
121
  if (carac === PLAYER_CARAC.AG) {
103
- return getMaxAG(player, game);
122
+ return getMaxAG(player, game || ({} as Game));
104
123
  } else if (carac === PLAYER_CARAC.MA) {
105
- return getMaxMA(player, game);
124
+ return getMaxMA(player, game || ({} as Game));
106
125
  } else if (carac === PLAYER_CARAC.AV) {
107
- return getMaxAV(player, game);
126
+ return getMaxAV(player, game || ({} as Game));
108
127
  } else if (carac === PLAYER_CARAC.ST) {
109
- return getMaxST(player, game);
128
+ return getMaxST(player, game || ({} as Game));
129
+ } else if (carac === PLAYER_CARAC.PA) {
130
+ return getMaxPA(player, game);
110
131
  }
111
132
  return -1;
112
133
  }
113
134
 
114
135
  function getMaxCommun(player: Player, carac: PLAYER_CARAC, _game: Game) {
115
- let c = player[carac];
136
+ let c: number | null | undefined = player[carac];
137
+ if (c == null) {
138
+ return 0;
139
+ }
116
140
  if (!player.tempCarac) {
117
141
  return c;
118
142
  }
@@ -137,29 +161,25 @@ function getMaxCommun(player: Player, carac: PLAYER_CARAC, _game: Game) {
137
161
  return c;
138
162
  }
139
163
 
140
- function getLimitedValue(c: number) {
141
- if (c > 10) {
142
- c = 10;
143
- }
144
- if (c < 0) {
145
- c = 0;
146
- }
147
- return c;
148
- }
149
-
150
164
  function getMaxAV(player: Player, game: Game) {
151
165
  let c = getMaxCommun(player, PLAYER_CARAC.AV, game);
152
- return getLimitedValue(c);
166
+ if (c > CARAC_MAX[PLAYER_CARAC.AV]) c = CARAC_MAX[PLAYER_CARAC.AV];
167
+ if (c < CARAC_MIN[PLAYER_CARAC.AV]) c = CARAC_MIN[PLAYER_CARAC.AV];
168
+ return c;
153
169
  }
154
170
 
155
171
  function getMaxST(player: Player, game: Game) {
156
172
  let c = getMaxCommun(player, PLAYER_CARAC.ST, game);
157
- return getLimitedValue(c);
173
+ if (c > CARAC_MAX[PLAYER_CARAC.ST]) c = CARAC_MAX[PLAYER_CARAC.ST];
174
+ if (c < CARAC_MIN[PLAYER_CARAC.ST]) c = CARAC_MIN[PLAYER_CARAC.ST];
175
+ return c;
158
176
  }
159
177
 
160
178
  function getMaxAG(player: Player, game: Game) {
161
179
  let c = getMaxCommun(player, PLAYER_CARAC.AG, game);
162
- return getLimitedValue(c);
180
+ if (c > CARAC_MAX[PLAYER_CARAC.AG]) c = CARAC_MAX[PLAYER_CARAC.AG];
181
+ if (c < CARAC_MIN[PLAYER_CARAC.AG]) c = CARAC_MIN[PLAYER_CARAC.AG];
182
+ return c;
163
183
  }
164
184
 
165
185
  function getMaxMA(player: Player, game: Game) {
@@ -170,8 +190,57 @@ function getMaxMA(player: Player, game: Game) {
170
190
  c = maxMA;
171
191
  }
172
192
  }
193
+ if (c > CARAC_MAX[PLAYER_CARAC.MA]) c = CARAC_MAX[PLAYER_CARAC.MA];
194
+ if (c < CARAC_MIN[PLAYER_CARAC.MA]) c = CARAC_MIN[PLAYER_CARAC.MA];
195
+ return c;
196
+ }
173
197
 
174
- return getLimitedValue(c);
198
+ function clampPA(c: number): number {
199
+ if (c <= 0) return CARAC_MIN[PLAYER_CARAC.PA]; // 0 = pas de PA
200
+ if (c > CARAC_MAX[PLAYER_CARAC.PA]) return CARAC_MAX[PLAYER_CARAC.PA];
201
+ return c;
202
+ }
203
+
204
+ function getMaxPA(player: Player, game?: Game) {
205
+ // Si le joueur a déjà une valeur PA en base, l'utiliser
206
+ if (player.PA != null) {
207
+ let c = getMaxCommun(player, PLAYER_CARAC.PA, game || ({} as Game));
208
+ return clampPA(c);
209
+ }
210
+
211
+ // Sinon, déterminer la valeur selon la version de la career
212
+ const career = getCareerFromPlayer(player);
213
+ if (!career) {
214
+ // Fallback: utiliser AG si pas de career
215
+ return getMaxAG(player, game || ({} as Game));
216
+ }
217
+
218
+ // Si c'est une career 2025, utiliser career.PA
219
+ if (career.version === CAREER_VERSION.V2025 && career.PA != null) {
220
+ let c = career.PA;
221
+ // Appliquer les modificateurs tempCarac si présents
222
+ if (player.tempCarac) {
223
+ let obj;
224
+ try {
225
+ obj = JSON.parse(player.tempCarac);
226
+ } catch (e) {
227
+ return clampPA(c);
228
+ }
229
+ for (const value of Object.values(obj)) {
230
+ if ((value as any).gainCarac === PLAYER_CARAC.PA) {
231
+ c += 1;
232
+ }
233
+ if ((value as any).loseCarac === PLAYER_CARAC.PA) {
234
+ c -= 1;
235
+ }
236
+ }
237
+ c = getCareerBaseCaracLimit(player, c, PLAYER_CARAC.PA);
238
+ }
239
+ return clampPA(c);
240
+ }
241
+
242
+ // Sinon (career 2020 ou V6), utiliser AG
243
+ return getMaxAG(player, game || ({} as Game));
175
244
  }
176
245
 
177
246
  function getMaxGfi(player: PlayerWithSkill) {
@@ -219,15 +288,26 @@ function getAgingBonus(player: Player, totalGames: number) {
219
288
  return bonus;
220
289
  }
221
290
 
222
- function BB2020Carac(value: number, carac: PLAYER_CARAC) {
291
+ function BB2020Carac(
292
+ value: number,
293
+ carac: PLAYER_CARAC,
294
+ careerVersion?: CAREER_VERSION
295
+ ) {
223
296
  // CHARACTERISTIC TABLE
224
297
  // Characteristic MA ST AG PA AV
225
298
  // Maximum 9 8 1+ 1+ 11+
226
299
  //Minimum 1 1 6+ 6+ 3+
300
+ // Les valeurs sont maintenant normalisées au format 2020 pour toutes les versions
301
+ // (les valeurs 2025 sont converties lors de la génération du fichier career2025.ts)
227
302
  if (carac === PLAYER_CARAC.AV && value) {
228
303
  // TODO: min3+, max11+
229
304
  return `${+value + 1}+`;
230
- } else if (carac === PLAYER_CARAC.AG && value) {
305
+ } else if (
306
+ (carac === PLAYER_CARAC.AG || carac === PLAYER_CARAC.PA) &&
307
+ value
308
+ ) {
309
+ // Conversion pour afficher la notation "+"
310
+ // PA se comporte exactement comme AG (PA n'existait pas en 2020)
231
311
  let newValue = 6;
232
312
  if (value <= 1) newValue = 6;
233
313
  if (value === 2) newValue = 5;
@@ -250,6 +330,8 @@ export {
250
330
  areFromSameTeam,
251
331
  arePlayingSameGame,
252
332
  BB2020Carac,
333
+ CARAC_MAX,
334
+ CARAC_MIN,
253
335
  gamesforAgingRoll,
254
336
  getAgingBonus,
255
337
  getCareerBaseCarac,
@@ -260,6 +342,7 @@ export {
260
342
  getMaxCommun,
261
343
  getMaxGfi,
262
344
  getMaxMA,
345
+ getMaxPA,
263
346
  getMaxST,
264
347
  hasMove,
265
348
  hasTeam,
package/position.ts CHANGED
@@ -19,6 +19,22 @@ export const isAdjacent = (p1: Point, p2: Point): boolean => {
19
19
  );
20
20
  };
21
21
 
22
+ /**
23
+ * Returns all 8 adjacent squares around a given position
24
+ */
25
+ export const getAllAdjacentSquares = (x: number, y: number): Point[] => {
26
+ return [
27
+ { x: x - 1, y: y - 1 },
28
+ { x: x, y: y - 1 },
29
+ { x: x + 1, y: y - 1 },
30
+ { x: x - 1, y: y },
31
+ { x: x + 1, y: y },
32
+ { x: x - 1, y: y + 1 },
33
+ { x: x, y: y + 1 },
34
+ { x: x + 1, y: y + 1 },
35
+ ];
36
+ };
37
+
22
38
  export const getEnemyTackleZones = (
23
39
  location: Point,
24
40
  teamId: string,