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:
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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)},
|
||||||
@@ -173,6 +203,11 @@ _ROLE_TO_ORIGIN: dict[str, list[str]] = {
|
|||||||
"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
@@ -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",
|
||||||
|
|||||||
+78
-20
@@ -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,11 +8,15 @@ 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"),
|
||||||
@@ -25,30 +30,83 @@ PNJ_SPECS = [
|
|||||||
("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()
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user