Files
Forge-Engine/game_engine/scenes/add_scene_object.py
T
williamandClaude Sonnet 5 b2e933f322
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
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
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>
2026-09-19 12:27:53 +02:00

147 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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