Files
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

138 lines
7.3 KiB
Python
Raw Permalink 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.
from flask import abort, g, jsonify, redirect, request, url_for
from werkzeug.wrappers import Response
import auth
import db
import game_engine
from core.flask_app import app
from core.sprite_gate import forbid_admin_only_background, forbid_admin_only_character
@app.route("/game/<slug>/screens/<int:screen_id>/scene-objects/add", methods=["POST"])
def scene_object_add(slug: str, screen_id: int) -> Response:
"""Pose un nouvel objet sur la scène — mirror de routes/elements/
element_add.py, mais pour _scene_objects (voir game_engine/scenes/
add_scene_object.py).
Appelée en AJAX (voir addSceneObject(), static/game/js/scenes/scene-editor.js)
plutôt qu'en soumission de formulaire classique — sans ça, poser un
objet rechargeait TOUTE la page : perceptible comme un "vidage" de
l'éditeur de collision/quêtes le temps du rechargement, et perdait tout
état JS en cours (modale ouverte, etc.). Répond en JSON avec de quoi
ajouter l'objet au canevas SANS recharger — jamais sélectionné/ouvert
dans le panneau de propriétés pour autant (ça, voir
onSceneObjectMouseDown, reste un rechargement pour l'instant). La liste
"🧩 Collision" n'est PAS incluse ici : addSceneObject() la redemande
ensuite à routes/game/scenes/scene_collision_rule_objects.py (même endpoint
que pour un changement de rôle/nom, ou une suppression) plutôt que de
dupliquer ici la logique de build_collision_rule_objects()."""
kind = request.form.get("kind", "personnage")
forge_character = request.form.get("forge_character")
background_slug = request.form.get("background_slug")
asset_id = request.form.get("asset_id", type=int)
# Vrai glisser-déposer depuis la galerie (demande explicite : "n'importe
# où sur la scène", voir static/game/js/scenes/scene-editor.js::onCanvasDrop)
# — position EXACTE du dépôt, en pixels "monde" déjà calculés côté
# client (pas de zoom sur #canvas, voir onCanvasDrop). Absents pour un
# simple clic sur la galerie (comportement par défaut inchangé).
drop_x = request.form.get("x", type=float)
drop_y = request.form.get("y", type=float)
if forge_character:
forbid_admin_only_character(forge_character)
if background_slug:
forbid_admin_only_background(background_slug)
image_url = None
image_width = image_height = None
if asset_id:
# "Mes assets" (voir auth/user_assets_dir.py) : isolation stricte
# par propriétaire, même esprit que les projets (core/auth_guard.py)
# — jamais poser l'asset d'un autre compte, même en devinant son id.
asset = auth.get_user_asset(asset_id)
if not asset or asset["user_id"] != g.current_user["id"]:
abort(403)
image_url = url_for("serve_user_asset", user_id=asset["user_id"], filename=asset["filename"])
if kind == "fond":
# Sans ça, un "fond" importé (contrairement à un fond du
# catalogue Forge, voir background_library.py) retombait
# TOUJOURS sur 128×128 (les valeurs par défaut du schéma) —
# trop petit pour agrandir le "monde" au-delà du champ de la
# caméra, rendant impossible d'y poser un objet en dehors de
# ce champ (voir game_engine.resolve_scene_world_size). Un "decor"
# n'en a pas besoin (demande explicite : toujours 200×200 à la
# pose, voir add_scene_object.py::_DECOR_DEFAULT_SIZE).
import os
path = os.path.join(auth.user_assets_dir(asset["user_id"]), asset["filename"])
try:
with open(path, "rb") as fh:
dims = auth.image_dimensions(fh.read())
except OSError:
dims = None
if dims:
image_width, image_height = dims
# Un "fond" reste TOUJOURS ancré au coin haut-gauche du monde (0,0),
# jamais à la position du dépôt (bug corrigé : "décalage" — le vrai
# glisser-déposer envoie une position EXACTE même pour un fond, ce qui
# le décalait du coin de la scène et laissait un bandeau de canevas vide
# visible sur le dessus/le côté). Un "decor"/"personnage" garde lui sa
# position de dépôt normale.
new_id = game_engine.add_scene_object(
slug,
screen_id,
kind=kind,
forge_character=forge_character,
background_slug=background_slug,
image_url=image_url,
image_width=image_width,
image_height=image_height,
x=(None if kind == "fond" else drop_x),
y=(None if kind == "fond" else drop_y),
)
if drop_x is not None and drop_y is not None and kind != "fond":
# Ramène dans le cadre de la caméra si le dépôt était trop proche
# d'un bord (l'objet, une fois sa taille réelle connue, pourrait
# légèrement déborder) — même esprit que ai/tools.py::
# _clamp_to_camera pour l'IA, ici pour un dépôt manuel.
obj_for_clamp = game_engine.get_scene_object(slug, new_id)
screen = game_engine.get_screen(slug, screen_id)
obj_for_clamp = db.assert_not_none(
obj_for_clamp, "new_id vient d'etre cree par game_engine.add_scene_object juste au-dessus"
)
screen = db.assert_not_none(
screen, "screen_id valide : add_scene_object aurait leve IntegrityError sinon (FK scene_id)"
)
max_x = max(0, screen["scene_width"] - obj_for_clamp["width"])
max_y = max(0, screen["scene_height"] - obj_for_clamp["height"])
clamped_x = min(max(drop_x, 0), max_x)
clamped_y = min(max(drop_y, 0), max_y)
if (clamped_x, clamped_y) != (obj_for_clamp["x"], obj_for_clamp["y"]):
game_engine.update_scene_object_geometry(
slug, new_id, clamped_x, clamped_y, obj_for_clamp["width"], obj_for_clamp["height"]
)
if request.headers.get("X-Requested-With") == "fetch":
obj = game_engine.get_scene_object(slug, new_id)
obj = db.assert_not_none(obj, "new_id vient d'etre cree par game_engine.add_scene_object plus haut")
return jsonify(
{
"id": new_id,
"kind": obj["kind"],
"x": obj["x"],
"y": obj["y"],
"width": obj["width"],
"height": obj["height"],
"rendered_html": game_engine.render_scene_object(obj),
# Relance l'aperçu animé tout de suite (voir personnage-preview.js
# ::restartPersonnageIdlePreview) — sans ça, un personnage
# fraîchement posé restait figé sur sa première frame jusqu'au
# prochain rechargement de page (qui n'arrive plus jamais, voir
# addSceneObject() côté JS).
"animations": game_engine.resolve_personnage_animations(obj) if obj["kind"] == "personnage" else None,
}
)
# Même route que l'éditeur document (/game/<slug>/screens/<id>/edit,
# endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la
# vue de scène selon game_type, voir routes/game/screens/screen_edit.py.
# Conservé pour tout appelant non-JS (aucun connu actuellement, mais
# ne coûte rien à garder en secours).
return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))