369 lines
13 KiB
Python
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()
|