@@ -83,13 +83,13 @@ def _to_poke_tuple(entry: list) -> tuple[str | int | None, ...]:
8383POKEMON_TYPES = ["🌿" , "🔥" , "💧" ]
8484
8585TYPE_MULTIPLIER = {
86- ("🔥" , "🌿" ): 1.4 ,
87- ("🌿" , "💧" ): 1.4 ,
88- ("💧" , "🔥" ): 1.4 ,
86+ ("🔥" , "🌿" ): 1.25 ,
87+ ("🌿" , "💧" ): 1.25 ,
88+ ("💧" , "🔥" ): 1.25 ,
8989
90- ("🌿" , "🔥" ): 0.6 ,
91- ("💧" , "🌿" ): 0.6 ,
92- ("🔥" , "💧" ): 0.6 ,
90+ ("🌿" , "🔥" ): 0.8 ,
91+ ("💧" , "🌿" ): 0.8 ,
92+ ("🔥" , "💧" ): 0.8 ,
9393}
9494
9595COLORS = {
@@ -101,20 +101,48 @@ def _to_poke_tuple(entry: list) -> tuple[str | int | None, ...]:
101101COMBAT_COOLDOWN = 60 * 5
102102XP_COOLDOWN = 60
103103
104- CRIT_CHANCE = 0.07
104+ BASE_HP = 15
105+ HP_PER_LEVEL = 1.8
106+
107+ CRIT_CHANCE = 0.06
105108CRIT_MULTIPLIER = 1.5
106109
107110XP_MIN , XP_MAX = 15 , 55
108111
109- DEFAULT = {
110- "HP" : 15 ,
112+ POKEMON_ENTRY_DEFAULTS = {
113+ "starter" : None ,
114+ "pokemon" : None ,
115+ "level" : 1 ,
116+ "xp" : 0 ,
117+ "last_time" : None ,
118+ "HP" : BASE_HP ,
119+ "type" : None ,
111120 "wins" : 0 ,
112121 "losses" : 0 ,
113- "last_combat " : 0
122+ "begin_date " : None ,
114123}
115124
116- POKEMON_DATA_FILE = "data/pokemon_starters.json"
125+ import logging
126+
127+ log = logging .getLogger (__name__ )
128+
129+
130+ def normalize_pokemon_entry (entry ):
131+ if not isinstance (entry , dict ):
132+ log .warning (
133+ "Entrée Pokémon ignorée (type invalide): %r (%s)" ,
134+ entry , type (entry ).__name__
135+ )
136+ return None
137+
138+ for key , default in POKEMON_ENTRY_DEFAULTS .items ():
139+ if key not in entry or entry [key ] is None :
140+ if key == "begin_date" :
141+ entry [key ] = datetime .datetime .now ().replace (microsecond = 0 ).isoformat ()
142+ else :
143+ entry [key ] = default
117144
145+ return entry
118146
119147def pokepedia_url (name : str ) -> str :
120148 # remplace espaces par underscore + encode safe URL
@@ -125,7 +153,23 @@ def load_pokemon_data() -> dict:
125153 os .makedirs ("data" , exist_ok = True )
126154 try :
127155 with open (POKEMON_DATA_FILE , "r" , encoding = "utf-8" ) as f :
128- return json .load (f )
156+ data = json .load (f )
157+ for guild_id , users in list (data .items ()):
158+ if not isinstance (users , dict ):
159+ log .error ("Guild %s a une structure invalide: %r" , guild_id , users )
160+ data [guild_id ] = {}
161+ continue
162+
163+ for user_id , entry in list (users .items ()):
164+ fixed = normalize_pokemon_entry (entry )
165+ if fixed is None :
166+ log .warning (
167+ "Suppression entrée Pokémon invalide | guild=%s user=%s" ,
168+ guild_id , user_id
169+ )
170+ del users [user_id ]
171+ save_pokemon_data (data )
172+ return data
129173 except (FileNotFoundError , json .JSONDecodeError ) as e :
130174 logger .error (f"Error reading file: { e } " )
131175 return {}
@@ -275,8 +319,14 @@ def power_emoji_for_level(level: int) -> str:
275319 return emoji
276320 return POWER_EMOJIS [- 1 ][1 ]
277321
322+
323+ def ensure_hp_field (entry : dict ) -> None :
324+ if "HP" not in entry or not isinstance (entry ["HP" ], int ):
325+ entry ["HP" ] = 0
326+
278327def compute_damage (attacker : dict , defender : dict ) -> tuple [int , bool ]:
279- damage_base = attacker ["level" ] * 0.55 * random .uniform (0.85 , 1.35 )
328+ damage_base = attacker ["level" ] * 0.78
329+ rng = random .uniform (0.9 , 1.3 )
280330
281331 multiplier = TYPE_MULTIPLIER .get (
282332 (attacker ["type" ], defender ["type" ]),
@@ -287,8 +337,14 @@ def compute_damage(attacker: dict, defender: dict) -> tuple[int, bool]:
287337 if crit :
288338 multiplier *= CRIT_MULTIPLIER
289339
290- dmg = round (damage_base * multiplier )
291- return max (1 , dmg ), crit
340+ damage = round (damage_base * rng * multiplier )
341+
342+ return max (1 , damage ), crit
343+
344+
345+ def compute_max_hp (entry : dict ) -> int :
346+ ensure_hp_field (entry )
347+ return BASE_HP + int (entry ["level" ] * HP_PER_LEVEL ) + entry ["HP" ]
292348
293349def health_bar (current : int , max_hp : int , size : int = 10 ) -> str :
294350 ratio = current / max_hp if max_hp > 0 else 0
@@ -406,7 +462,6 @@ async def on_message(self, message: discord.Message):
406462 if not entry :
407463 return
408464
409- entry .setdefault ("last_time" , DEFAULT .get ("last_time" ))
410465 now = time .time ()
411466 if entry .get ("last_time" , 0 ) + XP_COOLDOWN > now :
412467 return
@@ -443,8 +498,6 @@ async def level_up(self, entry: dict, user: discord.Member) -> str | None:
443498 message = None
444499 leveled_up = False
445500
446- entry .setdefault ("HP" , DEFAULT .get ("HP" ) + round (1.5 * entry ["level" ]))
447-
448501 while entry ["xp" ] >= xp_to_next_level (entry ["level" ]) and entry ["level" ] < 100 :
449502 entry ["xp" ] -= xp_to_next_level (entry ["level" ])
450503 entry ["level" ] += 1
@@ -507,8 +560,7 @@ async def evolve(self, entry: dict, user: discord.Member, channel: discord.TextC
507560 old_name = entry ["pokemon" ]
508561 next_name = chain [i + 1 ][0 ]
509562 entry ["pokemon" ] = next_name
510- entry .setdefault ("HP" , DEFAULT .get ("HP" ) + round (1.5 * entry ["level" ]))
511- entry ["HP" ] += random .randint (5 , 10 )
563+ compute_max_hp (entry )
512564
513565 old_id = chain [i ][2 ] if len (chain [i ]) > 2 else None
514566 new_id = chain [i + 1 ][2 ] if len (chain [i + 1 ]) > 2 else None
@@ -678,18 +730,7 @@ async def choose_starter(self, interaction: discord.Interaction, choix: str | No
678730 )
679731 return
680732
681- self .pokemon_data .setdefault (guild_id , {})[user_id ] = {
682- "starter" : found_chain [0 ][0 ],
683- "pokemon" : found_chain [0 ][0 ],
684- "level" : 1 ,
685- "xp" : 0 ,
686- "last_time" : DEFAULT .get ("last_time" ),
687- "HP" : DEFAULT .get ("HP" ),
688- "type" : starter_type ,
689- "wins" : DEFAULT .get ("wins" ),
690- "losses" : DEFAULT .get ("wins" ),
691- "begin_date" : datetime .datetime .now ().replace (microsecond = 0 ).isoformat ()
692- }
733+ self .pokemon_data .setdefault (guild_id , {})[user_id ] = POKEMON_ENTRY_DEFAULTS
693734
694735 save_pokemon_data (self .pokemon_data )
695736
@@ -765,7 +806,7 @@ async def starter(
765806 )
766807 return
767808
768- entry . setdefault ( "HP" , DEFAULT . get ( "HP" ) + round ( 1.5 * entry [ "level" ]) )
809+ compute_max_hp ( entry )
769810
770811 chain = STARTER_CHAINS .get (entry ["pokemon" ].lower (), [])
771812 xp_needed = xp_to_next_level (entry ["level" ])
@@ -857,9 +898,6 @@ async def combat(self, interaction: discord.Interaction, adversaire: discord.Mem
857898 await interaction .followup .send ("Combat annulé !" )
858899 return
859900
860- p1 .setdefault ("last_combat" , now )
861- p2 .setdefault ("last_combat" , now )
862-
863901 p1 ["last_combat" ] = now
864902 p2 ["last_combat" ] = now
865903
@@ -894,13 +932,10 @@ async def start_combat(
894932 p2 : dict ,
895933 thread : discord .Thread
896934 ):
935+
897936 max_hp1 = hp1 = p1 ["HP" ]
898937 max_hp2 = hp2 = p2 ["HP" ]
899938
900- for p in (p1 , p2 ):
901- p .setdefault ("wins" , DEFAULT .get ("wins" ))
902- p .setdefault ("losses" , DEFAULT .get ("losses" ))
903-
904939 # Qui commence
905940 if p1 ["level" ] > p2 ["level" ]:
906941 turn = 1
@@ -961,8 +996,8 @@ def format_pokemon_label(pokemon_name: str, trainer: discord.Member) -> str:
961996 msg += "\n # ⚡ **COUP CRITIQUE !**"
962997
963998 msg += (
964- f"\n \n ❤️ **{ def_label_1 } ** : { hp1 } /{ max_hp1 } HP HP \n { bar1 } "
965- f"\n \n ❤️ **{ def_label_2 } ** : { hp2 } /{ max_hp2 } HP HP \n { bar2 } "
999+ f"\n \n ❤️ **{ def_label_1 } ** : { hp1 } /{ max_hp1 } HP\n { bar1 } "
1000+ f"\n \n ❤️ **{ def_label_2 } ** : { hp2 } /{ max_hp2 } HP\n { bar2 } "
9661001 )
9671002
9681003 await thread .send (msg )
0 commit comments