This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user