diff --git a/src/engine/actions.py b/src/engine/actions.py index 1db7cd0..1dde9ee 100644 --- a/src/engine/actions.py +++ b/src/engine/actions.py @@ -44,6 +44,42 @@ _ROLE_SKILL_BONUS: dict[str, dict[str, int]] = { "survivant_generique":{"survie": 1}, } +# --------------------------------------------------------------------------- +# Tables de loot par action de survie +# Chance de succès = food_level ou water_level de la zone (0-100) +# divisé par un facteur de difficulté (plus la zone est riche, plus c'est facile) +# +# Format : (item_slug, item_name, item_type, weight, hp_heal, properties) +# TODO : enrichir avec animaux/cadavres/types de zone +# --------------------------------------------------------------------------- + +_FORAGE_LOOT: list[tuple] = [ + # (slug, nom, type, poids, hp_heal, props) + ("cram", "Cram", "food", 0.5, 5, {}), + ("igname_sauvage","Igname sauvage", "food", 0.3, 3, {}), + ("champignon", "Champignons des marais","food", 0.2, 2, {"cooked": False}), + ("rat_grille", "Rat grillé", "food", 0.4, 4, {}), + ("nuka_cola", "Nuka-Cola", "drink", 0.5, 2, {"thirst_gain": 1}), + ("cram", "Cram", "food", 0.5, 5, {}), # double poids pour fréquence +] + +_WATER_LOOT: list[tuple] = [ + ("eau_sale", "Eau sale", "drink", 0.5, 0, {"irradiated": True, "rad_on_drink": 1}), + ("eau_sale", "Eau sale", "drink", 0.5, 0, {"irradiated": True, "rad_on_drink": 1}), + ("eau_purifiee", "Eau purifiée", "drink", 0.5, 0, {}), + ("nuka_cola", "Nuka-Cola", "drink", 0.5, 2, {"thirst_gain": 1}), +] + +# Seuils de chance de succès selon le niveau de ressource de la zone +def _forage_chance(food_level: float) -> float: + """0-100 → probabilité de trouver quelque chose (0.05 à 0.70)""" + return max(0.05, min(0.70, food_level / 100 * 0.65 + 0.05)) + +def _water_chance(water_level: float) -> float: + """0-100 → probabilité de trouver de l'eau (0.10 à 0.80)""" + return max(0.10, min(0.80, water_level / 100 * 0.70 + 0.10)) + + # Paires de factions hostiles l'une envers l'autre _HOSTILE_PAIRS: set[frozenset] = { frozenset({"ecumeurs", "union"}), @@ -306,6 +342,68 @@ def _decide_action(char: dict, all_chars: list[dict], "description": f"{char['name']} en veille"} +def _resolve_forage(char: dict, session_id: int, day: int, tick: int, + world_map: dict, rng: random.Random | None) -> None: + """Tente de trouver de la nourriture selon le food_level de la zone.""" + import survival as sv_module + rng = rng or random.Random() + loc = char.get("location_slug", "") + ws = world_map.get(loc, {}) + food_lvl = ws.get("food_level", 20) + chance = _forage_chance(food_lvl) + + if rng.random() < chance: + item = rng.choice(_FORAGE_LOOT) + slug, name, itype, weight, hp_heal, props = item + db.add_item(char["id"], session_id, slug, name, itype, + quantity=1, weight=weight, hp_heal=hp_heal, properties=props) + # Consommer immédiatement si c'est de la nourriture + if itype == "food": + sv_module.feed_character(session_id, char["id"], day, tick, "normal") + db.remove_item(char["id"], session_id, slug, 1) + desc = f"{char['name']} trouve {name} en fouillant ({loc}, food={food_lvl:.0f})" + effect = {"found": slug, "food_level": food_lvl, "chance": round(chance, 2)} + else: + desc = f"{char['name']} ne trouve rien à manger ({loc}, food={food_lvl:.0f})" + effect = {"found": None, "food_level": food_lvl, "chance": round(chance, 2)} + + db.log_event(session_id=session_id, day=day, tick=tick, + event_type="fouille_nourriture", + description=desc, actor_id=char["id"], + location_slug=loc, mechanical_effect=effect) + + +def _resolve_find_water(char: dict, session_id: int, day: int, tick: int, + world_map: dict, rng: random.Random | None) -> None: + """Tente de trouver de l'eau selon le water_level de la zone.""" + import survival as sv_module + rng = rng or random.Random() + loc = char.get("location_slug", "") + ws = world_map.get(loc, {}) + water_lvl = ws.get("water_level", 20) + chance = _water_chance(water_lvl) + + if rng.random() < chance: + item = rng.choice(_WATER_LOOT) + slug, name, itype, weight, hp_heal, props = item + db.add_item(char["id"], session_id, slug, name, itype, + quantity=1, weight=weight, hp_heal=hp_heal, properties=props) + # Consommer immédiatement + sv_module.hydrate_character(session_id, char["id"], day, tick, + "purified" if slug == "eau_purifiee" else "normal") + db.remove_item(char["id"], session_id, slug, 1) + desc = f"{char['name']} trouve {name} ({loc}, water={water_lvl:.0f})" + effect = {"found": slug, "water_level": water_lvl, "chance": round(chance, 2)} + else: + desc = f"{char['name']} ne trouve pas d'eau ({loc}, water={water_lvl:.0f})" + effect = {"found": None, "water_level": water_lvl, "chance": round(chance, 2)} + + db.log_event(session_id=session_id, day=day, tick=tick, + event_type="fouille_eau", + description=desc, actor_id=char["id"], + location_slug=loc, mechanical_effect=effect) + + def _execute_action(char: dict, decision: dict, chars_map: dict, inventories: dict, session_id: int, day: int, tick: int) -> None: @@ -387,14 +485,12 @@ def _execute_action(char: dict, decision: dict, location_slug=char.get("location_slug"), ) - elif action in ("forage", "find_water"): - db.log_event( - session_id=session_id, day=day, tick=tick, - event_type="survie_action", - description=decision["description"], - actor_id=char_id, - location_slug=char.get("location_slug"), - ) + elif action == "forage": + _resolve_forage(char, session_id, day, tick, world_map=decision.get("_world_map", {}), rng=decision.get("_rng")) + + elif action == "find_water": + _resolve_find_water(char, session_id, day, tick, world_map=decision.get("_world_map", {}), rng=decision.get("_rng")) + # idle : pas de log @@ -443,6 +539,9 @@ def process_actions_tick(session_id: int, day: int, tick: int, char, list(chars_map.values()), states, inventories, world_map, rng, phase, mode ) + # Injecter world_map et rng pour les actions de fouille + decision["_world_map"] = world_map + decision["_rng"] = rng _execute_action(char, decision, chars_map, inventories, session_id, day, tick) # Mise à jour locale de chars_map si le PNJ est mort (évite une requête DB par PNJ)