Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur Support de formation (voir docs/plan/PLAN.md), sans toucher a l'architecture en couches existante : - screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D, avant l'arrivee d'un second "moteur" cote document) : ~85 imports corriges, contrat import-linter mis a jour, meme forme de couches. - routes/, scripts/, static/, templates/, tests/ : tout ce qui est propre au jeu 2D deplace dans un sous-dossier game/ de chacun (routes/game/, static/game/, templates/game/, tests/game/, scripts/game/) ; ce qui est partage par le site (auth, onboarding, dashboard, uploads, db/) reste a la racine de chaque dossier. Un sous-dossier document/ (vide) cree dans chacun pour le futur chantier. - styles/ volontairement inchange : les 3 fichiers sources sont concatenes en un seul static/style.css charge par tout le site, scinder leur CONTENU (editeur vs partage) serait un refactor CSS distinct, pas un deplacement mecanique. - Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en profondeur : copie des assets, URLs d'icones relatives a static/style.css (qui ne bouge pas), manifeste, wrapper SCORM. - Deux regressions d'un sweep de renommage anterieur corrigees au passage (screens.js/screens/scene-objects incorrectement convertis en game_engine.js/game_engine/scene-objects dans des commentaires). - Effet de bord Windows decouvert et corrige : git mv + Path.write_text convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers normalises en LF. - .eslintrc.json/package.json : uniquement les chemins de glob mis a jour (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7 reste volontairement non committee (package-lock.json restaure a la version precedente). Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit, lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ; demarrage serveur + requetes HTTP manuelles confirmant que les assets deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien. SKIP=djlint : backlog H021 (styles inline) deja documente comme dette assumee dans CODE_QUALITY.md section 6, aucun template touche par ce commit au-dela d'un deplacement de fichier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
7.2 KiB
Python
147 lines
7.2 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", game_engine/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: str,
|
||
scene_id: int,
|
||
kind: str = "personnage",
|
||
forge_character: str | None = None,
|
||
background_slug: str | None = None,
|
||
image_url: str | None = None,
|
||
image_width: int | None = None,
|
||
image_height: int | None = None,
|
||
x: float | None = None,
|
||
y: float | None = None,
|
||
) -> int:
|
||
"""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 (game_engine/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 game_engine/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/game/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/game/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/game/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.animal_sprite_library import ADMIN_SPRITE_LIBRARY
|
||
from ..labels.sprite_library import PUBLIC_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 game_engine/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) # nosec B608 # noqa: S608 - cols construit uniquement a partir de litteraux Python, jamais d'une cle arbitraire ; valeurs parametrees (?)
|
||
object_id = int(conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"])
|
||
conn.commit()
|
||
conn.close()
|
||
return object_id
|