import json # Éditeur de collision (nouvel onglet dédié aux scènes 2D, voir # templates/scene_edit.html) : chaque objet de scène (hors le personnage # "joueur" — voir screens/rendering/personnage_role.py — et un "fond", # jamais une cible/source de collision) porte une LISTE de règles # "déclencheur -> action", construites carte par carte dans l'éditeur. # # Déclencheur : "collision" (chevauchement direct, voir # static/js/play/conditions.js::forgeShapesOverlap) ou "perimetre" (le # joueur entre dans un rayon de `perimetre_px` pixels autour de l'objet). # # Action (une seule par règle) : # - "quete" : quete_id (id technique d'une quête — voir db/quests/, # choisie/créée via l'assistant "+ Action" qui ouvre # directement l'éditeur de quête, static/js/quests/ # quest-editor.js) — une quête inexistante/supprimée # depuis reste une référence orpheline plutôt qu'une # contrainte SQL, comme le reste de ce moteur (voir # evenement_id ci-dessous, même principe). # - "attaque" : joue la pose "attack" du personnage si disponible (voir # personnage-controller.js) — pas de système de combat # (dégâts/PV...) pour l'instant, effet visuel seul. # - "evenement" : evenement_id — réutilise TEL QUEL le système # d'événements personnalisés existant (db.list_custom_events, # window.dispatchGameEvent), la logique de flow d'un écran # document peut donc déjà écouter ce déclenchement. # - "interagir" : affiche "Appuie sur [touche]" (la touche "interagir" du # joueur, voir personnage_commands.py) tant que la # collision/le périmètre est actif, puis exécute # `sub_action` (même forme qu'une action, SAUF # "interagir" à nouveau — UN seul niveau d'imbrication, # pas une récursion illimitée) à l'appui de la touche. ACTION_TYPES = ["quete", "attaque", "evenement", "interagir"] TRIGGER_TYPES = ["collision", "perimetre"] DEFAULT_PERIMETRE_PX = 5 # Profondeur max d'imbrication ("interagir" -> sous-action) — évite une # structure sans fin construite par un JSON posté à la main. _MAX_ACTION_DEPTH = 2 def _sanitize_action(raw_action, depth=1): if not isinstance(raw_action, dict): return None action_type = raw_action.get("type") if action_type not in ACTION_TYPES: return None if action_type == "interagir" and depth >= _MAX_ACTION_DEPTH: return None # un seul niveau d'imbrication autorisé action = {"type": action_type} if action_type == "quete": try: action["quete_id"] = int(raw_action.get("quete_id")) except (TypeError, ValueError): return None elif action_type == "evenement": try: action["evenement_id"] = int(raw_action.get("evenement_id")) except (TypeError, ValueError): return None elif action_type == "interagir": sub_action = _sanitize_action(raw_action.get("sub_action"), depth=depth + 1) if not sub_action: return None action["sub_action"] = sub_action # "attaque" : aucun champ supplémentaire. return action def sanitize_collision_rules(raw_rules): """Valide/nettoie une liste de règles postée par le client (voir routes/scenes/scene_object_collision_rules.py) — une règle invalide (déclencheur/action inconnus, imbrication trop profonde...) est IGNORÉE plutôt que de faire planter la sauvegarde entière.""" if not isinstance(raw_rules, list): return [] cleaned = [] for raw_rule in raw_rules[:50]: # marge large, jamais illimité if not isinstance(raw_rule, dict): continue trigger = raw_rule.get("trigger") if trigger not in TRIGGER_TYPES: continue action = _sanitize_action(raw_rule.get("action")) if not action: continue rule = {"trigger": trigger, "action": action} if trigger == "perimetre": try: rule["perimetre_px"] = max(1, float(raw_rule.get("perimetre_px", DEFAULT_PERIMETRE_PX))) except (TypeError, ValueError): rule["perimetre_px"] = DEFAULT_PERIMETRE_PX cleaned.append(rule) return cleaned def resolve_collision_rules(obj): """Décode attributes._collision_rules en liste Python, jamais None — un attribut absent/corrompu retombe sur aucune règle (comportement inchangé pour tout objet déjà posé avant l'ajout de cet éditeur).""" raw = (obj.get("attributes") or {}).get("_collision_rules") if not raw: return [] try: parsed = json.loads(raw) except (TypeError, ValueError): return [] return sanitize_collision_rules(parsed)