Nouvel espace "🗺️ Quêtes" (game-wide, comme les événements personnalisés) : liste de cartes + "+ Nouvelle quête", chaque carte ouvrant une modale d'arbre de dialogue en bulles de théâtre ("speaker: texte"), une colonne par statut de quête (nouvelle/en cours/terminée) puisque le dialogue joué dépend de l'état de la quête au moment où le joueur parle au PNJ. Bulles alternées joueur/PNJ, couleur différente selon qui parle. L'action "Déclencher une quête" de l'éditeur de collision n'est plus un champ texte libre : elle ouvre directement CETTE MÊME modale (picker de quêtes existantes + création à la volée), et finalise la règle avec l'id numérique de la quête choisie — collision_rules.py migré en conséquence (quete_id devient un entier, comme evenement_id). Corrige aussi la miniature d'objet de la carte "🧩 Collision", 3× plus grande (120px) comme demandé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
109 lines
4.9 KiB
Python
109 lines
4.9 KiB
Python
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)
|