feat: systeme tiers PNJ 0-3 + equipment by role + archetype factory

migration_003: tier/archetype_key, boss exclus sim, equipement distinctif
equipment_by_role.json: loadouts par role (armes/armure/consommables)
archetypes.json: 11 archetypes tier3 + lieutenant tier2
pnj_factory.py: spawn_from_archetype, equip_character, spawn_batch
character_creator.py: nouveaux roles + tier + archetype_key dans INSERT
init_pnj.py: 10 PNJ tier1 + 8 lieutenants tier2 par faction
db.py: get_characters(min_tier=1) exclut boss, get_all_characters pour seeds
tick.py: seeds tous tiers, survival/actions tier>=1 seulement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Corback
2026-06-16 01:04:01 +02:00
parent c422d62f10
commit 873106d5cb
8 changed files with 895 additions and 44 deletions
+159
View File
@@ -0,0 +1,159 @@
{
"_comment": "Bibliothèque d'archétypes PNJ. Le LLM pioche ici pour instancier un PNJ de passage (tier 3) ou nommé (tier 2). Chaque archétype définit le template — le LLM choisit le nom, la faction et la localisation.",
"garde_faction": {
"role": "garde",
"origin": "survivant",
"tier": 3,
"description": "Garde d'une faction, posté à un point stratégique",
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak"],
"location_hints": ["zone contrôlée par la faction d'appartenance"],
"caps_range": [50, 250],
"personality_traits": ["loyal", "méfiant envers étrangers", "obéit aux ordres"],
"motivations": ["protéger son territoire", "nourrir sa famille", "progresser dans la hiérarchie"],
"equipment_role": "garde"
},
"vendeur_ambulant": {
"role": "marchand",
"origin": "survivant",
"tier": 3,
"description": "Marchand itinérant qui circule entre les zones sûres",
"faction_pool": [null, "consortium", "regie"],
"location_hints": ["laplace", "nola_vieux_carre", "independance", "baton_rouge"],
"caps_range": [300, 1500],
"personality_traits": ["opportuniste", "bavard", "prudent", "connait les routes"],
"motivations": ["faire du profit", "survivre", "collecter des informations"],
"equipment_role": "marchand"
},
"habitant_nola": {
"role": "survivant_generique",
"origin": "survivant",
"tier": 3,
"description": "Habitant ordinaire du Vieux Carré ou d'Indépendance",
"faction_pool": [null, "union", "regie"],
"location_hints": ["nola_vieux_carre", "independance"],
"caps_range": [10, 100],
"personality_traits": ["peureux", "curieux", "communautaire", "se méfie des armés"],
"motivations": ["rester en vie", "protéger sa famille", "trouver de l'eau propre"],
"equipment_role": "survivant_generique"
},
"pillard_errant": {
"role": "pillard",
"origin": "survivant",
"tier": 3,
"description": "Pillard solitaire ou en petite bande, sans base fixe",
"faction_pool": ["ecumeurs", null],
"location_hints": ["nola_cbd", "pearl_river", "donaldsonville"],
"caps_range": [0, 150],
"personality_traits": ["impulsif", "violent", "opportuniste", "craintif devant la force"],
"motivations": ["piller", "manger", "dominer les faibles"],
"equipment_role": "pillard"
},
"passeur_bayou": {
"role": "passeur",
"origin": "survivant",
"tier": 3,
"description": "Guide qui connaît les routes secrètes à travers les bayous",
"faction_pool": [null, "ecumeurs"],
"location_hints": ["pearl_river", "donaldsonville", "oak_plantation"],
"caps_range": [100, 600],
"personality_traits": ["taciturne", "fiable si payé", "indépendant", "survivaliste"],
"motivations": ["caps", "indépendance totale", "connaître les secrets des factions"],
"equipment_role": "passeur"
},
"chasseur_primes": {
"role": "chasseur_primes",
"origin": "survivant",
"tier": 3,
"description": "Mercenaire qui traite les contrats d'élimination ou de capture",
"faction_pool": [null, "syndicat_capitole"],
"location_hints": ["baton_rouge", "laplace", "nola_fleuve"],
"caps_range": [200, 800],
"personality_traits": ["professionnel", "froid", "pragmatique", "loyauté achetable"],
"motivations": ["les caps", "la réputation", "accomplir le contrat"],
"equipment_role": "chasseur_primes"
},
"scavenger_freelance": {
"role": "scavenger",
"origin": "survivant",
"tier": 3,
"description": "Récupérateur de ferraille et de technologie pré-guerre",
"faction_pool": [null],
"location_hints": ["nola_cbd", "donaldsonville", "pearl_river"],
"caps_range": [20, 200],
"personality_traits": ["curieux", "bricoleur", "avare", "évite les conflits"],
"motivations": ["trouver des pièces rares", "vendre au Consortium", "survivre"],
"equipment_role": "scavenger"
},
"medecin_freelance": {
"role": "docteur",
"origin": "habitant_abri",
"tier": 3,
"description": "Médecin indépendant qui soigne sans question contre paiement",
"faction_pool": [null, "union", "consortium"],
"location_hints": ["laplace", "independance", "nola_vieux_carre"],
"caps_range": [200, 1000],
"personality_traits": ["neutre politiquement", "humaniste", "épuisé", "précieux"],
"motivations": ["soigner", "survivre", "garder la neutralité"],
"equipment_role": "docteur"
},
"messager_faction": {
"role": "passeur",
"origin": "survivant",
"tier": 3,
"description": "Courrier entre factions, transporte messages et petits colis",
"faction_pool": ["union", "consortium", "regie", "syndicat_capitole"],
"location_hints": ["toutes zones sûres"],
"caps_range": [50, 300],
"personality_traits": ["discret", "rapide", "mémorise sans noter", "refuse de lire les messages"],
"motivations": ["caps par livraison", "rester neutre", "éviter d'être intercepté"],
"equipment_role": "passeur"
},
"fugitif": {
"role": "survivant_generique",
"origin": "survivant",
"tier": 3,
"description": "Quelqu'un qui fuit une faction — esclave évadé, déserteur, criminel",
"faction_pool": [null],
"location_hints": ["pearl_river", "nola_cbd", "donaldsonville"],
"caps_range": [0, 50],
"personality_traits": ["terrorisé", "méfiant extrême", "désespéré", "potentiellement dangereux"],
"motivations": ["ne pas être retrouvé", "atteindre une zone sûre", "survivre une journée de plus"],
"equipment_role": "survivant_generique"
},
"technicien_freelance": {
"role": "technicien",
"origin": "initie_confrerie",
"tier": 3,
"description": "Ingénieur indépendant qui répare armes, générateurs et pip-boys",
"faction_pool": [null, "consortium"],
"location_hints": ["laplace", "independance", "baton_rouge"],
"caps_range": [100, 500],
"personality_traits": ["concentré", "peu bavard", "obsédé par les machines", "honnête sur les prix"],
"motivations": ["réparer des choses", "collecter des composants rares", "fuir le conflit"],
"equipment_role": "technicien"
},
"lieutenant_faction": {
"role": "lieutenant",
"origin": "survivant",
"tier": 2,
"description": "Officier de rang intermédiaire, bras droit opérationnel d'un chef",
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
"location_hints": ["QG ou zone clé de la faction"],
"caps_range": [200, 800],
"personality_traits": ["ambitieux", "dévoué à la faction", "respecté des gardes", "jaloux du boss"],
"motivations": ["monter en grade", "protéger la faction", "accumuler des ressources personnelles"],
"equipment_role": "lieutenant"
}
}
+248
View File
@@ -0,0 +1,248 @@
{
"_comment": "Loadouts d'équipement par rôle. Chaque entrée = un item inventory complet.",
"garde": {
"_desc": "Milicien faction — armé, léger blindage",
"weapons": [
{"item_slug": "pistolet_10mm", "item_name": "Pistolet 10mm", "item_type": "weapon_ranged",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "medium", "is_equipped": true}
],
"armor": [
{"item_slug": "veste_blindee", "item_name": "Veste blindée", "item_type": "armor",
"quantity": 1, "weight": 3.0, "armor_rating": 1, "is_equipped": true}
],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 1, "hp_heal": 4},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_purifiee", "item_name": "Eau purifiée", "item_type": "drink", "quantity": 2}
]
},
"marchand": {
"_desc": "Commerçant — défense minimale, stock commercial",
"weapons": [
{"item_slug": "pistolet_10mm", "item_name": "Pistolet 10mm", "item_type": "weapon_ranged",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "medium", "is_equipped": false}
],
"armor": [],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 2, "hp_heal": 4},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 5, "hp_heal": 2},
{"item_slug": "eau_purifiee", "item_name": "Eau purifiée", "item_type": "drink", "quantity": 3}
],
"stock": [
{"item_slug": "igname_cuite", "item_name": "Igname cuite", "item_type": "food", "quantity": 6, "hp_heal": 3},
{"item_slug": "outils_reparation","item_name": "Outils de répar.", "item_type": "misc", "quantity": 2},
{"item_slug": "eau_purifiee_vrac","item_name": "Eau purifiée (lot)","item_type": "drink","quantity": 5}
]
},
"eclaireur": {
"_desc": "Rôdeur marais — longue portée, discret, autonome",
"weapons": [
{"item_slug": "fusil_chasse", "item_name": "Fusil de chasse", "item_type": "weapon_ranged",
"quantity": 1, "weight": 3.0, "damage_dice": 3, "damage_type": "physical",
"damage_range": "medium", "is_equipped": true},
{"item_slug": "couteau_survie", "item_name": "Couteau de survie", "item_type": "weapon_melee",
"quantity": 1, "weight": 0.3, "damage_dice": 1, "damage_type": "physical",
"damage_range": "short", "is_equipped": false}
],
"armor": [
{"item_slug": "combinaison_renforcee", "item_name": "Combinaison renforcée", "item_type": "armor",
"quantity": 1, "weight": 2.0, "armor_rating": 1,
"properties": {"stealth_bonus": 1}, "is_equipped": true}
],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 2, "hp_heal": 4},
{"item_slug": "antidote", "item_name": "Antidote", "item_type": "drug", "quantity": 1},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 4, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 2,
"properties": {"irradiated": true, "rad_on_drink": 1}}
]
},
"docteur": {
"_desc": "Médecin terrain — trousse complète, défense minimale",
"weapons": [
{"item_slug": "pistolet_10mm", "item_name": "Pistolet 10mm", "item_type": "weapon_ranged",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "medium", "is_equipped": false}
],
"armor": [],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 5, "hp_heal": 4},
{"item_slug": "radaway", "item_name": "RadAway", "item_type": "drug", "quantity": 2, "rad_heal": 3},
{"item_slug": "trousse_soin", "item_name": "Trousse de soin","item_type":"misc", "quantity": 2},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_purifiee", "item_name": "Eau purifiée", "item_type": "drink", "quantity": 3}
]
},
"technicien": {
"_desc": "Ingénieur — arme énergie, outils de réparation",
"weapons": [
{"item_slug": "pistolet_laser", "item_name": "Pistolet laser", "item_type": "weapon_energy",
"quantity": 1, "weight": 1.0, "damage_dice": 2, "damage_type": "energy",
"damage_range": "medium", "is_equipped": true}
],
"armor": [],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 2, "hp_heal": 4},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 2,
"properties": {"irradiated": true, "rad_on_drink": 1}},
{"item_slug": "outils_reparation","item_name": "Outils de répar.","item_type": "misc", "quantity": 1},
{"item_slug": "composants_elec", "item_name": "Composants élec.", "item_type": "misc", "quantity": 5}
]
},
"pillard": {
"_desc": "Pillard — violence rapide, armure bricole",
"weapons": [
{"item_slug": "batte_cloutee", "item_name": "Batte cloutée", "item_type": "weapon_melee",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_bonus": 1, "damage_type": "physical",
"damage_range": "short", "properties": {"brutal": true}, "is_equipped": true},
{"item_slug": "couteau_combat", "item_name": "Couteau de combat", "item_type": "weapon_melee",
"quantity": 1, "weight": 0.5, "damage_dice": 1, "damage_type": "physical",
"damage_range": "short", "is_equipped": false}
],
"armor": [
{"item_slug": "armure_planches", "item_name": "Armure de planches", "item_type": "armor",
"quantity": 1, "weight": 4.0, "armor_rating": 1,
"properties": {"homemade": true}, "is_equipped": true}
],
"consumables": [
{"item_slug": "jet", "item_name": "Jet", "item_type": "drug", "quantity": 1,
"properties": {"speed_bonus": 1, "addictive": true}},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 2, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 1,
"properties": {"irradiated": true, "rad_on_drink": 1}}
]
},
"survivant_generique": {
"_desc": "Habitant lambda — outil basique, survie quotidienne",
"weapons": [
{"item_slug": "baton", "item_name": "Bâton", "item_type": "weapon_melee",
"quantity": 1, "weight": 1.0, "damage_dice": 1, "damage_type": "physical",
"damage_range": "short", "is_equipped": true}
],
"armor": [],
"consumables": [
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 2, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 2,
"properties": {"irradiated": true, "rad_on_drink": 1}}
]
},
"raider_boss": {
"_desc": "Chef de bande — armé lourd, butin accumulé",
"weapons": [
{"item_slug": "fusil_chasse", "item_name": "Fusil de chasse", "item_type": "weapon_ranged",
"quantity": 1, "weight": 3.0, "damage_dice": 3, "damage_type": "physical",
"damage_range": "medium", "is_equipped": true},
{"item_slug": "batte_cloutee", "item_name": "Batte cloutée", "item_type": "weapon_melee",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_bonus": 1, "damage_type": "physical",
"damage_range": "short", "properties": {"brutal": true}, "is_equipped": false}
],
"armor": [
{"item_slug": "cuir_renforce", "item_name": "Cuir renforcé", "item_type": "armor",
"quantity": 1, "weight": 4.5, "armor_rating": 2, "is_equipped": true}
],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 2, "hp_heal": 4},
{"item_slug": "jet", "item_name": "Jet", "item_type": "drug", "quantity": 2,
"properties": {"speed_bonus": 1, "addictive": true}},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 2,
"properties": {"irradiated": true, "rad_on_drink": 1}}
]
},
"lieutenant": {
"_desc": "Bras droit de faction — mieux équipé qu'un garde",
"weapons": [
{"item_slug": "fusil_chasse", "item_name": "Fusil de chasse", "item_type": "weapon_ranged",
"quantity": 1, "weight": 3.0, "damage_dice": 3, "damage_type": "physical",
"damage_range": "medium", "is_equipped": true},
{"item_slug": "pistolet_10mm", "item_name": "Pistolet 10mm", "item_type": "weapon_ranged",
"quantity": 1, "weight": 1.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "medium", "is_equipped": false}
],
"armor": [
{"item_slug": "cuir_renforce", "item_name": "Cuir renforcé", "item_type": "armor",
"quantity": 1, "weight": 4.5, "armor_rating": 2, "is_equipped": true}
],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 2, "hp_heal": 4},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_purifiee", "item_name": "Eau purifiée", "item_type": "drink", "quantity": 2}
]
},
"chasseur_primes": {
"_desc": "Mercenaire — équipé pour capturer ou tuer",
"weapons": [
{"item_slug": "carabine_308", "item_name": "Carabine .308", "item_type": "weapon_ranged",
"quantity": 1, "weight": 3.5, "damage_dice": 3, "damage_bonus": 1, "damage_type": "physical",
"damage_range": "long", "properties": {"piercing": 1}, "is_equipped": true},
{"item_slug": "couteau_combat", "item_name": "Couteau de combat", "item_type": "weapon_melee",
"quantity": 1, "weight": 0.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "short", "is_equipped": false}
],
"armor": [
{"item_slug": "cuir_renforce", "item_name": "Cuir renforcé", "item_type": "armor",
"quantity": 1, "weight": 4.5, "armor_rating": 2, "is_equipped": true}
],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 3, "hp_heal": 4},
{"item_slug": "mentats", "item_name": "Mentats", "item_type": "drug", "quantity": 1,
"properties": {"int_bonus": 2, "per_bonus": 1}},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_purifiee", "item_name": "Eau purifiée", "item_type": "drink", "quantity": 2}
]
},
"passeur": {
"_desc": "Guide / passeur entre zones — mobile, discret",
"weapons": [
{"item_slug": "couteau_combat", "item_name": "Couteau de combat", "item_type": "weapon_melee",
"quantity": 1, "weight": 0.5, "damage_dice": 2, "damage_type": "physical",
"damage_range": "short", "is_equipped": true}
],
"armor": [],
"consumables": [
{"item_slug": "stimpak", "item_name": "Stimpak", "item_type": "drug", "quantity": 1, "hp_heal": 4},
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 3, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink", "quantity": 3,
"properties": {"irradiated": true, "rad_on_drink": 1}}
]
},
"scavenger": {
"_desc": "Récupérateur de ferraille — outils, peu d'armes",
"weapons": [
{"item_slug": "baton", "item_name": "Bâton", "item_type": "weapon_melee",
"quantity": 1, "weight": 1.0, "damage_dice": 1, "damage_type": "physical",
"damage_range": "short", "is_equipped": true}
],
"armor": [],
"consumables": [
{"item_slug": "cram", "item_name": "Cram", "item_type": "food", "quantity": 2, "hp_heal": 2},
{"item_slug": "eau_sale", "item_name": "Eau sale", "item_type": "drink","quantity": 2,
"properties": {"irradiated": true, "rad_on_drink": 1}},
{"item_slug": "outils_reparation", "item_name": "Outils de répar.", "item_type": "misc", "quantity": 1},
{"item_slug": "ferraille", "item_name": "Ferraille", "item_type": "misc", "quantity": 5,
"weight": 2.0}
]
},
"chef_faction": {
"_desc": "Chef de faction — équipement narratif unique, hors sim",
"weapons": [],
"armor": [],
"consumables": []
}
}
+145
View File
@@ -0,0 +1,145 @@
-- =============================================================================
-- Migration 003 — Système de tiers PNJ + équipement boss + nouveaux rôles
--
-- Tier 0 : BOSS (chefs de faction, hors boucle sim, LLM 14B uniquement)
-- Tier 1 : ACTIFS SIM (agents permanents, LLM 7B + déterministe)
-- Tier 2 : PNJ+ (personnages nommés secondaires, LLM 7B light)
-- Tier 3 : PASSAGE (instanciés depuis archétypes, recyclables)
-- =============================================================================
-- 1. Nouvelles colonnes sur characters
ALTER TABLE characters
ADD COLUMN IF NOT EXISTS tier INTEGER DEFAULT 1,
ADD COLUMN IF NOT EXISTS archetype_key TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_characters_tier ON characters(session_id, tier);
-- 2. Les 5 boss existants passent en tier=0 et sont exclus de la sim
-- Ils n'ont pas de besoins biologiques dans la boucle (simulation narrative seule)
UPDATE characters
SET tier = 0,
needs_food = FALSE,
needs_water = FALSE,
needs_sleep = FALSE
WHERE session_id = 1
AND name IN ('Kingfish Robichaux','Maître Beaumont','Mme LaVeau',
'Commissaire Delacroix','Directeur Tran');
-- Assigner les rôles narratifs aux boss (pour la cohérence)
UPDATE characters SET role = 'chef_faction', origin = 'survivant'
WHERE session_id = 1 AND tier = 0;
-- 3. Équipements distinctifs pour les boss (matching leurs personnalités)
-- Kingfish Robichaux (Syndicat du Capitole) — gangster charismatique
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_bonus, damage_type, damage_range,
armor_rating, properties, is_equipped)
SELECT id, 1,
'revolver_44', '.44 Magnumblast', 'weapon_ranged',
1, 1.8, 3, 1, 'physical', 'medium', 0, '{"piercing": 1}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Kingfish Robichaux'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, armor_rating, properties, is_equipped)
SELECT id, 1,
'costume_patron', 'Costume du Patron', 'armor',
1, 2.0, 0, '{"intimidation_bonus": 2, "social": true}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Kingfish Robichaux'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
-- Maître Beaumont (Dynaste d'Oak) — aristocrate esclavagiste
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_type, damage_range, is_equipped)
SELECT id, 1,
'fouet_serviteur', 'Fouet du Maître', 'weapon_melee',
1, 0.8, 2, 'physical', 'short', TRUE
FROM characters WHERE session_id=1 AND name='Maître Beaumont'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_type, damage_range, is_equipped)
SELECT id, 1,
'pistolet_10mm', 'Pistolet 10mm', 'weapon_ranged',
1, 1.5, 2, 'physical', 'medium', FALSE
FROM characters WHERE session_id=1 AND name='Maître Beaumont'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
-- Mme LaVeau (Grand Krewe) — prêtresse vaudou des goules
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_bonus, damage_type, damage_range,
properties, is_equipped)
SELECT id, 1,
'baton_vaudou', 'Bâton Vaudou', 'weapon_melee',
1, 1.2, 1, 2, 'radiation', 'short',
'{"rad_on_hit": 2, "fear": true}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Mme LaVeau'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, rad_heal, properties)
SELECT id, 1,
'herbe_marais', 'Herbes des marais', 'drug',
3, 0.1, 2, '{"goule_only_heal": true}'::jsonb
FROM characters WHERE session_id=1 AND name='Mme LaVeau'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
-- Commissaire Delacroix (Union) — dirigeante militaire
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_type, damage_range, is_equipped)
SELECT id, 1,
'pistolet_10mm', 'Pistolet 10mm', 'weapon_ranged',
1, 1.5, 2, 'physical', 'medium', TRUE
FROM characters WHERE session_id=1 AND name='Commissaire Delacroix'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, armor_rating, properties, is_equipped)
SELECT id, 1,
'armure_union', 'Armure de l''Union', 'armor',
1, 5.0, 2, '{"faction": "union", "authority": true}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Commissaire Delacroix'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, hp_heal)
SELECT id, 1, 'stimpak', 'Stimpak', 'drug', 3, 0.2, 4
FROM characters WHERE session_id=1 AND name='Commissaire Delacroix'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
-- Directeur Tran (Consortium) — PDG technologue
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, damage_dice, damage_type, damage_range, properties, is_equipped)
SELECT id, 1,
'pistolet_laser', 'Pistolet Laser Mk.II', 'weapon_energy',
1, 1.0, 3, 'energy', 'medium',
'{"piercing_energy": 1, "consortium_tech": true}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Directeur Tran'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
INSERT INTO inventory
(character_id, session_id, item_slug, item_name, item_type,
quantity, weight, properties, is_equipped)
SELECT id, 1,
'pip_boy_mk3', 'Pip-Boy MkIII', 'misc',
1, 0.5, '{"data_device": true, "trade_bonus": 2}'::jsonb, TRUE
FROM characters WHERE session_id=1 AND name='Directeur Tran'
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING;
-- 4. Capsules des boss (pouvoir économique narratif)
UPDATE characters SET caps = 5000 WHERE session_id=1 AND name='Kingfish Robichaux';
UPDATE characters SET caps = 3000 WHERE session_id=1 AND name='Maître Beaumont';
UPDATE characters SET caps = 1500 WHERE session_id=1 AND name='Mme LaVeau';
UPDATE characters SET caps = 4000 WHERE session_id=1 AND name='Commissaire Delacroix';
UPDATE characters SET caps = 8000 WHERE session_id=1 AND name='Directeur Tran';
+50 -9
View File
@@ -67,6 +67,36 @@ _ROLES: dict[str, dict] = {
"faction_pool": ["ecumeurs", None], "faction_pool": ["ecumeurs", None],
"caps": (100, 1000), "caps": (100, 1000),
}, },
"lieutenant": {
"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)},
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
"caps": (200, 800),
},
"chasseur_primes": {
"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)},
"faction_pool": [None, "syndicat_capitole"],
"caps": (200, 800),
},
"passeur": {
"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)},
"faction_pool": [None, "ecumeurs"],
"caps": (100, 600),
},
"scavenger": {
"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)},
"faction_pool": [None],
"caps": (20, 200),
},
"chef_faction": {
"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)},
"faction_pool": ["union", "cda", "syndicat_capitole", "regie", "dynaste_oak", "ecumeurs", "consortium", "grand_krewe"],
"caps": (1000, 8000),
},
"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)},
@@ -166,13 +196,18 @@ ORIGINS: dict[str, dict] = {
# Mapping rôle → origine probable (pour génération automatique PNJ) # Mapping rôle → origine probable (pour génération automatique PNJ)
_ROLE_TO_ORIGIN: dict[str, list[str]] = { _ROLE_TO_ORIGIN: dict[str, list[str]] = {
"pillard": ["survivant"], "pillard": ["survivant"],
"marchand": ["survivant", "habitant_abri"], "marchand": ["survivant", "habitant_abri"],
"garde": ["survivant", "habitant_abri", "initie_confrerie"], "garde": ["survivant", "habitant_abri", "initie_confrerie"],
"eclaireur": ["survivant", "goule"], "eclaireur": ["survivant", "goule"],
"docteur": ["survivant", "habitant_abri"], "docteur": ["survivant", "habitant_abri"],
"technicien": ["initie_confrerie", "habitant_abri", "survivant"], "technicien": ["initie_confrerie", "habitant_abri", "survivant"],
"raider_boss": ["survivant", "super_mutant"], "raider_boss": ["survivant", "super_mutant"],
"lieutenant": ["survivant", "habitant_abri"],
"chasseur_primes": ["survivant"],
"passeur": ["survivant", "goule"],
"scavenger": ["survivant", "goule"],
"chef_faction": ["survivant", "habitant_abri"],
"survivant_generique": ["survivant", "habitant_abri"], "survivant_generique": ["survivant", "habitant_abri"],
} }
@@ -250,6 +285,8 @@ def generate_character(
seed: int | None = None, seed: int | None = None,
caps: int | None = None, caps: int | None = None,
save_to_db: bool = True, save_to_db: bool = True,
tier: int = 1,
archetype_key: str | None = None,
) -> dict: ) -> dict:
""" """
Génère un personnage complet. Génère un personnage complet.
@@ -329,6 +366,8 @@ def generate_character(
"rads": 0, "rads": 0,
"is_alive": True, "is_alive": True,
"is_in_shelter": False, "is_in_shelter": False,
"tier": tier,
"archetype_key": archetype_key,
} }
if save_to_db: if save_to_db:
@@ -348,13 +387,15 @@ def _insert_character(char: dict) -> int:
strength, perception, endurance, charisma, intelligence, agility, luck, strength, perception, endurance, charisma, intelligence, agility, luck,
hp, max_hp, caps, rads, is_alive, is_in_shelter, hp, max_hp, caps, rads, is_alive, is_in_shelter,
role, origin, needs_food, needs_water, needs_sleep, role, origin, needs_food, needs_water, needs_sleep,
immune_radiation, immune_poison immune_radiation, immune_poison,
tier, archetype_key
) VALUES ( ) VALUES (
%(session_id)s, %(name)s, %(character_type)s, %(faction_slug)s, %(location_slug)s, %(seed_base)s, %(session_id)s, %(name)s, %(character_type)s, %(faction_slug)s, %(location_slug)s, %(seed_base)s,
%(strength)s, %(perception)s, %(endurance)s, %(charisma)s, %(intelligence)s, %(agility)s, %(luck)s, %(strength)s, %(perception)s, %(endurance)s, %(charisma)s, %(intelligence)s, %(agility)s, %(luck)s,
%(hp)s, %(max_hp)s, %(caps)s, %(rads)s, %(is_alive)s, %(is_in_shelter)s, %(hp)s, %(max_hp)s, %(caps)s, %(rads)s, %(is_alive)s, %(is_in_shelter)s,
%(role)s, %(origin)s, %(needs_food)s, %(needs_water)s, %(needs_sleep)s, %(role)s, %(origin)s, %(needs_food)s, %(needs_water)s, %(needs_sleep)s,
%(immune_radiation)s, %(immune_poison)s %(immune_radiation)s, %(immune_poison)s,
%(tier)s, %(archetype_key)s
) RETURNING id ) RETURNING id
""", """,
char, char,
+12 -1
View File
@@ -43,7 +43,18 @@ def update_session_tick(session_id: int, day: int, tick: int):
# --- Personnages --- # --- Personnages ---
def get_characters(session_id: int) -> list[dict]: def get_characters(session_id: int, min_tier: int = 1) -> list[dict]:
"""Retourne les PNJ vivants. min_tier=1 exclut les boss (tier=0) par défaut."""
with cursor() as cur:
cur.execute(
"SELECT * FROM characters WHERE session_id=%s AND is_alive=TRUE AND COALESCE(tier,1) >= %s",
(session_id, min_tier)
)
return [dict(r) for r in cur.fetchall()]
def get_all_characters(session_id: int) -> list[dict]:
"""Retourne tous les PNJ vivants tous tiers confondus (y compris boss)."""
with cursor() as cur: with cursor() as cur:
cur.execute( cur.execute(
"SELECT * FROM characters WHERE session_id=%s AND is_alive=TRUE", "SELECT * FROM characters WHERE session_id=%s AND is_alive=TRUE",
+88 -30
View File
@@ -1,5 +1,6 @@
""" """
Script one-shot : crée 10 PNJ de test en DB pour la pré-sim 3 jours pacifique. Script one-shot : crée les PNJ tier 1 (actifs sim) et tier 2 (PNJ+ nommés).
Les boss tier 0 sont créés via fixtures.sql + migration_003.
Lance avec : python init_pnj.py Lance avec : python init_pnj.py
""" """
@@ -7,48 +8,105 @@ import sys, os
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
import character_creator as cc import character_creator as cc
import pnj_factory as factory
import db import db
SESSION_ID = 1 SESSION_ID = 1
PNJ_SPECS = [ # ---------------------------------------------------------------------------
# Tier 1 — ACTIFS SIM (15 PNJ, boucle survival + actions complète)
# ---------------------------------------------------------------------------
TIER1_SPECS = [
# (role, origin, name, faction, location) # (role, origin, name, faction, location)
("garde", "survivant", "Thibodaux 'Gator'", "union", "nola_cbd"), ("garde", "survivant", "Thibodaux 'Gator'", "union", "nola_cbd"),
("garde", "initie_confrerie","Fontenot 'Ironhide'", "cda", "nola_cbd"), ("garde", "initie_confrerie","Fontenot 'Ironhide'", "cda", "nola_cbd"),
("marchand", "habitant_abri", "LaFleur 'Vieux'", "consortium", "nola_vieux_carre"), ("marchand", "habitant_abri", "LaFleur 'Vieux'", "consortium", "nola_vieux_carre"),
("marchand", "survivant", "Broussard 'Fil-de-Fer'", "regie", "nola_vieux_carre"), ("marchand", "survivant", "Broussard 'Fil-de-Fer'", "regie", "nola_vieux_carre"),
("eclaireur", "goule", "Mouton 'Marécage'", None, "pearl_river"), ("eclaireur", "goule", "Mouton 'Marécage'", None, "pearl_river"),
("eclaireur", "survivant", "Trahan 'Mudcat'", "union", "pearl_river"), ("eclaireur", "survivant", "Trahan 'Mudcat'", "union", "pearl_river"),
("docteur", "habitant_abri", "Hébert 'Tripette'", "union", "nola_cbd"), ("docteur", "habitant_abri", "Hébert 'Tripette'", "union", "nola_cbd"),
("technicien", "initie_confrerie","Landry 'Rouille'", "consortium", "baton_rouge"), ("technicien", "initie_confrerie","Landry 'Rouille'", "consortium", "baton_rouge"),
("pillard", "survivant", "Arceneaux 'Crapaud'", "ecumeurs", "pearl_river"), ("pillard", "survivant", "Arceneaux 'Crapaud'", "ecumeurs", "pearl_river"),
("survivant_generique","survivant", "Guidry 'Gris-Gris'", None, "nola_vieux_carre"), ("survivant_generique","survivant", "Guidry 'Gris-Gris'", None, "nola_vieux_carre"),
] ]
# ---------------------------------------------------------------------------
# Tier 2 — PNJ+ (personnages nommés secondaires, 1-2 par faction majeure)
# ---------------------------------------------------------------------------
TIER2_SPECS = [
# (archetype, name, faction, location, seed)
("lieutenant_faction", "Jean-Baptiste 'Fer' Caillouet", "union", "independance", 2001),
("lieutenant_faction", "Ramon 'Cobra' Villaverde", "syndicat_capitole", "baton_rouge", 2002),
("lieutenant_faction", "Lila 'Plomb' Moreau", "cda", "nola_fleuve", 2003),
("lieutenant_faction", "Zuzu 'Os-Blanc' Delacourt", "grand_krewe", "la_paroisse", 2004),
("lieutenant_faction", "Thérèse 'Circuit' Nguyen", "consortium", "laplace", 2005),
("lieutenant_faction", "Edmond 'Sèche' Bautista", "regie", "nola_vieux_carre",2006),
("lieutenant_faction", "Calhoun 'Fouet' Arceneaux", "dynaste_oak", "oak_plantation", 2007),
("lieutenant_faction", "Roscoe 'Requiem' Fontenot", "ecumeurs", "pearl_river", 2008),
]
def equip_tier1(char_id: int, role: str):
"""Équipe un PNJ tier 1 selon son rôle via pnj_factory."""
items = factory.equip_character(char_id, role, SESSION_ID)
return items
def main(): def main():
print(f"=== Création de {len(PNJ_SPECS)} PNJ — Session {SESSION_ID} ===\n") print(f"=== Initialisation PNJ — Session {SESSION_ID} ===\n")
created = []
for i, (role, origin, name, faction, location) in enumerate(PNJ_SPECS, 1): # --- Tier 1 ---
print(f"--- Tier 1 : {len(TIER1_SPECS)} PNJ actifs ---\n")
tier1_created = []
for i, (role, origin, name, faction, location) in enumerate(TIER1_SPECS, 1):
char = cc.generate_character( char = cc.generate_character(
session_id=SESSION_ID, session_id = SESSION_ID,
role=role, role = role,
origin=origin, origin = origin,
name=name, name = name,
faction_slug=faction, faction_slug = faction,
location_slug=location, location_slug = location,
character_type="pnj_sim", character_type = "pnj_sim",
seed=1000 + i, seed = 1000 + i,
save_to_db=True, save_to_db = True,
tier = 1,
) )
created.append(char) items = equip_tier1(char["id"], role)
tier1_created.append(char)
print(cc.format_sheet(char)) print(cc.format_sheet(char))
print() print(f" Équipement : {', '.join(items) or ''}\n")
print(f"{len(created)} PNJ insérés en DB (session {SESSION_ID})") # --- Tier 2 ---
print(f"\n--- Tier 2 : {len(TIER2_SPECS)} PNJ+ nommés ---\n")
tier2_created = []
for archetype, name, faction, location, seed in TIER2_SPECS:
char = factory.spawn_from_archetype(
archetype_key = archetype,
session_id = SESSION_ID,
name = name,
faction_slug = faction,
location_slug = location,
seed = seed,
tier_override = 2,
)
tier2_created.append(char)
print(f" [TIER2] {name:<40} {faction or 'sans faction':<20} @ {location}")
print(f" Équipement : {', '.join(char.get('items_equipped', []))}\n")
# --- Résumé ---
total = len(tier1_created) + len(tier2_created)
all_chars = db.get_characters(SESSION_ID)
print(f"\n{total} PNJ créés ({len(tier1_created)} tier1 + {len(tier2_created)} tier2)")
print(f" DB total (all tiers) : {len(all_chars)} PNJ vivants en session {SESSION_ID}")
by_tier = {}
for c in all_chars:
t = c.get("tier", 1)
by_tier[t] = by_tier.get(t, 0) + 1
for t in sorted(by_tier):
labels = {0: "BOSS", 1: "ACTIFS SIM", 2: "PNJ+", 3: "PASSAGE"}
print(f" Tier {t} ({labels.get(t,'?')}) : {by_tier[t]} PNJ")
# Vérification rapide
chars = db.get_characters(SESSION_ID)
print(f" DB confirme : {len(chars)} PNJ vivants en session {SESSION_ID}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+188
View File
@@ -0,0 +1,188 @@
"""
PNJ Factory — Instancie des archétypes de la bibliothèque en PNJ réels en DB.
Deux modes :
spawn_from_archetype(key, ...) — crée un PNJ tier 3 depuis la bibliothèque
equip_character(char_id, role, session_id) — équipe un PNJ existant selon son rôle
"""
import json
import os
import random
import db
import character_creator as cc
_DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
def _load_archetypes() -> dict:
path = os.path.join(_DATA_DIR, "archetypes.json")
with open(path, encoding="utf-8") as f:
data = json.load(f)
return {k: v for k, v in data.items() if not k.startswith("_")}
def _load_equipment() -> dict:
path = os.path.join(_DATA_DIR, "equipment_by_role.json")
with open(path, encoding="utf-8") as f:
data = json.load(f)
return {k: v for k, v in data.items() if not k.startswith("_")}
def equip_character(character_id: int, role: str, session_id: int, seed: int | None = None) -> list[str]:
"""
Insère l'équipement standard du rôle en DB pour un personnage.
Retourne la liste des item_slug insérés.
"""
equipment = _load_equipment()
role_equip = equipment.get(role, equipment["survivant_generique"])
rng = random.Random(seed)
inserted = []
all_items = (
role_equip.get("weapons", [])
+ role_equip.get("armor", [])
+ role_equip.get("consumables", [])
+ role_equip.get("stock", [])
)
for item in all_items:
slug = item["item_slug"]
name = item["item_name"]
itype = item["item_type"]
qty = item.get("quantity", 1)
weight = item.get("weight", 0.0)
hp_heal = item.get("hp_heal", 0)
rad_heal = item.get("rad_heal", 0)
dmg_dice = item.get("damage_dice", 0)
dmg_bonus = item.get("damage_bonus", 0)
dmg_type = item.get("damage_type")
dmg_range = item.get("damage_range")
armor_r = item.get("armor_rating", 0)
props = json.dumps(item.get("properties", {}))
equipped = item.get("is_equipped", False)
with db.cursor() as cur:
cur.execute(
"""
INSERT INTO inventory (
character_id, session_id, item_slug, item_name, item_type,
quantity, weight, hp_heal, rad_heal,
damage_dice, damage_bonus, damage_type, damage_range,
armor_rating, properties, is_equipped
) VALUES (
%s, %s, %s, %s, %s,
%s, %s, %s, %s,
%s, %s, %s, %s,
%s, %s::jsonb, %s
)
ON CONFLICT (character_id, session_id, item_slug) DO NOTHING
""",
(
character_id, session_id, slug, name, itype,
qty, weight, hp_heal, rad_heal,
dmg_dice, dmg_bonus, dmg_type, dmg_range,
armor_r, props, equipped
)
)
if cur.rowcount:
inserted.append(slug)
return inserted
def spawn_from_archetype(
archetype_key: str,
session_id: int,
name: str | None = None,
faction_slug: str | None = None,
location_slug: str | None = None,
seed: int | None = None,
tier_override: int | None = None,
) -> dict:
"""
Instancie un archétype en PNJ réel dans la DB.
Args:
archetype_key : clé dans archetypes.json (ex: 'vendeur_ambulant')
session_id : ID de session
name : nom custom (généré si None)
faction_slug : faction (tirée de l'archétype si None)
location_slug : localisation de départ
seed : graine reproductible
tier_override : force un tier (ex: 2 pour un lieutenant nommé)
Returns:
dict du personnage créé (avec id DB)
"""
archetypes = _load_archetypes()
if archetype_key not in archetypes:
raise ValueError(f"Archétype inconnu: {archetype_key}. Disponibles: {list(archetypes)}")
arch = archetypes[archetype_key]
rng = random.Random(seed)
role = arch["role"]
origin = arch.get("origin", "survivant")
tier = tier_override if tier_override is not None else arch.get("tier", 3)
if faction_slug is None:
pool = [f for f in arch.get("faction_pool", [None])]
faction_slug = rng.choice(pool)
char = cc.generate_character(
session_id = session_id,
role = role,
origin = origin,
character_type = "pnj_sim",
name = name,
faction_slug = faction_slug,
location_slug = location_slug,
seed = seed,
save_to_db = True,
tier = tier,
archetype_key = archetype_key,
)
equip_role = arch.get("equipment_role", role)
items = equip_character(char["id"], equip_role, session_id, seed=seed)
char["items_equipped"] = items
return char
def list_archetypes() -> list[dict]:
"""Retourne la liste des archétypes disponibles (pour le LLM)."""
archetypes = _load_archetypes()
return [
{
"key": key,
"role": data["role"],
"tier": data.get("tier", 3),
"description": data["description"],
"faction_pool": data.get("faction_pool", []),
}
for key, data in archetypes.items()
]
def spawn_batch(specs: list[dict], session_id: int) -> list[dict]:
"""
Instancie plusieurs PNJ depuis une liste de specs.
specs: [{"archetype": "vendeur_ambulant", "name": "Jean", "faction": "consortium",
"location": "laplace", "seed": 42}, ...]
"""
created = []
for spec in specs:
char = spawn_from_archetype(
archetype_key = spec["archetype"],
session_id = session_id,
name = spec.get("name"),
faction_slug = spec.get("faction"),
location_slug = spec.get("location"),
seed = spec.get("seed"),
tier_override = spec.get("tier"),
)
created.append(char)
return created
+5 -4
View File
@@ -41,7 +41,8 @@ def process_tick(session_id: int, day: int, tick: int, mode: str):
- Logue le tick - Logue le tick
""" """
phase = get_phase(tick) phase = get_phase(tick)
characters = db.get_characters(session_id) characters = db.get_characters(session_id) # tier 1-3 : boucle survival/actions
all_characters = db.get_all_characters(session_id) # tous tiers : recalcul seeds
world_states = db.get_world_state(session_id, day) world_states = db.get_world_state(session_id, day)
world_map = {ws["location_slug"]: ws for ws in world_states} world_map = {ws["location_slug"]: ws for ws in world_states}
@@ -60,8 +61,8 @@ def process_tick(session_id: int, day: int, tick: int, mode: str):
pop_count=ws.get("pop_count", 0) pop_count=ws.get("pop_count", 0)
) )
# -- Recalcul seeds PNJ ---------------------------------------------- # -- Recalcul seeds PNJ (tous tiers, y compris boss tier 0) ---------
for char in characters: for char in all_characters:
prev_state = db.get_pnj_state(session_id, char["id"], day) prev_state = db.get_pnj_state(session_id, char["id"], day)
stress = prev_state["stress_level"] if prev_state else 0 stress = prev_state["stress_level"] if prev_state else 0
new_seed = recalculate_seed(char["seed_base"], day, tick, stress) new_seed = recalculate_seed(char["seed_base"], day, tick, stress)
@@ -90,7 +91,7 @@ def process_tick(session_id: int, day: int, tick: int, mode: str):
mechanical_effect={"phase": phase, "mode": mode} mechanical_effect={"phase": phase, "mode": mode}
) )
print(f" [J{day:03d} T{tick:02d}h] {phase.upper():5s} | {len(characters)} PNJ actifs") print(f" [J{day:03d} T{tick:02d}h] {phase.upper():5s} | {len(characters)} PNJ en sim (tier 1-3)")
def _compute_location_tick_delta(ws: dict, phase: str, mode: str) -> dict: def _compute_location_tick_delta(ws: dict, phase: str, mode: str) -> dict: