Files
uberwald 8123b53f75
Release Creation / build (release) Successful in 1m21s
Import de personnages du précédent système
2026-04-27 22:29:49 +02:00

369 lines
13 KiB
Python

#!/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 <fichier.json> [fichier2.json ...]
Le fichier converti est écrit à côté du fichier source sous le nom :
<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.<clé>'."""
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 <base>-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 <fichier.json> [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()