Éditeur de collision (jeu 2D) : règles trigger -> action par objet
Build and deploy / test-python (push) Successful in 6m31s
Build and deploy / test-js (push) Successful in 50s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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:
william
2026-09-02 17:11:42 +02:00
co-authored by Claude Sonnet 5
parent dfaf85fac1
commit ee270d0286
15 changed files with 989 additions and 0 deletions
+104
View File
@@ -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)