Éditeur de collision (jeu 2D) : règles trigger -> action par objet
Nouvel onglet "🧩 Collision" dans l'éditeur de scène 2D : chaque objet (hors joueur et fond) peut porter des règles "à la collision" ou "dans un périmètre (px)" déclenchant une action (quête, attaque, événement, ou interagir - qui affiche "Appuie sur [touche]" puis exécute une sous-action à l'appui, un seul niveau d'imbrication). Backend (sanitisation, route de persistance, exposition dans full_game_payload) + assistant modal en cartes empilées côté client. Ajoute le moteur d'exécution runtime (collision-rules-controller.js) : détecte l'entrée en collision/périmètre avec le joueur à chaque tick, déclenche l'action une seule fois par entrée, pur JS client sans appel serveur (fonctionne à l'identique en ligne et en export SCORM). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
dfaf85fac1
commit
ee270d0286
@@ -0,0 +1,104 @@
|
||||
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 (référence libre — l'éditeur de quête lui-même
|
||||
# reste à construire, cette action ne fait QUE mémoriser
|
||||
# la référence pour l'instant).
|
||||
# - "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":
|
||||
quete_id = raw_action.get("quete_id")
|
||||
if not (isinstance(quete_id, str) and quete_id.strip()):
|
||||
return None
|
||||
action["quete_id"] = quete_id.strip()
|
||||
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)
|
||||
Reference in New Issue
Block a user