Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
Build and deploy / test-python (push) Successful in 10m59s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance,
  video, son, visibilite, indication, attendre comme actions, utilisables
  aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par
  Ruby (IA), avec blocs deplacables/supprimables dans une chaine.
- Corrige plusieurs variantes du bug "impossible de poser un objet hors
  du champ de la camera" (troncature du chainage d'actions a 4 maillons,
  fond importe pose a 128x128 au lieu de sa taille reelle, decalage du
  fond au vrai glisser-depose, redimensionnement manuel jamais propage
  au monde).
- Ajoute un vrai glisser-depose depuis la galerie vers la scene, la
  gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/
  Videos, suppression, reclassement fond<->decor sans re-upload).
- Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement,
  limite 5 min), avec validation de la duree reelle du fichier, et une
  replique audio optionnelle dans une bulle de dialogue.
- Fixe la taille de pose d'un objet/decor importe a 200x200 avec une
  boite de collision de 150x150.
- Filtre le selecteur de fichier des actions son/video par type reel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-12 00:28:46 +02:00
co-authored by Claude Sonnet 5
parent b07b231a61
commit 559331f9cf
67 changed files with 4739 additions and 306 deletions
+44 -10
View File
@@ -13,9 +13,18 @@ _KINDS = {"personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widg
_DIALOGUE_BOX_DEFAULT_SIZE = (320, 150)
_QUIZ_BOX_DEFAULT_SIZE = (340, 240)
_SCORE_WIDGET_DEFAULT_SIZE = (140, 50)
# "decor" (demande explicite) : taille fixe à la pose, JAMAIS la taille
# réelle de l'image importée (contrairement à "fond", voir plus bas) —
# une photo/illustration "Mes assets" est souvent bien plus grande qu'un
# petit objet de décor, 200×200 reste un point de départ raisonnable à
# ajuster ensuite à la poignée. La boîte de collision, elle, est posée
# plus PETITE que l'objet (150×150, centrée — voir collision_settings.py)
# pour rester proche de la silhouette réelle plutôt que du cadre entier.
_DECOR_DEFAULT_SIZE = (200, 200)
_DECOR_COLLISION_SIZE = (150, 150)
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None, image_url=None):
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None, image_url=None, image_width=None, image_height=None, x=None, y=None):
"""Pose un nouvel objet sur une scène (voir ensure_scene_schema.py).
kind="personnage" : attributes._personnage_data, MÊME structure/mêmes
fonctions resolve_personnage_* que le widget "personnage" de l'éditeur
@@ -33,9 +42,20 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
image_url (kind "fond"/"decor" uniquement) : une image ARBITRAIRE
(voir "Mes assets", auth/user_assets/) plutôt qu'une clé de
BACKGROUND_LIBRARY — prioritaire sur background_slug si les deux sont
fournis. Pas de largeur/hauteur connues d'avance ici (contrairement à
BACKGROUND_LIBRARY qui les documente) : retombe sur les colonnes par
défaut de _scene_objects, exactement comme un "decor" ordinaire."""
fournis. image_width/image_height (kind "fond" uniquement, voir
routes/scenes/scene_object_add.py::auth.image_dimensions) : taille
RÉELLE de l'image quand connue, pour que le "monde" puisse s'agrandir
à sa vraie taille. Un "decor" les ignore volontairement (demande
explicite) : toujours posé à _DECOR_DEFAULT_SIZE (200×200), quelle que
soit la taille réelle de l'image importée.
x/y (optionnels) : position EXACTE demandée par le créateur (voir
"vrai glisser-déposer" depuis la galerie, static/js/scenes/
scene-editor.js::onGalleryDragStart/onCanvasDrop) — remplace le
défaut habituel (100,100 posé par le schéma, voir
ensure_scene_schema.py) pour TOUT type d'objet, y compris "fond"
(qui, SANS position explicite, continue de démarrer au coin
haut-gauche du monde comme avant)."""
ensure_scene_schema(slug)
kind = kind if kind in _KINDS else "personnage"
attributes = {}
@@ -59,8 +79,16 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
# et disparaîtrait à tort de "🧩 Collision" (voir scene_edit_view.py,
# qui exclut toujours le joueur).
attributes["_personnage_role"] = "pnj"
elif kind in ("fond", "decor") and image_url:
elif kind == "fond" and image_url:
attributes["src"] = image_url
if image_width is not None and image_height is not None:
width, height = image_width, image_height
elif kind == "decor" and image_url:
attributes["src"] = image_url
width, height = _DECOR_DEFAULT_SIZE
attributes["_collision"] = json.dumps({
"width": _DECOR_COLLISION_SIZE[0], "height": _DECOR_COLLISION_SIZE[1],
})
elif kind == "fond" and background_slug:
from ..labels.background_library import BACKGROUND_LIBRARY
background = BACKGROUND_LIBRARY.get(background_slug)
@@ -83,11 +111,17 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
cols = ["scene_id", "kind", "attributes", "z_index"]
values = [scene_id, kind, json.dumps(attributes), z_index]
if width is not None:
# x=0/y=0 (jamais le défaut 100/100, voir ensure_scene_schema.py) :
# un fond commence au coin haut-gauche de la scène/du monde, pas
# décalé arbitrairement comme un objet ordinaire.
cols += ["width", "height", "x", "y"]
values += [width, height, 0, 0]
cols += ["width", "height"]
values += [width, height]
if x is None and y is None and kind == "fond":
# Comportement inchangé pour un fond posé SANS position explicite
# (galerie Forge/Mes assets par simple clic) : jamais le défaut
# 100/100 (voir ensure_scene_schema.py), toujours le coin
# haut-gauche de la scène/du monde.
x, y = 0, 0
if x is not None and y is not None:
cols += ["x", "y"]
values += [x, y]
placeholders = ", ".join(["?"] * len(values))
conn.execute(f"INSERT INTO _scene_objects ({', '.join(cols)}) VALUES ({placeholders})", values)
object_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
+12 -10
View File
@@ -4,16 +4,18 @@ from ..rendering.collision_rules import resolve_collision_rules, sanitize_collis
def append_action_to_trigger(slug, object_id, after_id, raw_action):
""""+ Ajouter une action" en bout de chaîne — extrait de
routes/triggers/triggers.py::trigger_append_action pour être
l'UNIQUE chemin d'écriture, partagé par la route HTTP ET par l'agent
IA (voir ai/tools.py) : ni l'un ni l'autre n'écrit `_collision_rules`
directement, tous deux passent par ici. `after_id` : dialogue.id
(feuille "dialogue") ou id (feuille "variable") de la DERNIÈRE
feuille d'une chaîne. Renvoie False si l'objet, `after_id`, ou
`raw_action` sont invalides, ou si la feuille visée a déjà un "then"
(jamais écrasé par cette fonction — set_then_action_in_rules ne fait
qu'ajouter en bout de chaîne réelle)."""
""""+ Ajouter une action" — extrait de routes/triggers/triggers.py::
trigger_append_action pour être l'UNIQUE chemin d'écriture, partagé
par la route HTTP ET par l'agent IA (voir ai/tools.py) : ni l'un ni
l'autre n'écrit `_collision_rules` directement, tous deux passent par
ici. `after_id` : dialogue.id (feuille "dialogue") ou id (toute autre
feuille) — INSÈRE `raw_action` juste après cette feuille, où qu'elle
soit dans la chaîne (pas seulement en bout réel — voir
set_then_action_in_rules, demande explicite : "pouvoir ajouter
n'importe quelle action à n'importe quel niveau du schéma") : une
suite déjà présente passe DERRIÈRE la nouvelle action, jamais
écrasée. Renvoie False si l'objet, `after_id`, ou `raw_action` sont
invalides."""
obj = get_scene_object(slug, object_id)
if obj is None:
return False
+18
View File
@@ -0,0 +1,18 @@
from .get_scene_object import get_scene_object
from .set_scene_object_collision_rules import set_scene_object_collision_rules
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, move_action_in_rules
def move_action_in_trigger(slug, object_id, leaf_id, direction):
"""Déplace une action précise d'un cran ("up"/"down", voir
screens/rendering/collision_rules.py::move_action_in_rules) dans son
déclencheur d'OBJET — même patron qu'append_action_to_trigger."""
obj = get_scene_object(slug, object_id)
if obj is None:
return False
rules = resolve_collision_rules(obj)
if not move_action_in_rules(rules, leaf_id, direction):
return False
sanitized = sanitize_collision_rules(rules)
set_scene_object_collision_rules(slug, object_id, sanitized)
return True
@@ -0,0 +1,20 @@
from .get_scene_object import get_scene_object
from .set_scene_object_collision_rules import set_scene_object_collision_rules
from ..rendering.collision_rules import resolve_collision_rules, sanitize_collision_rules, remove_action_from_rules
def remove_action_from_trigger(slug, object_id, leaf_id):
"""Retire une action précise (voir screens/rendering/collision_rules.py
::remove_action_from_rules) d'un déclencheur d'OBJET — même patron
qu'append_action_to_trigger : UNIQUE chemin d'écriture, partagé par la
route HTTP (routes/triggers/triggers.py) et l'agent IA (voir
ai/tools.py). Renvoie False si l'objet ou `leaf_id` sont introuvables."""
obj = get_scene_object(slug, object_id)
if obj is None:
return False
rules = resolve_collision_rules(obj)
if not remove_action_from_rules(rules, leaf_id):
return False
sanitized = sanitize_collision_rules(rules)
set_scene_object_collision_rules(slug, object_id, sanitized)
return True