feat: système de config externe + multi-instance + modes simulation
sim_config.py (NOUVEAU):
- Chargeur config JSON avec deep-merge des modes (sim_001/002.json)
- sim_config.get('survival'), get('encounter'), get('world') depuis n'importe quel module
- SIM_CONFIG env var ou --config CLI arg
run.py:
- --config/-c arg, SIM_CONFIG env var, SESSION_ID env var
- _init_world_state() initialise pop_count depuis config au démarrage
- Mode actif depuis sim_config (prime sur la DB)
survival.py:
- drain_speed: multiplicateur sur HUNGER/THIRST/SLEEP_TICKS (2.0=2x plus vite)
- fatigue_hp_per_2pts: dégâts fatigue depuis config
- survival.enabled: peut désactiver la survie entièrement
tick.py / encounter.py:
- world params (water/food drain, security delta, pop counts) depuis sim_config
- encounter.global_rate_multiplier appliqué sur chance_per_tick
- encounter.enabled: peut désactiver les rencontres
character_creator.py — Caps réalistes (divise par ~4-5):
pillard 0-60, garde 15-100, marchand 60-400, eclaireur 10-80
docteur 40-200, technicien 20-150, lieutenant 60-300
raider_boss 30-200, chasseur_primes 80-350, passeur 30-180
scavenger 5-70, chef_faction 300-1500, survivant_generique 5-60
sim_001.json: mode=pacifiste (rencontres x0.4, drain x0.7, sécurité stable)
sim_002.json: mode=guerre_commerciale (rencontres x1.2, drain x1.5, caps x1.5)
Modes disponibles: pacifiste, politique, guerre_commerciale, guerre, survie_extreme
init_pnj.py: SESSION_ID depuis os.getenv (était hardcodé à 1)
Multi-instance: SESSION_ID=1 sim_001.json + SESSION_ID=2 sim_002.json en parallèle
This commit is contained in:
+108
-20
@@ -1,11 +1,14 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Configuration de la simulation session 1 — Venice of Wasteland",
|
"_comment": "Config session 1 — Venice of Wasteland. Sections 'modes.<mode>' font un deep-merge sur les valeurs de base.",
|
||||||
|
|
||||||
"session": {
|
"session": {
|
||||||
"session_id": 1,
|
"session_id": 1,
|
||||||
"name": "Session 001 — Louisiane post-apo",
|
"name": "Session 001 — Louisiane post-apo",
|
||||||
"seed_global": 42,
|
"seed_global": 42
|
||||||
"mode": "sandbox"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"mode": "pacifiste",
|
||||||
|
|
||||||
"tick": {
|
"tick": {
|
||||||
"tick_sleep_sec": 60,
|
"tick_sleep_sec": 60,
|
||||||
"ticks_per_day": 24,
|
"ticks_per_day": 24,
|
||||||
@@ -13,21 +16,55 @@
|
|||||||
"night_start_tick": 20,
|
"night_start_tick": 20,
|
||||||
"llm_tick_interval": 6
|
"llm_tick_interval": 6
|
||||||
},
|
},
|
||||||
"difficulty": {
|
|
||||||
"preset": "normal",
|
"survival": {
|
||||||
"survival_drain_food": 1,
|
"_comment": "drain_speed : diviseur sur HUNGER/THIRST/SLEEP_TICKS (2.0 = 2x plus vite, 0.5 = 2x plus lent)",
|
||||||
"survival_drain_water": 1,
|
"drain_speed": 1.0,
|
||||||
"survival_drain_sleep": 1,
|
"fatigue_hp_per_2pts": 2,
|
||||||
"encounter_rate_multiplier": 1.0,
|
"enabled": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"economy": {
|
||||||
|
"_comment": "Multiplicateur global caps (appliqué à tous les caps_range des rôles)",
|
||||||
|
"caps_global_multiplier": 1.0,
|
||||||
|
"loot_caps_min": 3,
|
||||||
|
"loot_caps_max": 25,
|
||||||
|
"trade_price_variance": 0.2
|
||||||
|
},
|
||||||
|
|
||||||
|
"encounter": {
|
||||||
|
"_comment": "global_rate_multiplier : appliqué sur chance_per_tick de chaque zone",
|
||||||
|
"global_rate_multiplier": 1.0,
|
||||||
"combat_lethality": 1.0,
|
"combat_lethality": 1.0,
|
||||||
"resource_scarcity": 1.0
|
"enabled": true
|
||||||
},
|
},
|
||||||
"difficulty_presets": {
|
|
||||||
"easy": { "survival_drain_food": 0, "survival_drain_water": 0, "survival_drain_sleep": 0, "encounter_rate_multiplier": 0.5, "combat_lethality": 0.7 },
|
"world": {
|
||||||
"normal": { "survival_drain_food": 1, "survival_drain_water": 1, "survival_drain_sleep": 1, "encounter_rate_multiplier": 1.0, "combat_lethality": 1.0 },
|
"_comment": "Populations réalistes pour un wasteland post-apo. 500 habitants c'est une grande cité en zone sûre.",
|
||||||
"hard": { "survival_drain_food": 2, "survival_drain_water": 2, "survival_drain_sleep": 1, "encounter_rate_multiplier": 1.5, "combat_lethality": 1.3 },
|
"pop_count_by_zone": {
|
||||||
"nightmare": { "survival_drain_food": 3, "survival_drain_water": 3, "survival_drain_sleep": 2, "encounter_rate_multiplier": 2.0, "combat_lethality": 1.8 }
|
"independance": 120,
|
||||||
|
"nola_vieux_carre": 90,
|
||||||
|
"baton_rouge": 70,
|
||||||
|
"laplace": 60,
|
||||||
|
"nola_fleuve": 40,
|
||||||
|
"la_paroisse": 35,
|
||||||
|
"nola_cbd": 25,
|
||||||
|
"oak_plantation": 20,
|
||||||
|
"donaldsonville": 15,
|
||||||
|
"pearl_river": 10
|
||||||
},
|
},
|
||||||
|
"water_drain_per_pop": 0.004,
|
||||||
|
"food_drain_per_pop": 0.003,
|
||||||
|
"security_day_regen": 0.2,
|
||||||
|
"security_night_drain": 0.3,
|
||||||
|
"conflict_security_range": [-0.5, 0.2]
|
||||||
|
},
|
||||||
|
|
||||||
|
"faction_relations": {
|
||||||
|
"initial_volatility": 0.05,
|
||||||
|
"drift_per_llm_tick": 1
|
||||||
|
},
|
||||||
|
|
||||||
"llm": {
|
"llm": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"model_mj": "qwen2.5:14b",
|
"model_mj": "qwen2.5:14b",
|
||||||
@@ -36,10 +73,61 @@
|
|||||||
"chroma_url": "http://localhost:8000",
|
"chroma_url": "http://localhost:8000",
|
||||||
"chroma_collection": "fallout_vst"
|
"chroma_collection": "fallout_vst"
|
||||||
},
|
},
|
||||||
"multi_session": {
|
|
||||||
"allow_parallel": true,
|
"modes": {
|
||||||
"max_sessions": 4,
|
"pacifiste": {
|
||||||
"session_db_host": "localhost",
|
"_comment": "Monde presque sûr, idéal pour tester la sim sans mort en cascade",
|
||||||
"session_db_port": 15432
|
"encounter": { "global_rate_multiplier": 0.4, "combat_lethality": 0.5 },
|
||||||
|
"survival": { "drain_speed": 0.7 },
|
||||||
|
"world": { "security_day_regen": 0.4, "security_night_drain": 0.1, "conflict_security_range": [-0.1, 0.3] },
|
||||||
|
"faction_relations": { "initial_volatility": 0.02, "drift_per_llm_tick": 1 }
|
||||||
|
},
|
||||||
|
|
||||||
|
"politique": {
|
||||||
|
"_comment": "Intrigues factions, peu de violence directe, mais relations volatiles",
|
||||||
|
"encounter": { "global_rate_multiplier": 0.6, "combat_lethality": 0.7 },
|
||||||
|
"survival": { "drain_speed": 1.0 },
|
||||||
|
"world": { "security_day_regen": 0.2, "security_night_drain": 0.3, "conflict_security_range": [-0.8, 0.1] },
|
||||||
|
"faction_relations": { "initial_volatility": 0.3, "drift_per_llm_tick": 5 }
|
||||||
|
},
|
||||||
|
|
||||||
|
"guerre_commerciale": {
|
||||||
|
"_comment": "Blocus, routes coupées, prix instables. Survie difficile mais pas de front de guerre.",
|
||||||
|
"encounter": { "global_rate_multiplier": 1.2, "combat_lethality": 0.9 },
|
||||||
|
"survival": { "drain_speed": 1.5 },
|
||||||
|
"economy": { "caps_global_multiplier": 1.5, "trade_price_variance": 0.5 },
|
||||||
|
"world": {
|
||||||
|
"pop_count_by_zone": { "nola_cbd": 15, "pearl_river": 6, "donaldsonville": 8 },
|
||||||
|
"security_day_regen": 0.1, "security_night_drain": 0.5, "conflict_security_range": [-1.5, 0.0]
|
||||||
|
},
|
||||||
|
"faction_relations": { "initial_volatility": 0.4, "drift_per_llm_tick": 8 }
|
||||||
|
},
|
||||||
|
|
||||||
|
"guerre": {
|
||||||
|
"_comment": "Front de guerre actif. Rencontres fréquentes, ressources rares, morts quotidiennes.",
|
||||||
|
"encounter": { "global_rate_multiplier": 2.0, "combat_lethality": 1.5 },
|
||||||
|
"survival": { "drain_speed": 1.8, "fatigue_hp_per_2pts": 3 },
|
||||||
|
"economy": { "caps_global_multiplier": 0.5, "loot_caps_max": 40, "trade_price_variance": 0.8 },
|
||||||
|
"world": {
|
||||||
|
"pop_count_by_zone": {
|
||||||
|
"independance": 80, "nola_vieux_carre": 50, "baton_rouge": 40,
|
||||||
|
"laplace": 30, "nola_fleuve": 20, "la_paroisse": 15,
|
||||||
|
"nola_cbd": 8, "oak_plantation": 10, "donaldsonville": 6, "pearl_river": 4
|
||||||
|
},
|
||||||
|
"security_day_regen": 0.05, "security_night_drain": 0.8, "conflict_security_range": [-2.0, -0.3]
|
||||||
|
},
|
||||||
|
"faction_relations": { "initial_volatility": 0.8, "drift_per_llm_tick": 15 }
|
||||||
|
},
|
||||||
|
|
||||||
|
"survie_extreme": {
|
||||||
|
"_comment": "Pas de mode narration. Purement sim de survie hostile. Test de résistance.",
|
||||||
|
"encounter": { "global_rate_multiplier": 1.8, "combat_lethality": 2.0 },
|
||||||
|
"survival": { "drain_speed": 2.5, "fatigue_hp_per_2pts": 4 },
|
||||||
|
"economy": { "caps_global_multiplier": 0.3, "loot_caps_max": 15 },
|
||||||
|
"world": {
|
||||||
|
"security_day_regen": 0.05, "security_night_drain": 1.0, "conflict_security_range": [-3.0, -0.5]
|
||||||
|
},
|
||||||
|
"faction_relations": { "initial_volatility": 0.5, "drift_per_llm_tick": 10 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Config session 2 — Test guerre commerciale. Seed différente, mode plus hostile.",
|
||||||
|
|
||||||
|
"session": {
|
||||||
|
"session_id": 2,
|
||||||
|
"name": "Session 002 — Guerre commerciale",
|
||||||
|
"seed_global": 137
|
||||||
|
},
|
||||||
|
|
||||||
|
"mode": "guerre_commerciale",
|
||||||
|
|
||||||
|
"tick": {
|
||||||
|
"tick_sleep_sec": 60,
|
||||||
|
"ticks_per_day": 24,
|
||||||
|
"day_start_tick": 6,
|
||||||
|
"night_start_tick": 20,
|
||||||
|
"llm_tick_interval": 6
|
||||||
|
},
|
||||||
|
|
||||||
|
"survival": {
|
||||||
|
"drain_speed": 1.0,
|
||||||
|
"fatigue_hp_per_2pts": 2,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"economy": {
|
||||||
|
"caps_global_multiplier": 1.0,
|
||||||
|
"loot_caps_min": 3,
|
||||||
|
"loot_caps_max": 25,
|
||||||
|
"trade_price_variance": 0.2
|
||||||
|
},
|
||||||
|
|
||||||
|
"encounter": {
|
||||||
|
"global_rate_multiplier": 1.0,
|
||||||
|
"combat_lethality": 1.0,
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
|
||||||
|
"world": {
|
||||||
|
"pop_count_by_zone": {
|
||||||
|
"independance": 120,
|
||||||
|
"nola_vieux_carre": 90,
|
||||||
|
"baton_rouge": 70,
|
||||||
|
"laplace": 60,
|
||||||
|
"nola_fleuve": 40,
|
||||||
|
"la_paroisse": 35,
|
||||||
|
"nola_cbd": 25,
|
||||||
|
"oak_plantation": 20,
|
||||||
|
"donaldsonville": 15,
|
||||||
|
"pearl_river": 10
|
||||||
|
},
|
||||||
|
"water_drain_per_pop": 0.004,
|
||||||
|
"food_drain_per_pop": 0.003,
|
||||||
|
"security_day_regen": 0.2,
|
||||||
|
"security_night_drain": 0.3,
|
||||||
|
"conflict_security_range": [-0.5, 0.2]
|
||||||
|
},
|
||||||
|
|
||||||
|
"faction_relations": {
|
||||||
|
"initial_volatility": 0.05,
|
||||||
|
"drift_per_llm_tick": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
"llm": {
|
||||||
|
"enabled": false,
|
||||||
|
"model_mj": "qwen2.5:14b",
|
||||||
|
"model_pnj": "qwen2.5:7b",
|
||||||
|
"ollama_url": "http://localhost:11434",
|
||||||
|
"chroma_url": "http://localhost:8000",
|
||||||
|
"chroma_collection": "fallout_vst"
|
||||||
|
},
|
||||||
|
|
||||||
|
"modes": {
|
||||||
|
"guerre_commerciale": {
|
||||||
|
"encounter": { "global_rate_multiplier": 1.2, "combat_lethality": 0.9 },
|
||||||
|
"survival": { "drain_speed": 1.5 },
|
||||||
|
"economy": { "caps_global_multiplier": 1.5, "trade_price_variance": 0.5 },
|
||||||
|
"world": {
|
||||||
|
"pop_count_by_zone": { "nola_cbd": 15, "pearl_river": 6, "donaldsonville": 8 },
|
||||||
|
"security_day_regen": 0.1, "security_night_drain": 0.5, "conflict_security_range": [-1.5, 0.0]
|
||||||
|
},
|
||||||
|
"faction_relations": { "initial_volatility": 0.4, "drift_per_llm_tick": 8 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,79 +29,79 @@ _ROLES: dict[str, dict] = {
|
|||||||
"description": "Pillard des terres désolées, violent et imprévisible",
|
"description": "Pillard des terres désolées, violent et imprévisible",
|
||||||
"special": {"STR": (5,8), "PER": (4,6), "END": (5,7), "CHA": (3,5), "INT": (3,5), "AGI": (5,7), "LCK": (3,6)},
|
"special": {"STR": (5,8), "PER": (4,6), "END": (5,7), "CHA": (3,5), "INT": (3,5), "AGI": (5,7), "LCK": (3,6)},
|
||||||
"faction_pool": ["ecumeurs", None],
|
"faction_pool": ["ecumeurs", None],
|
||||||
"caps": (0, 100),
|
"caps": (0, 60),
|
||||||
},
|
},
|
||||||
"marchand": {
|
"marchand": {
|
||||||
"description": "Commerçant itinérant, prudent mais bien connecté",
|
"description": "Commerçant itinérant, prudent mais bien connecté",
|
||||||
"special": {"STR": (4,6), "PER": (5,7), "END": (4,6), "CHA": (7,9), "INT": (6,8), "AGI": (4,6), "LCK": (5,8)},
|
"special": {"STR": (4,6), "PER": (5,7), "END": (4,6), "CHA": (7,9), "INT": (6,8), "AGI": (4,6), "LCK": (5,8)},
|
||||||
"faction_pool": ["consortium", "regie", None],
|
"faction_pool": ["consortium", "regie", None],
|
||||||
"caps": (200, 2000),
|
"caps": (60, 400),
|
||||||
},
|
},
|
||||||
"garde": {
|
"garde": {
|
||||||
"description": "Milicien ou garde d'une faction, entraîné et discipliné",
|
"description": "Milicien ou garde d'une faction, entraîné et discipliné",
|
||||||
"special": {"STR": (6,8), "PER": (5,7), "END": (6,8), "CHA": (4,6), "INT": (4,6), "AGI": (5,7), "LCK": (4,6)},
|
"special": {"STR": (6,8), "PER": (5,7), "END": (6,8), "CHA": (4,6), "INT": (4,6), "AGI": (5,7), "LCK": (4,6)},
|
||||||
"faction_pool": ["union", "cda", "syndicat_capitole", "regie"],
|
"faction_pool": ["union", "cda", "syndicat_capitole", "regie"],
|
||||||
"caps": (50, 300),
|
"caps": (15, 100),
|
||||||
},
|
},
|
||||||
"eclaireur": {
|
"eclaireur": {
|
||||||
"description": "Rôdeur solitaire des marais, discret et endurant",
|
"description": "Rôdeur solitaire des marais, discret et endurant",
|
||||||
"special": {"STR": (5,7), "PER": (7,9), "END": (6,8), "CHA": (3,5), "INT": (5,7), "AGI": (7,9), "LCK": (5,7)},
|
"special": {"STR": (5,7), "PER": (7,9), "END": (6,8), "CHA": (3,5), "INT": (5,7), "AGI": (7,9), "LCK": (5,7)},
|
||||||
"faction_pool": [None, "ecumeurs"],
|
"faction_pool": [None, "ecumeurs"],
|
||||||
"caps": (30, 200),
|
"caps": (10, 80),
|
||||||
},
|
},
|
||||||
"docteur": {
|
"docteur": {
|
||||||
"description": "Médecin de terrain, rare et précieux dans les terres désolées",
|
"description": "Médecin de terrain, rare et précieux dans les terres désolées",
|
||||||
"special": {"STR": (4,6), "PER": (6,8), "END": (5,7), "CHA": (6,8), "INT": (8,10), "AGI": (5,7), "LCK": (5,7)},
|
"special": {"STR": (4,6), "PER": (6,8), "END": (5,7), "CHA": (6,8), "INT": (8,10), "AGI": (5,7), "LCK": (5,7)},
|
||||||
"faction_pool": ["union", "consortium", None],
|
"faction_pool": ["union", "consortium", None],
|
||||||
"caps": (100, 500),
|
"caps": (40, 200),
|
||||||
},
|
},
|
||||||
"technicien": {
|
"technicien": {
|
||||||
"description": "Bricoleur et réparateur, obsédé par les technologies de l'Ancien Monde",
|
"description": "Bricoleur et réparateur, obsédé par les technologies de l'Ancien Monde",
|
||||||
"special": {"STR": (4,6), "PER": (6,8), "END": (4,6), "CHA": (4,6), "INT": (8,10), "AGI": (6,8), "LCK": (4,7)},
|
"special": {"STR": (4,6), "PER": (6,8), "END": (4,6), "CHA": (4,6), "INT": (8,10), "AGI": (6,8), "LCK": (4,7)},
|
||||||
"faction_pool": ["consortium", "union", None],
|
"faction_pool": ["consortium", "union", None],
|
||||||
"caps": (50, 400),
|
"caps": (20, 150),
|
||||||
},
|
},
|
||||||
"raider_boss": {
|
"raider_boss": {
|
||||||
"description": "Chef de bande, charismatique et dangereux",
|
"description": "Chef de bande, charismatique et dangereux",
|
||||||
"special": {"STR": (7,9), "PER": (5,7), "END": (6,8), "CHA": (6,8), "INT": (4,6), "AGI": (5,7), "LCK": (5,7)},
|
"special": {"STR": (7,9), "PER": (5,7), "END": (6,8), "CHA": (6,8), "INT": (4,6), "AGI": (5,7), "LCK": (5,7)},
|
||||||
"faction_pool": ["ecumeurs", None],
|
"faction_pool": ["ecumeurs", None],
|
||||||
"caps": (100, 1000),
|
"caps": (30, 200),
|
||||||
},
|
},
|
||||||
"lieutenant": {
|
"lieutenant": {
|
||||||
"description": "Bras droit d'un chef de faction, mieux équipé qu'un garde",
|
"description": "Bras droit d'un chef de faction, mieux équipé qu'un garde",
|
||||||
"special": {"STR": (6,8), "PER": (5,7), "END": (6,8), "CHA": (5,7), "INT": (5,7), "AGI": (5,7), "LCK": (4,6)},
|
"special": {"STR": (6,8), "PER": (5,7), "END": (6,8), "CHA": (5,7), "INT": (5,7), "AGI": (5,7), "LCK": (4,6)},
|
||||||
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
|
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
|
||||||
"caps": (200, 800),
|
"caps": (60, 300),
|
||||||
},
|
},
|
||||||
"chasseur_primes": {
|
"chasseur_primes": {
|
||||||
"description": "Mercenaire spécialisé dans la traque et la capture",
|
"description": "Mercenaire spécialisé dans la traque et la capture",
|
||||||
"special": {"STR": (5,8), "PER": (7,9), "END": (5,7), "CHA": (4,6), "INT": (5,7), "AGI": (6,8), "LCK": (5,7)},
|
"special": {"STR": (5,8), "PER": (7,9), "END": (5,7), "CHA": (4,6), "INT": (5,7), "AGI": (6,8), "LCK": (5,7)},
|
||||||
"faction_pool": [None, "syndicat_capitole"],
|
"faction_pool": [None, "syndicat_capitole"],
|
||||||
"caps": (200, 800),
|
"caps": (80, 350),
|
||||||
},
|
},
|
||||||
"passeur": {
|
"passeur": {
|
||||||
"description": "Guide qui connaît les routes secrètes entre les zones",
|
"description": "Guide qui connaît les routes secrètes entre les zones",
|
||||||
"special": {"STR": (4,6), "PER": (6,8), "END": (5,7), "CHA": (5,7), "INT": (5,7), "AGI": (7,9), "LCK": (5,7)},
|
"special": {"STR": (4,6), "PER": (6,8), "END": (5,7), "CHA": (5,7), "INT": (5,7), "AGI": (7,9), "LCK": (5,7)},
|
||||||
"faction_pool": [None, "ecumeurs"],
|
"faction_pool": [None, "ecumeurs"],
|
||||||
"caps": (100, 600),
|
"caps": (30, 180),
|
||||||
},
|
},
|
||||||
"scavenger": {
|
"scavenger": {
|
||||||
"description": "Récupérateur de ferraille et de technologie pré-guerre",
|
"description": "Récupérateur de ferraille et de technologie pré-guerre",
|
||||||
"special": {"STR": (4,7), "PER": (6,8), "END": (5,7), "CHA": (3,5), "INT": (5,8), "AGI": (5,7), "LCK": (5,8)},
|
"special": {"STR": (4,7), "PER": (6,8), "END": (5,7), "CHA": (3,5), "INT": (5,8), "AGI": (5,7), "LCK": (5,8)},
|
||||||
"faction_pool": [None],
|
"faction_pool": [None],
|
||||||
"caps": (20, 200),
|
"caps": (5, 70),
|
||||||
},
|
},
|
||||||
"chef_faction": {
|
"chef_faction": {
|
||||||
"description": "Chef de faction — tier 0, hors simulation, décisions narratives LLM",
|
"description": "Chef de faction — tier 0, hors simulation, décisions narratives LLM",
|
||||||
"special": {"STR": (5,8), "PER": (6,8), "END": (5,8), "CHA": (7,10), "INT": (7,10), "AGI": (4,7), "LCK": (5,8)},
|
"special": {"STR": (5,8), "PER": (6,8), "END": (5,8), "CHA": (7,10), "INT": (7,10), "AGI": (4,7), "LCK": (5,8)},
|
||||||
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
|
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
|
||||||
"caps": (1000, 8000),
|
"caps": (300, 1500),
|
||||||
},
|
},
|
||||||
"survivant_generique": {
|
"survivant_generique": {
|
||||||
"description": "Habitant lambda des terres désolées, équilibré et débrouillard",
|
"description": "Habitant lambda des terres désolées, équilibré et débrouillard",
|
||||||
"special": {"STR": (5,7), "PER": (5,7), "END": (5,7), "CHA": (5,7), "INT": (5,7), "AGI": (5,7), "LCK": (5,7)},
|
"special": {"STR": (5,7), "PER": (5,7), "END": (5,7), "CHA": (5,7), "INT": (5,7), "AGI": (5,7), "LCK": (5,7)},
|
||||||
"faction_pool": [None, "union", "regie"],
|
"faction_pool": [None, "union", "regie"],
|
||||||
"caps": (10, 200),
|
"caps": (5, 60),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import random
|
|||||||
import db
|
import db
|
||||||
import actions as act_module
|
import actions as act_module
|
||||||
import combat_engine
|
import combat_engine
|
||||||
|
import sim_config
|
||||||
|
|
||||||
_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
|
_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||||
|
|
||||||
@@ -91,19 +92,22 @@ def should_trigger_encounter(char: dict, world_state: dict, rng: random.Random)
|
|||||||
if not profile:
|
if not profile:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
base_chance = profile["chance_per_tick"]
|
enc_cfg = sim_config.get("encounter")
|
||||||
|
if not enc_cfg.get("enabled", True):
|
||||||
|
return False
|
||||||
|
|
||||||
|
rate_multiplier = enc_cfg.get("global_rate_multiplier", 1.0)
|
||||||
|
base_chance = profile["chance_per_tick"] * rate_multiplier
|
||||||
|
|
||||||
# Modificateurs selon l'état du PNJ
|
# Modificateurs selon l'état du PNJ
|
||||||
state = char.get("_pnj_state") or {}
|
state = char.get("_pnj_state") or {}
|
||||||
stress = state.get("stress_level", 0) if isinstance(state, dict) else 0
|
stress = state.get("stress_level", 0) if isinstance(state, dict) else 0
|
||||||
|
|
||||||
# Stress élevé → PNJ moins attentif → légèrement plus de chances d'encounter
|
|
||||||
stress_mod = min(0.05, stress * 0.005)
|
stress_mod = min(0.05, stress * 0.005)
|
||||||
# HP bas → PNJ plus vulnérable, attire prédateurs
|
|
||||||
hp_ratio = char.get("hp", 1) / max(char.get("max_hp", 1), 1)
|
hp_ratio = char.get("hp", 1) / max(char.get("max_hp", 1), 1)
|
||||||
hp_mod = 0.03 if hp_ratio < 0.5 else 0.0
|
hp_mod = 0.03 if hp_ratio < 0.5 else 0.0
|
||||||
|
|
||||||
final_chance = base_chance + stress_mod + hp_mod
|
final_chance = min(0.95, base_chance + stress_mod + hp_mod)
|
||||||
return rng.random() < final_chance
|
return rng.random() < final_chance
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import character_creator as cc
|
|||||||
import pnj_factory as factory
|
import pnj_factory as factory
|
||||||
import db
|
import db
|
||||||
|
|
||||||
SESSION_ID = 1
|
import os
|
||||||
|
SESSION_ID = int(os.getenv("SESSION_ID", 1))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tier 1 — ACTIFS SIM (15 PNJ, boucle survival + actions complète)
|
# Tier 1 — ACTIFS SIM (15 PNJ, boucle survival + actions complète)
|
||||||
|
|||||||
+56
-16
@@ -1,50 +1,86 @@
|
|||||||
"""
|
"""
|
||||||
Lanceur de simulation — Fallout Venice of Wasteland
|
Lanceur de simulation — Fallout Venice of Wasteland
|
||||||
Usage :
|
Usage :
|
||||||
python run.py # session 1, bot-speed
|
python run.py # session 1, sim_001.json, bot-speed
|
||||||
SESSION_ID=1 python run.py
|
SESSION_ID=2 python run.py # session 2, sim_002.json
|
||||||
TICK_SLEEP_SEC=2 python run.py # 1 tick toutes les 2 secondes
|
SIM_CONFIG=src/config/sim_002.json python run.py
|
||||||
|
python run.py --config src/config/sim_002.json
|
||||||
|
TICK_SLEEP_SEC=60 python run.py # override tick speed (priorité sur config)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time, sys, os, argparse
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Ajoute src/engine au path
|
|
||||||
sys.path.insert(0, os.path.dirname(__file__))
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import sim_config as cfg
|
||||||
import tick as tick_engine
|
import tick as tick_engine
|
||||||
from config import SESSION_ID, TICK_SLEEP_SEC
|
from config import SESSION_ID
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def _init_world_state(session_id: int, day: int):
|
||||||
|
"""Initialise le world_state avec les pop_count de la config si vide."""
|
||||||
|
existing = db.get_world_state(session_id, day)
|
||||||
|
if existing:
|
||||||
|
return # déjà initialisé
|
||||||
|
pop_counts = cfg.get("world").get("pop_count_by_zone", {})
|
||||||
|
for location_slug, pop in pop_counts.items():
|
||||||
|
db.upsert_world_state(
|
||||||
|
session_id, location_slug, day,
|
||||||
|
water_level=80, food_level=70, security_level=60,
|
||||||
|
pop_count=pop
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def run(config_path: str | None = None):
|
||||||
|
# Charger la config
|
||||||
|
cfg.load(config_path, session_id=SESSION_ID)
|
||||||
|
tick_cfg = cfg.get("tick")
|
||||||
|
active_mode = cfg.mode()
|
||||||
|
|
||||||
|
# TICK_SLEEP_SEC env var override (compatibilité)
|
||||||
|
tick_sleep = float(os.getenv("TICK_SLEEP_SEC", tick_cfg.get("tick_sleep_sec", 0)))
|
||||||
|
|
||||||
session = db.get_session(SESSION_ID)
|
session = db.get_session(SESSION_ID)
|
||||||
|
if not session:
|
||||||
|
print(f"Session {SESSION_ID} introuvable en DB.")
|
||||||
|
return
|
||||||
|
|
||||||
day = session["current_day"]
|
day = session["current_day"]
|
||||||
current_tick = session["current_tick"]
|
current_tick = session["current_tick"]
|
||||||
mode = session["mode"]
|
db_mode = session["mode"]
|
||||||
status = session["status"]
|
status = session["status"]
|
||||||
|
|
||||||
if status != "active":
|
if status != "active":
|
||||||
print(f"Session {SESSION_ID} n'est pas active (status={status}). Abandon.")
|
print(f"Session {SESSION_ID} n'est pas active (status={status}). Abandon.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Mode : config.json prime sur la DB
|
||||||
|
effective_mode = active_mode or db_mode
|
||||||
|
|
||||||
print(f"=== Simulation Fallout : Venice of Wasteland ===")
|
print(f"=== Simulation Fallout : Venice of Wasteland ===")
|
||||||
print(f"Session #{SESSION_ID} — {session['name']}")
|
print(f"Session #{SESSION_ID} — {session['name']}")
|
||||||
print(f"Mode : {mode} | Départ : Jour {day}, Tick {current_tick}h")
|
print(f"Mode : {effective_mode} | Config : {cfg.get()['_active_mode']}")
|
||||||
|
print(f"Départ : Jour {day}, Tick {current_tick}h | Tick sleep : {tick_sleep}s")
|
||||||
print(f"Seed globale : {session['seed_global']}")
|
print(f"Seed globale : {session['seed_global']}")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Initialiser le world_state si vide
|
||||||
|
_init_world_state(SESSION_ID, day)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
tick_engine.process_tick(SESSION_ID, day, current_tick, mode)
|
tick_engine.process_tick(SESSION_ID, day, current_tick, effective_mode)
|
||||||
db.update_session_tick(SESSION_ID, day, current_tick)
|
db.update_session_tick(SESSION_ID, day, current_tick)
|
||||||
|
|
||||||
if TICK_SLEEP_SEC > 0:
|
if tick_sleep > 0:
|
||||||
time.sleep(TICK_SLEEP_SEC)
|
time.sleep(tick_sleep)
|
||||||
|
|
||||||
day, current_tick = tick_engine.next_tick(day, current_tick)
|
day, current_tick = tick_engine.next_tick(day, current_tick)
|
||||||
|
|
||||||
|
# Réinit world_state au début de chaque nouveau jour
|
||||||
|
if current_tick == 0:
|
||||||
|
_init_world_state(SESSION_ID, day)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"\n[STOP] Simulation interrompue — Jour {day}, Tick {current_tick}h")
|
print(f"\n[STOP] Simulation interrompue — Jour {day}, Tick {current_tick}h")
|
||||||
db.update_session_tick(SESSION_ID, day, current_tick)
|
db.update_session_tick(SESSION_ID, day, current_tick)
|
||||||
@@ -52,4 +88,8 @@ def run():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
parser = argparse.ArgumentParser(description="Simulation Fallout Venice of Wasteland")
|
||||||
|
parser.add_argument("--config", "-c", default=None,
|
||||||
|
help="Chemin vers le fichier de config JSON (ex: src/config/sim_002.json)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
run(config_path=args.config)
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
SimConfig — Gestionnaire de configuration de simulation.
|
||||||
|
|
||||||
|
Charge src/config/sim_{SESSION_ID:03d}.json (ou SIM_CONFIG env var).
|
||||||
|
Les sections 'modes.<mode>' écrasent les valeurs de base (deep merge).
|
||||||
|
Usage :
|
||||||
|
import sim_config
|
||||||
|
sim_config.load() # charge sim_001.json (SESSION_ID=1)
|
||||||
|
sim_config.load("src/config/sim_002.json")
|
||||||
|
sim_config.get("survival")["drain_speed"]
|
||||||
|
sim_config.get("economy")["caps_global_multiplier"]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
_CONFIG_DIR = os.path.join(os.path.dirname(__file__), "..", "config")
|
||||||
|
_cfg: dict = {}
|
||||||
|
_loaded_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def load(path: str | None = None, session_id: int | None = None) -> dict:
|
||||||
|
"""Charge et retourne la config. Priorité : path > SIM_CONFIG env > sim_{id:03d}.json"""
|
||||||
|
global _cfg, _loaded_path
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
path = os.getenv("SIM_CONFIG")
|
||||||
|
if path is None:
|
||||||
|
sid = session_id or int(os.getenv("SESSION_ID", 1))
|
||||||
|
path = os.path.join(_CONFIG_DIR, f"sim_{sid:03d}.json")
|
||||||
|
|
||||||
|
with open(path, encoding="utf-8") as f:
|
||||||
|
base = json.load(f)
|
||||||
|
|
||||||
|
# Merge des overrides du mode actif
|
||||||
|
mode = base.get("mode", "pacifiste")
|
||||||
|
modes = base.get("modes", {})
|
||||||
|
if mode in modes:
|
||||||
|
_cfg = _deep_merge(base, modes[mode])
|
||||||
|
else:
|
||||||
|
_cfg = dict(base)
|
||||||
|
|
||||||
|
_cfg["_active_mode"] = mode
|
||||||
|
_loaded_path = path
|
||||||
|
return _cfg
|
||||||
|
|
||||||
|
|
||||||
|
def get(section: str | None = None, key: str | None = None, default=None):
|
||||||
|
"""
|
||||||
|
Accès à la config chargée.
|
||||||
|
sim_config.get() → dict complet
|
||||||
|
sim_config.get("survival") → section survival
|
||||||
|
sim_config.get("survival","drain_speed") → valeur scalaire
|
||||||
|
"""
|
||||||
|
if not _cfg:
|
||||||
|
load() # autoload avec défauts
|
||||||
|
|
||||||
|
if section is None:
|
||||||
|
return _cfg
|
||||||
|
|
||||||
|
sec = _cfg.get(section, {})
|
||||||
|
if key is None:
|
||||||
|
return sec
|
||||||
|
return sec.get(key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def mode() -> str:
|
||||||
|
return _cfg.get("_active_mode", "pacifiste")
|
||||||
|
|
||||||
|
|
||||||
|
def _deep_merge(base, override):
|
||||||
|
if not isinstance(base, dict) or not isinstance(override, dict):
|
||||||
|
return override if override is not None else base
|
||||||
|
out = dict(base)
|
||||||
|
for k, v in override.items():
|
||||||
|
out[k] = _deep_merge(out.get(k), v)
|
||||||
|
return out
|
||||||
+35
-31
@@ -13,36 +13,33 @@ Fatigue : accumulée, réduit PA et inflige -2 PV par tranche de 2 pts au début
|
|||||||
|
|
||||||
import db
|
import db
|
||||||
import psycopg2.extras
|
import psycopg2.extras
|
||||||
|
import sim_config
|
||||||
|
|
||||||
# Heures entre dégradations d'état (conditions normales, mode normal)
|
# Tables 2D20 officielles (Manuel FR ch.5) — NE PAS MODIFIER, utiliser drain_speed pour tuner
|
||||||
HUNGER_TICKS = {
|
_HUNGER_TICKS_BASE = {4: 1, 3: 4, 2: 8, 1: 16}
|
||||||
# de Repu(4) → Rassasié(3) après 1h
|
_THIRST_TICKS_BASE = {3: 1, 2: 2, 1: 4}
|
||||||
4: 1,
|
_SLEEP_TICKS_BASE = {3: 8, 2: 8, 1: 8}
|
||||||
# de Rassasié(3) → Petit creux(2) après 4h
|
|
||||||
3: 4,
|
|
||||||
# de Petit creux(2) → Faim(1) après 8h
|
|
||||||
2: 8,
|
|
||||||
# de Faim(1) → Affamé(0) après 16h → +1 Fatigue/jour = +1 toutes les 24h depuis Faim
|
|
||||||
1: 16,
|
|
||||||
}
|
|
||||||
|
|
||||||
THIRST_TICKS = {
|
|
||||||
# de Désaltéré(3) → Hydraté(2) après 1h
|
|
||||||
3: 1,
|
|
||||||
# de Hydraté(2) → Soif(1) après 2h
|
|
||||||
2: 2,
|
|
||||||
# de Soif(1) → Déshydraté(0) après 4h → +1 Fatigue/8h
|
|
||||||
1: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
SLEEP_TICKS = {
|
def _scaled_ticks(base: dict, speed: float) -> dict:
|
||||||
# de Reposé(3) → Fatigué(2) après 8h
|
"""Applique drain_speed : vitesse 2.0 = seuils divisés par 2 (dégrade 2x plus vite)."""
|
||||||
3: 8,
|
if speed <= 0:
|
||||||
# de Fatigué(2) → Éreinté(1) après 8h
|
speed = 1.0
|
||||||
2: 8,
|
return {k: max(1, round(v / speed)) for k, v in base.items()}
|
||||||
# de Éreinté(1) → Épuisé(0) après 8h
|
|
||||||
1: 8,
|
|
||||||
}
|
def _get_thresholds():
|
||||||
|
speed = sim_config.get("survival", "drain_speed") or 1.0
|
||||||
|
return (
|
||||||
|
_scaled_ticks(_HUNGER_TICKS_BASE, speed),
|
||||||
|
_scaled_ticks(_THIRST_TICKS_BASE, speed),
|
||||||
|
_scaled_ticks(_SLEEP_TICKS_BASE, speed),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
HUNGER_TICKS = _HUNGER_TICKS_BASE
|
||||||
|
THIRST_TICKS = _THIRST_TICKS_BASE
|
||||||
|
SLEEP_TICKS = _SLEEP_TICKS_BASE
|
||||||
|
|
||||||
# Noms lisibles
|
# Noms lisibles
|
||||||
HUNGER_NAMES = {4: "repu", 3: "rassasié", 2: "petit_creux", 1: "faim", 0: "affamé"}
|
HUNGER_NAMES = {4: "repu", 3: "rassasié", 2: "petit_creux", 1: "faim", 0: "affamé"}
|
||||||
@@ -89,6 +86,13 @@ def process_survival_tick(session_id: int, day: int, tick: int):
|
|||||||
Met à jour faim/soif/sommeil/fatigue de chaque PNJ vivant.
|
Met à jour faim/soif/sommeil/fatigue de chaque PNJ vivant.
|
||||||
Tue les PNJ dont les PV tombent à 0 ou en dessous.
|
Tue les PNJ dont les PV tombent à 0 ou en dessous.
|
||||||
"""
|
"""
|
||||||
|
sv_cfg = sim_config.get("survival")
|
||||||
|
if not sv_cfg.get("enabled", True):
|
||||||
|
return
|
||||||
|
|
||||||
|
hunger_thr, thirst_thr, sleep_thr = _get_thresholds()
|
||||||
|
fatigue_dmg_per_2pts = sv_cfg.get("fatigue_hp_per_2pts", 2)
|
||||||
|
|
||||||
characters = db.get_characters(session_id)
|
characters = db.get_characters(session_id)
|
||||||
|
|
||||||
for char in characters:
|
for char in characters:
|
||||||
@@ -100,7 +104,7 @@ def process_survival_tick(session_id: int, day: int, tick: int):
|
|||||||
|
|
||||||
# --- Faim ---
|
# --- Faim ---
|
||||||
new_hunger, new_hunger_ticks = _tick_stat(
|
new_hunger, new_hunger_ticks = _tick_stat(
|
||||||
sv["hunger"], sv["hunger_ticks"], HUNGER_TICKS
|
sv["hunger"], sv["hunger_ticks"], hunger_thr
|
||||||
)
|
)
|
||||||
# +1 Fatigue par jour si Affamé (tick 0 de chaque jour = tick 0)
|
# +1 Fatigue par jour si Affamé (tick 0 de chaque jour = tick 0)
|
||||||
if new_hunger == 0 and tick == 0:
|
if new_hunger == 0 and tick == 0:
|
||||||
@@ -110,7 +114,7 @@ def process_survival_tick(session_id: int, day: int, tick: int):
|
|||||||
|
|
||||||
# --- Soif ---
|
# --- Soif ---
|
||||||
new_thirst, new_thirst_ticks = _tick_stat(
|
new_thirst, new_thirst_ticks = _tick_stat(
|
||||||
sv["thirst"], sv["thirst_ticks"], THIRST_TICKS
|
sv["thirst"], sv["thirst_ticks"], thirst_thr
|
||||||
)
|
)
|
||||||
# Si déshydraté, +1 Fatigue toutes les 8h
|
# Si déshydraté, +1 Fatigue toutes les 8h
|
||||||
if new_thirst == 0:
|
if new_thirst == 0:
|
||||||
@@ -125,7 +129,7 @@ def process_survival_tick(session_id: int, day: int, tick: int):
|
|||||||
|
|
||||||
# --- Sommeil ---
|
# --- Sommeil ---
|
||||||
new_sleep, new_sleep_ticks = _tick_stat(
|
new_sleep, new_sleep_ticks = _tick_stat(
|
||||||
sv["sleep"], sv["sleep_ticks"], SLEEP_TICKS
|
sv["sleep"], sv["sleep_ticks"], sleep_thr
|
||||||
)
|
)
|
||||||
# Si Épuisé, +1 Fatigue toutes les 4h
|
# Si Épuisé, +1 Fatigue toutes les 4h
|
||||||
if new_sleep == 0:
|
if new_sleep == 0:
|
||||||
@@ -144,7 +148,7 @@ def process_survival_tick(session_id: int, day: int, tick: int):
|
|||||||
# Règle 2D20 : "début de scène" ≠ chaque heure. On applique une seule fois
|
# Règle 2D20 : "début de scène" ≠ chaque heure. On applique une seule fois
|
||||||
# à l'aube pour éviter la mort en cascade sur une seule nuit.
|
# à l'aube pour éviter la mort en cascade sur une seule nuit.
|
||||||
if tick == 0 and new_fatigue >= 2:
|
if tick == 0 and new_fatigue >= 2:
|
||||||
hp_loss = (new_fatigue // 2) * 2
|
hp_loss = (new_fatigue // 2) * fatigue_dmg_per_2pts
|
||||||
new_hp = max(0, char["hp"] - hp_loss)
|
new_hp = max(0, char["hp"] - hp_loss)
|
||||||
else:
|
else:
|
||||||
new_hp = char["hp"]
|
new_hp = char["hp"]
|
||||||
|
|||||||
+12
-7
@@ -10,6 +10,7 @@ import db
|
|||||||
import survival
|
import survival
|
||||||
import actions
|
import actions
|
||||||
import encounter
|
import encounter
|
||||||
|
import sim_config
|
||||||
|
|
||||||
|
|
||||||
def is_daytime(tick: int) -> bool:
|
def is_daytime(tick: int) -> bool:
|
||||||
@@ -112,16 +113,20 @@ def _compute_location_tick_delta(ws: dict, phase: str, mode: str) -> dict:
|
|||||||
Calcule les deltas passifs sur les ressources d'un lieu pour un tick.
|
Calcule les deltas passifs sur les ressources d'un lieu pour un tick.
|
||||||
Valeurs fines — 24 ticks = 1 jour, effets nets sur la journée.
|
Valeurs fines — 24 ticks = 1 jour, effets nets sur la journée.
|
||||||
"""
|
"""
|
||||||
# Consommation de base par tick (population divisée par 24)
|
wcfg = sim_config.get("world")
|
||||||
pop = ws.get("pop_count", 0)
|
pop = ws.get("pop_count", 0)
|
||||||
base_water_drain = -(pop * 0.004) # ~10% de consommation/jour pour 250 pop
|
|
||||||
base_food_drain = -(pop * 0.003)
|
|
||||||
|
|
||||||
# La nuit réduit la sécurité légèrement, le jour la restaure
|
water_drain_pp = wcfg.get("water_drain_per_pop", 0.004)
|
||||||
security_delta = +0.2 if phase == "jour" else -0.3
|
food_drain_pp = wcfg.get("food_drain_per_pop", 0.003)
|
||||||
|
sec_day_regen = wcfg.get("security_day_regen", 0.2)
|
||||||
|
sec_night_drain = wcfg.get("security_night_drain", 0.3)
|
||||||
|
conflict_rng = wcfg.get("conflict_security_range", [-0.5, 0.2])
|
||||||
|
|
||||||
# En mode pacifique, pas de dégradation supplémentaire
|
base_water_drain = -(pop * water_drain_pp)
|
||||||
conflict_modifier = 0 if mode == "pacifique" else random.uniform(-0.5, 0.2)
|
base_food_drain = -(pop * food_drain_pp)
|
||||||
|
security_delta = sec_day_regen if phase == "jour" else -sec_night_drain
|
||||||
|
|
||||||
|
conflict_modifier = 0.0 if mode in ("pacifiste", "pacifique") else random.uniform(*conflict_rng)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"water": base_water_drain + (0.1 if phase == "jour" else 0),
|
"water": base_water_drain + (0.1 if phase == "jour" else 0),
|
||||||
|
|||||||
Reference in New Issue
Block a user