#!/usr/bin/env python3 """ convert-old-system.py Convertit les fichiers JSON d'acteurs exportés de l'ancien système Célestopol 1922 (system id: celestopol1922) vers le format du nouveau système (fvtt-celestopol). Usage: python3 convert-old-system.py [fichier2.json ...] Le fichier converti est écrit à côté du fichier source sous le nom : -converted.json """ import json import os import re import sys import uuid # ─── Mapping des types d'anomalies (préfixe "CEL1922.opt." → clé nouveau système) ───── ANOMALY_TYPE_MAP = { "none": "none", "entropie": "entropie", "communicationaveclesmorts": "communicationaveclesmorts", "illusion": "illusion", "suggestion": "suggestion", "tarotdivinatoire": "tarotdivinatoire", "telekinesie": "telekinesie", "telepathie": "telepathie", "voyageastral": "voyageastral", } # Clé anomaly vide → "none" VALID_ANOMALY_TYPES = set(ANOMALY_TYPE_MAP.values()) # ─── Domaines (stats) ────────────────────────────────────────────────────────────────── STATS = ["ame", "corps", "coeur", "esprit"] # Compétences par domaine (dans l'ordre du nouveau système) SKILLS = { "ame": ["artifice", "attraction", "coercition", "faveur"], "corps": ["echauffouree", "effacement", "mobilite", "prouesse"], "coeur": ["appreciation", "arts", "inspiration", "traque"], "esprit": ["instruction", "mtechnologique", "raisonnement", "traitement"], } def make_id(): """Génère un identifiant Foundry-compatible (16 chars hex).""" return uuid.uuid4().hex[:16] def strip_prefix(label: str) -> str: """Extrait la clé depuis 'CEL1922.opt.'.""" return label.split(".")[-1] def resolve_anomaly_type(old_system: dict) -> str: """ Résout le type d'anomalie depuis l'ancien système. old_system.anomaly est un int (index dans anomalytypes[]). """ anomalytypes = old_system.get("skill", {}).get("anomalytypes", []) idx = old_system.get("anomaly", 0) try: idx = int(idx) except (ValueError, TypeError): idx = 0 if idx == 0 or not anomalytypes: return "none" if idx < len(anomalytypes): raw = strip_prefix(anomalytypes[idx]) return ANOMALY_TYPE_MAP.get(raw, "none") return "none" def resolve_anomaly_type_from_name(item_name: str) -> str: """ Infère le type d'anomalie depuis le nom de l'item (ex: 'Entropie 1' → 'entropie'). Fallback si le mapping par index est insuffisant. """ # Normalise : minuscules, retire accents, retire espaces et chiffres name_clean = item_name.lower().strip() name_clean = re.sub(r"\s*\d+\s*$", "", name_clean).strip() # retire le niveau en fin name_clean = re.sub(r"\s+", "", name_clean) # retire tous les espaces # Normalisation des accents courants replacements = [ ("é", "e"), ("è", "e"), ("ê", "e"), ("ë", "e"), ("à", "a"), ("â", "a"), ("ä", "a"), ("î", "i"), ("ï", "i"), ("ô", "o"), ("ö", "o"), ("û", "u"), ("ù", "u"), ("ü", "u"), ("ç", "c"), ] for src, dst in replacements: name_clean = name_clean.replace(src, dst) for key in ANOMALY_TYPE_MAP: if key == "none": continue if key in name_clean or name_clean in key: return key return "none" def convert_stats_character(old_skill: dict, warnings: list) -> dict: """Convertit system.skill (ancien) → system.stats (nouveau) pour un PJ.""" stats = {} for stat in STATS: old_stat = old_skill.get(stat, {}) new_stat = { "label": stat, "res": int(old_stat.get("res", 0) or 0), } for skill in SKILLS[stat]: old_sk = old_stat.get(skill, {}) val = old_sk.get("value", 0) try: val = int(val) if val is not None else 0 except (ValueError, TypeError): warnings.append(f"Valeur de compétence invalide pour {stat}.{skill}: {val!r} → 0") val = 0 new_stat[skill] = {"label": skill, "value": val} stats[stat] = new_stat return stats def convert_stats_npc(old_skill: dict, warnings: list) -> dict: """Convertit system.skill (ancien) → system.stats (nouveau) pour un PNJ.""" stats = {} for stat in STATS: old_stat = old_skill.get(stat, {}) res = int(old_stat.get("res", 0) or 0) stats[stat] = { "label": stat, "res": res, "actuel": res, # actuel = res par défaut } return stats def convert_attributs(old_attributs: dict) -> dict: """Convertit les attributs du vieux format plat vers SchemaField {value, max}.""" return { "entregent": { "value": int(old_attributs.get("entregent", 0) or 0), "max": int(old_attributs.get("entregentmax", 0) or 0), }, "fortune": { "value": int(old_attributs.get("fortune", 0) or 0), "max": int(old_attributs.get("fortunemax", 0) or 0), }, "reve": { "value": int(old_attributs.get("reve", 0) or 0), "max": int(old_attributs.get("revemax", 0) or 0), }, "vision": { "value": int(old_attributs.get("vision", 0) or 0), "max": int(old_attributs.get("visionmax", 0) or 0), }, } def convert_item(item: dict, warnings: list) -> dict | None: """ Convertit un item de l'ancien format vers le nouveau. Retourne None si l'item doit être ignoré (type inconnu sans données utiles). """ old_type = item.get("type", "") old_sys = item.get("system", {}) name = item.get("name", "Sans nom") new_item = { "_id": make_id(), "name": name, "img": item.get("img", "icons/svg/item-bag.svg"), "effects": [], "flags": {}, "sort": 0, } if old_type == "anomaly": level = 0 try: level = int(old_sys.get("value", 1) or 1) except (ValueError, TypeError): level = 1 level = max(1, min(4, level)) # Résolution du subtype : l'ancien système le stocke mal ("weapon"), on l'infère du nom subtype = resolve_anomaly_type_from_name(name) if subtype == "none": warnings.append(f"Impossible de déterminer le type d'anomalie depuis '{name}', 'none' utilisé") new_item["type"] = "anomaly" new_item["system"] = { "subtype": subtype, "level": level, "usesRemaining": level, "technique": old_sys.get("technique", "") or "", "narratif": old_sys.get("narratif", "") or "", } elif old_type == "aspect": valeur = 0 try: valeur = int(old_sys.get("value", 0) or 0) except (ValueError, TypeError): valeur = 0 new_item["type"] = "aspect" new_item["system"] = { "valeur": valeur, "description": old_sys.get("narratif", "") or old_sys.get("technique", "") or "", } elif old_type == "item": old_subtype = (old_sys.get("subtype") or "").lower().strip() damage = old_sys.get("damage", "") or "" protect = old_sys.get("protection", "") or "" if old_subtype == "weapon" or (damage and not protect): new_item["type"] = "weapon" new_item["system"] = { "type": "melee", "degats": "0", "portee": "contact", "equipped": False, "description": old_sys.get("technique", "") or old_sys.get("narratif", "") or "", } if damage: warnings.append(f"Item '{name}' : valeur damage='{damage}' non convertie automatiquement, à saisir manuellement") elif old_subtype == "armor" or (protect and not damage): new_item["type"] = "armure" new_item["system"] = { "protection": 1, "malus": 1, "equipped": False, "description": old_sys.get("technique", "") or old_sys.get("narratif", "") or "", } if protect: warnings.append(f"Item '{name}' : valeur protection='{protect}' non convertie automatiquement, à saisir manuellement") else: # Équipement générique new_item["type"] = "equipment" new_item["system"] = { "description": old_sys.get("technique", "") or old_sys.get("narratif", "") or "", } else: warnings.append(f"Type d'item inconnu '{old_type}' pour '{name}', ignoré") return None return new_item def convert_actor(old: dict) -> tuple[dict, list[str]]: """Convertit un acteur complet. Retourne (new_actor, warnings).""" warnings = [] actor_type = old.get("type", "character") old_sys = old.get("system", {}) old_skill = old_sys.get("skill", {}) new_sys = {} # ── Champs communs ────────────────────────────────────────────────────────────── new_sys["concept"] = old_sys.get("concept", "") or "" new_sys["description"] = old_sys.get("description", "") or "" # gardé dans metier/concept new_sys["metier"] = old_sys.get("metier", "") or old_sys.get("concept", "") or "" new_sys["faction"] = old_sys.get("faction", "") or "" # ── Blessures / Destin / Spleen ───────────────────────────────────────────────── new_sys["blessures"] = {"lvl": int(old_sys.get("blessures", {}).get("lvl", 0) or 0)} new_sys["destin"] = {"lvl": int(old_sys.get("destin", {}).get("lvl", 0) or 0)} new_sys["spleen"] = {"lvl": int(old_sys.get("spleen", {}).get("lvl", 0) or 0)} # ── Stats ─────────────────────────────────────────────────────────────────────── if actor_type == "character": new_sys["stats"] = convert_stats_character(old_skill, warnings) # Anomalie : type depuis l'index, niveau depuis anomalyval anomaly_type = resolve_anomaly_type(old_sys) anomaly_val = int(old_sys.get("anomalyval", 0) or 0) new_sys["anomaly"] = {"type": anomaly_type, "value": anomaly_val} # Attributs new_sys["attributs"] = convert_attributs(old_sys.get("attributs", {})) # Biographie → historique new_sys["historique"] = old_sys.get("description", "") or "" new_sys["descriptionPhysique"] = "" new_sys["descriptionPsychologique"] = "" new_sys["initiative"] = 0 # Factions (vide) new_sys["factions"] = old_sys.get("factions", {}) elif actor_type == "npc": new_sys["stats"] = convert_stats_npc(old_skill, warnings) new_sys["npcType"] = old_sys.get("npcType", "standard") or "standard" new_sys["historique"] = old_sys.get("description", "") or "" # ── Items ─────────────────────────────────────────────────────────────────────── new_items = [] for item in old.get("items", []): converted = convert_item(item, warnings) if converted: new_items.append(converted) new_actor = { "_id": make_id(), "name": old.get("name", "Personnage sans nom"), "type": actor_type, "img": old.get("img", "icons/svg/mystery-man.svg"), "system": new_sys, "items": new_items, "effects": [], "folder": None, "flags": {}, "prototypeToken": old.get("prototypeToken", {}), } return new_actor, warnings def convert_file(path: str) -> None: """Lit path, convertit, écrit -converted.json.""" print(f"\n{'─' * 60}") print(f"Traitement : {path}") with open(path, "r", encoding="utf-8") as f: old = json.load(f) new_actor, warnings = convert_actor(old) for w in warnings: print(f" ⚠ {w}") base, _ = os.path.splitext(path) out_path = f"{base}-converted.json" with open(out_path, "w", encoding="utf-8") as f: json.dump(new_actor, f, ensure_ascii=False, indent=2) print(f" ✓ Écrit : {out_path}") print(f" type={new_actor['type']} items={len(new_actor['items'])} avertissements={len(warnings)}") def main(): if len(sys.argv) < 2: print("Usage: python3 convert-old-system.py [fichier2.json ...]") sys.exit(1) for path in sys.argv[1:]: if not os.path.isfile(path): print(f"Fichier introuvable : {path}", file=sys.stderr) continue convert_file(path) print(f"\n{'─' * 60}") print("Conversion terminée.") if __name__ == "__main__": main()