- 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>
131 lines
6.8 KiB
Python
131 lines
6.8 KiB
Python
import json
|
||
|
||
import db
|
||
|
||
from .ensure_scene_schema import ensure_scene_schema
|
||
|
||
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
|
||
_KINDS = {"personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widget"}
|
||
# Widgets d'interface (voir "🖥️ Interface", screens/rendering/dialogue_box_style.py) :
|
||
# assez grands pour lire une réplique/question sans redimensionner,
|
||
# jamais confondus visuellement avec un personnage/décor (128×128 par
|
||
# défaut). "score_widget" reste petit — juste un nombre.
|
||
_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, 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
|
||
document (screens/rendering/personnage_data.py) — un objet de scène
|
||
"personnage" partage donc directement la bibliothèque Forge et le
|
||
système d'animation déjà écrits. kind="decor" : une simple image fixe
|
||
(attributes.src), pas d'animation. kind="fond" : même image fixe que
|
||
"decor" (render_scene_object.py ne les distingue pas au rendu), mais
|
||
posée en dessous de tout le reste (z_index très négatif, voir
|
||
static/style.css .sceneBackground) et à la taille RÉELLE de l'image
|
||
choisie (voir screens/labels/background_library.py) par défaut — une
|
||
image plus grande que la scène devient alors le "monde" que la caméra
|
||
suit en défilant (voir static/js/play/personnage-controller.js).
|
||
|
||
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. 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 = {}
|
||
width = height = None
|
||
conn = db.connect(slug)
|
||
if kind == "personnage":
|
||
data = dict(_DEFAULT_PERSONNAGE_DATA)
|
||
if forge_character:
|
||
from ..labels.sprite_library import PUBLIC_SPRITE_LIBRARY
|
||
from ..labels.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||
if forge_character in PUBLIC_SPRITE_LIBRARY or forge_character in ADMIN_SPRITE_LIBRARY:
|
||
data["forge_character"] = forge_character
|
||
attributes["_personnage_data"] = json.dumps(data)
|
||
# Rôle "pnj" explicite (demande explicite : un personnage fraîchement
|
||
# posé n'est JAMAIS le joueur par défaut, même le premier de la
|
||
# scène — voir screens/rendering/personnage_role.py, "🏷️ Rôle" dans
|
||
# le panneau de propriétés pour le passer en "Joueur" à la main).
|
||
# Un rôle non défini retomberait sur "joueur" par défaut (voir
|
||
# resolve_personnage_role) : sans cette affectation explicite, un
|
||
# personnage fraîchement posé se ferait donc passer pour le joueur
|
||
# et disparaîtrait à tort de "🧩 Collision" (voir scene_edit_view.py,
|
||
# qui exclut toujours le joueur).
|
||
attributes["_personnage_role"] = "pnj"
|
||
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)
|
||
if background:
|
||
attributes["src"] = background["url"]
|
||
width, height = background["width"], background["height"]
|
||
elif kind == "dialogue_box":
|
||
width, height = _DIALOGUE_BOX_DEFAULT_SIZE
|
||
elif kind == "quiz_box":
|
||
width, height = _QUIZ_BOX_DEFAULT_SIZE
|
||
elif kind == "score_widget":
|
||
width, height = _SCORE_WIDGET_DEFAULT_SIZE
|
||
if kind == "fond":
|
||
z_index = -1000
|
||
else:
|
||
max_z = conn.execute(
|
||
"SELECT MAX(z_index) AS m FROM _scene_objects WHERE scene_id = ?", (scene_id,)
|
||
).fetchone()["m"]
|
||
z_index = (max_z or 0) + 1
|
||
cols = ["scene_id", "kind", "attributes", "z_index"]
|
||
values = [scene_id, kind, json.dumps(attributes), z_index]
|
||
if width is not None:
|
||
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"]
|
||
conn.commit()
|
||
conn.close()
|
||
return object_id
|