É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
+5
View File
@@ -83,6 +83,10 @@ from .rendering.trigger_for import _trigger_for
from .rendering.personnage_data import resolve_personnage_data, resolve_personnage_animations, resolve_personnage_idle_frame
from .rendering.personnage_role import resolve_personnage_role, PERSONNAGE_ROLES, PERSONNAGE_ROLE_LABELS, DEFAULT_PERSONNAGE_ROLE
from .rendering.collision_settings import resolve_collision_settings, DEFAULT_COLLISION_SETTINGS, COLLISION_SHAPES
from .rendering.collision_rules import (
resolve_collision_rules, sanitize_collision_rules,
ACTION_TYPES, TRIGGER_TYPES, DEFAULT_PERIMETRE_PX,
)
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS, sanitize_extra_commands
from .screens_repo.ensure_schema import ensure_schema
@@ -120,6 +124,7 @@ from .scenes.set_scene_object_personnage_data import set_scene_object_personnage
from .scenes.set_scene_object_commands import set_scene_object_commands
from .scenes.set_scene_object_role import set_scene_object_role
from .scenes.set_scene_object_collision import set_scene_object_collision
from .scenes.set_scene_object_collision_rules import set_scene_object_collision_rules
from .scenes.render_scene_object import render_scene_object
from .scenes.flow_palette import TRIGGER_EVENTS_2D, ACTION_TYPE_LABELS_2D, OBJECT_ACTION_PROPERTIES
from .elements.element_label import element_label
+10
View File
@@ -11,6 +11,7 @@ from ..rendering.personnage_data import resolve_personnage_animations
from ..rendering.personnage_commands import resolve_personnage_commands
from ..rendering.personnage_role import resolve_personnage_role
from ..rendering.collision_settings import resolve_collision_settings
from ..rendering.collision_rules import resolve_collision_rules
from ..scenes.list_scene_objects import list_scene_objects
from ..scenes.render_scene_object import render_scene_object
@@ -61,6 +62,15 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
# seulement "personnage" — un décor/fond peut aussi servir
# d'obstacle.
o["collision"] = resolve_collision_settings(o)
# Règles "à la collision/dans un périmètre -> action" (voir
# "🧩 Collision", templates/scene_edit.html, et
# static/js/play/collision-rules-controller.js pour leur
# exécution) — jamais sur "fond" (jamais une cible), ni sur
# le personnage "joueur" lui-même (ce sont TOUJOURS les
# AUTRES objets qui réagissent à SA présence, jamais
# l'inverse).
if o["kind"] != "fond" and not (o["kind"] == "personnage" and o.get("personnage_role") == "joueur"):
o["collision_rules"] = resolve_collision_rules(o)
payload_screens.append({**s, "elements": objects})
else:
elements = list_elements(slug, s["id"], enforce_visibility=True, player_id=player_id)
+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)
@@ -0,0 +1,23 @@
import json
import db
from .ensure_scene_schema import ensure_scene_schema
def set_scene_object_collision_rules(slug, object_id, rules):
"""Enregistre la liste de règles "déclencheur -> action" d'un objet de
scène (voir screens/rendering/collision_rules.py, "🧩 Collision" dans
templates/scene_edit.html) — un attribut JSON distinct de tous les
autres réglages d'objet."""
ensure_scene_schema(slug)
conn = db.connect(slug)
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
if not row:
conn.close()
return
attributes = json.loads(row["attributes"] or "{}")
attributes["_collision_rules"] = json.dumps(rules)
conn.execute("UPDATE _scene_objects SET attributes = ? WHERE id = ?", (json.dumps(attributes), object_id))
conn.commit()
conn.close()