Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+204
-80
@@ -20,6 +20,7 @@ DATA_OPERATION_LABELS, ...) plutôt que recopiées en dur. Un test dédié
|
||||
remplacée par une copie littérale."""
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
import requests
|
||||
from flask import url_for
|
||||
@@ -28,12 +29,13 @@ import auth
|
||||
import db
|
||||
import screens
|
||||
from db.dialogue_lines import QUESTION_REWARD_TYPES
|
||||
from screens.rendering.collision_rules import ACTION_TYPES, LEAF_ACTION_TYPES, CONDITION_OPERATOR_KEYS, TRIGGER_TYPES
|
||||
from screens.rendering.screen_triggers import TRIGGER_TYPES_SCREEN
|
||||
from screens.labels.data_operations import DATA_OPERATION_LABELS
|
||||
from screens.labels.element_visibility import ELEMENT_VISIBILITY_LABELS
|
||||
from screens.labels.surbrillance_values import SURBRILLANCE_LABELS
|
||||
from screens.labels.video_modes import VIDEO_MODE_LABELS
|
||||
from screens.rendering.collision_rules import ACTION_TYPES, CONDITION_OPERATOR_KEYS, LEAF_ACTION_TYPES, TRIGGER_TYPES
|
||||
from screens.rendering.screen_triggers import TRIGGER_TYPES_SCREEN
|
||||
|
||||
from .scenario_client import generate_image_url
|
||||
|
||||
_CONDITION_OPERATORS = sorted(CONDITION_OPERATOR_KEYS)
|
||||
@@ -53,37 +55,49 @@ _GLOBAL_VARIABLE_TYPES = sorted(db.GLOBAL_VARIABLE_TYPES)
|
||||
_FORGE_CHARACTERS = sorted(screens.SPRITE_LIBRARY)
|
||||
_BACKGROUNDS = sorted(screens.BACKGROUND_LIBRARY)
|
||||
|
||||
_LEAF_ACTION_SCHEMA = {
|
||||
_LEAF_ACTION_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"Une action FEUILLE (\"dialogue\" ou \"variable\", voir "
|
||||
'Une action FEUILLE ("dialogue" ou "variable", voir '
|
||||
"screens/rendering/collision_rules.py) — peut porter un champ "
|
||||
"optionnel \"then\" (une autre feuille, chaînage borné à 4)."
|
||||
'optionnel "then" (une autre feuille, chaînage borné à 4).'
|
||||
),
|
||||
"properties": {
|
||||
"type": {"type": "string", "enum": list(LEAF_ACTION_TYPES)},
|
||||
"id": {"type": "string", "description": "Identifiant de cette feuille, pour l'adresser plus tard via append_action_to_trigger."},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Identifiant de cette feuille, pour l'adresser plus tard via append_action_to_trigger.",
|
||||
},
|
||||
"dialogue": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"Pour type=dialogue : {id, lines}. Chaque élément de `lines` est SOIT une "
|
||||
"RÉPLIQUE {\"type\":\"dialogue\", \"speaker\": <qui parle>, \"text\": <texte>}, "
|
||||
"SOIT une QUESTION DE QUIZ {\"type\":\"question\", \"text\", \"choices\": "
|
||||
"[2 à 4 réponses], \"correct_index\": <index de la bonne réponse>, "
|
||||
f"\"reward_type\": {list(QUESTION_REWARD_TYPES)!r}, \"reward_amount\": <entier>}}. "
|
||||
'RÉPLIQUE {"type":"dialogue", "speaker": <qui parle>, "text": <texte>}, '
|
||||
'SOIT une QUESTION DE QUIZ {"type":"question", "text", "choices": '
|
||||
'[2 à 4 réponses], "correct_index": <index de la bonne réponse>, '
|
||||
f'"reward_type": {list(QUESTION_REWARD_TYPES)!r}, "reward_amount": <entier>}}. '
|
||||
"reward_amount alimente le SCORE NATIF du jeu (visible via un objet "
|
||||
"kind=\"score_widget\", nécessite aussi un objet kind=\"quiz_box\" posé pour que "
|
||||
'kind="score_widget", nécessite aussi un objet kind="quiz_box" posé pour que '
|
||||
"la question s'affiche) — crédité UNIQUEMENT si la réponse est correcte, sinon "
|
||||
"ignoré ; le joueur avance toujours à la ligne suivante, bonne réponse ou pas. "
|
||||
"N'invente JAMAIS une variable séparée pour suivre un score de quiz : "
|
||||
"reward_amount fait déjà tout, sans action \"variable\" supplémentaire."
|
||||
'reward_amount fait déjà tout, sans action "variable" supplémentaire.'
|
||||
),
|
||||
"properties": {"id": {"type": "string"}, "lines": {"type": "array"}},
|
||||
},
|
||||
"mark_completed": {"type": "boolean"},
|
||||
"target_variable": {"type": "string", "description": "Pour type=variable : nom d'une variable globale existante."},
|
||||
"target_variable": {
|
||||
"type": "string",
|
||||
"description": "Pour type=variable : nom d'une variable globale existante.",
|
||||
},
|
||||
"data_operation": {"type": "string", "enum": _DATA_OPERATIONS},
|
||||
"data_value": {"type": "string", "description": "Pour type=variable : absent pour definir_bool_vrai/definir_bool_faux/basculer_bool. Pour type=attendre : nombre de SECONDES à attendre avant \"then\" (ex. \"2\", \"1.5\")."},
|
||||
"data_value": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Pour type=variable : absent pour definir_bool_vrai/definir_bool_faux/basculer_bool. "
|
||||
'Pour type=attendre : nombre de SECONDES à attendre avant "then" (ex. "2", "1.5").'
|
||||
),
|
||||
},
|
||||
"object_id": {
|
||||
"type": "integer",
|
||||
"description": (
|
||||
@@ -102,7 +116,7 @@ _LEAF_ACTION_SCHEMA = {
|
||||
},
|
||||
"asset_url": {
|
||||
"type": "string",
|
||||
"description": "Pour type=son/video : URL d'un fichier déjà présent dans \"Mes assets\".",
|
||||
"description": 'Pour type=son/video : URL d\'un fichier déjà présent dans "Mes assets".',
|
||||
},
|
||||
"mode": {"type": "string", "enum": sorted(VIDEO_MODE_LABELS), "description": "Pour type=video."},
|
||||
"texte": {"type": "string", "description": "Pour type=indication : le texte affiché dans la bulle."},
|
||||
@@ -112,9 +126,11 @@ _LEAF_ACTION_SCHEMA = {
|
||||
"required": ["type"],
|
||||
}
|
||||
|
||||
_ACTION_SCHEMA = {
|
||||
_ACTION_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"description": "Une action de déclencheur — voir screens/rendering/collision_rules.py pour la forme exacte de chaque type.",
|
||||
"description": (
|
||||
"Une action de déclencheur — voir screens/rendering/collision_rules.py pour la forme exacte de chaque type."
|
||||
),
|
||||
"properties": {
|
||||
"type": {"type": "string", "enum": list(ACTION_TYPES)},
|
||||
"id": {"type": "string"},
|
||||
@@ -129,8 +145,12 @@ _ACTION_SCHEMA = {
|
||||
"mode": _LEAF_ACTION_SCHEMA["properties"]["mode"],
|
||||
"texte": _LEAF_ACTION_SCHEMA["properties"]["texte"],
|
||||
"duree_ms": _LEAF_ACTION_SCHEMA["properties"]["duree_ms"],
|
||||
"then": {"description": "Feuille suivante (dialogue/variable/surbrillance/visibilite/son/video/indication), récursif."},
|
||||
"sub_action": {"description": "Pour type=interagir : une action (pas \"interagir\" à nouveau)."},
|
||||
"then": {
|
||||
"description": (
|
||||
"Feuille suivante (dialogue/variable/surbrillance/visibilite/son/video/indication), récursif."
|
||||
),
|
||||
},
|
||||
"sub_action": {"description": 'Pour type=interagir : une action (pas "interagir" à nouveau).'},
|
||||
"variable": {"type": "string", "description": "Pour type=condition."},
|
||||
"operateur": {"type": "string", "enum": _CONDITION_OPERATORS},
|
||||
"si_vrai": {"description": "Pour type=condition : null ou une feuille."},
|
||||
@@ -139,21 +159,43 @@ _ACTION_SCHEMA = {
|
||||
"required": ["type"],
|
||||
}
|
||||
|
||||
TOOLS = [
|
||||
TOOLS: list[dict[str, Any]] = [
|
||||
{
|
||||
"name": "add_scene_object",
|
||||
"description": (
|
||||
"Pose un nouvel objet sur l'écran en cours (personnage, décor, fond, "
|
||||
"ou widget d'interface). Pour une image générée/de \"Mes assets\", "
|
||||
'ou widget d\'interface). Pour une image générée/de "Mes assets", '
|
||||
"utiliser image_url (jamais générer un personnage/sprite par ce biais)."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string", "enum": ["personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widget"]},
|
||||
"forge_character": {"type": "string", "enum": _FORGE_CHARACTERS, "description": "Pour kind=personnage : slug d'un personnage de la bibliothèque Forge existante (jamais un sprite généré)."},
|
||||
"background_slug": {"type": "string", "enum": _BACKGROUNDS, "description": "Pour kind=fond : slug d'une image de fond déjà existante dans la bibliothèque Forge."},
|
||||
"image_url": {"type": "string", "description": "URL d'une image déjà uploadée/générée (voir \"Mes assets\") — kind decor/fond uniquement, prioritaire sur background_slug."},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["personnage", "decor", "fond", "dialogue_box", "quiz_box", "score_widget"],
|
||||
},
|
||||
"forge_character": {
|
||||
"type": "string",
|
||||
"enum": _FORGE_CHARACTERS,
|
||||
"description": (
|
||||
"Pour kind=personnage : slug d'un personnage de la bibliothèque Forge "
|
||||
"existante (jamais un sprite généré)."
|
||||
),
|
||||
},
|
||||
"background_slug": {
|
||||
"type": "string",
|
||||
"enum": _BACKGROUNDS,
|
||||
"description": (
|
||||
"Pour kind=fond : slug d'une image de fond déjà existante dans la bibliothèque Forge."
|
||||
),
|
||||
},
|
||||
"image_url": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
'URL d\'une image déjà uploadée/générée (voir "Mes assets") — kind '
|
||||
"decor/fond uniquement, prioritaire sur background_slug."
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["kind"],
|
||||
},
|
||||
@@ -165,8 +207,10 @@ TOOLS = [
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"object_id": {"type": "integer"},
|
||||
"x": {"type": "number"}, "y": {"type": "number"},
|
||||
"width": {"type": "number"}, "height": {"type": "number"},
|
||||
"x": {"type": "number"},
|
||||
"y": {"type": "number"},
|
||||
"width": {"type": "number"},
|
||||
"height": {"type": "number"},
|
||||
},
|
||||
"required": ["object_id", "x", "y", "width", "height"],
|
||||
},
|
||||
@@ -174,8 +218,8 @@ TOOLS = [
|
||||
{
|
||||
"name": "set_object_name",
|
||||
"description": (
|
||||
"Donne un nom à un objet de scène (ex. \"Stan\", \"Aka\") — affiché comme "
|
||||
"\"qui parle\" dans les dialogues qui lui sont attachés."
|
||||
'Donne un nom à un objet de scène (ex. "Stan", "Aka") — affiché comme '
|
||||
'"qui parle" dans les dialogues qui lui sont attachés.'
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
@@ -207,8 +251,10 @@ TOOLS = [
|
||||
"object_id": {"type": "integer"},
|
||||
"enabled": {"type": "boolean"},
|
||||
"shape": {"type": "string", "enum": _COLLISION_SHAPES},
|
||||
"width": {"type": "number"}, "height": {"type": "number"},
|
||||
"offset_x": {"type": "number"}, "offset_y": {"type": "number"},
|
||||
"width": {"type": "number"},
|
||||
"height": {"type": "number"},
|
||||
"offset_x": {"type": "number"},
|
||||
"offset_y": {"type": "number"},
|
||||
},
|
||||
"required": ["object_id"],
|
||||
},
|
||||
@@ -216,13 +262,13 @@ TOOLS = [
|
||||
{
|
||||
"name": "set_quiz_box_config",
|
||||
"description": (
|
||||
"Règle les options d'une \"❓ Boîte à quiz\" pensées pour un quiz AUTONOME "
|
||||
"(RH/formation) : plein écran, minuteur (jamais imposé — \"aucun\" par défaut, "
|
||||
"au choix du créateur : \"question\" redémarre à chaque question, \"quiz\" est un "
|
||||
'Règle les options d\'une "❓ Boîte à quiz" pensées pour un quiz AUTONOME '
|
||||
'(RH/formation) : plein écran, minuteur (jamais imposé — "aucun" par défaut, '
|
||||
'au choix du créateur : "question" redémarre à chaque question, "quiz" est un '
|
||||
"seul compte à rebours pour tout le quiz), et DEUX modèles visuels INDÉPENDANTS "
|
||||
"(les deux réglages sont conservés en même temps, jamais l'un n'écrase l'autre) : "
|
||||
"dialog_template (visible SEULEMENT hors plein écran — variantes de forme/couleur "
|
||||
"sur une structure proche de \"Classique\") et page_template (visible SEULEMENT en "
|
||||
'sur une structure proche de "Classique") et page_template (visible SEULEMENT en '
|
||||
"plein écran — thème complet et autonome, structure HTML propre à chaque modèle). "
|
||||
"En plein écran, le score se retrouve affiché directement dans la boîte."
|
||||
),
|
||||
@@ -232,16 +278,29 @@ TOOLS = [
|
||||
"object_id": {"type": "integer"},
|
||||
"fullscreen": {"type": "boolean"},
|
||||
"timer_mode": {"type": "string", "enum": _QUIZ_BOX_TIMER_MODES},
|
||||
"timer_seconds": {"type": "integer", "description": "Durée du minuteur (3 à 600s), ignorée si timer_mode=\"aucun\"."},
|
||||
"dialog_template": {"type": "string", "enum": _QUIZ_BOX_DIALOG_TEMPLATES, "description": "Modèle \"boîte de dialogue\" — ne s'affiche que si fullscreen=false."},
|
||||
"page_template": {"type": "string", "enum": _QUIZ_BOX_PAGE_TEMPLATES, "description": "Modèle \"page de quiz\" — ne s'affiche que si fullscreen=true."},
|
||||
"timer_seconds": {
|
||||
"type": "integer",
|
||||
"description": 'Durée du minuteur (3 à 600s), ignorée si timer_mode="aucun".',
|
||||
},
|
||||
"dialog_template": {
|
||||
"type": "string",
|
||||
"enum": _QUIZ_BOX_DIALOG_TEMPLATES,
|
||||
"description": 'Modèle "boîte de dialogue" — ne s\'affiche que si fullscreen=false.',
|
||||
},
|
||||
"page_template": {
|
||||
"type": "string",
|
||||
"enum": _QUIZ_BOX_PAGE_TEMPLATES,
|
||||
"description": 'Modèle "page de quiz" — ne s\'affiche que si fullscreen=true.',
|
||||
},
|
||||
},
|
||||
"required": ["object_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "create_global_variable",
|
||||
"description": "Crée une variable globale (idempotent par nom) — utilisable ensuite dans les déclencheurs/conditions.",
|
||||
"description": (
|
||||
"Crée une variable globale (idempotent par nom) — utilisable ensuite dans les déclencheurs/conditions."
|
||||
),
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -256,10 +315,10 @@ TOOLS = [
|
||||
{
|
||||
"name": "set_collision_rules",
|
||||
"description": (
|
||||
"Remplace TOUTES les règles \"déclencheur -> action\" d'un objet. "
|
||||
"Déclencheurs disponibles : \"collision\" (contact avec le personnage "
|
||||
"\"joueur\" — voir set_object_role), \"clic\" (l'objet est cliqué/touché, "
|
||||
"aucun joueur requis), \"survol\" (le pointeur survole l'objet, aucun "
|
||||
'Remplace TOUTES les règles "déclencheur -> action" d\'un objet. '
|
||||
'Déclencheurs disponibles : "collision" (contact avec le personnage '
|
||||
'"joueur" — voir set_object_role), "clic" (l\'objet est cliqué/touché, '
|
||||
'aucun joueur requis), "survol" (le pointeur survole l\'objet, aucun '
|
||||
"joueur requis). Sanitizé côté serveur (screens.sanitize_collision_rules) : "
|
||||
"toute valeur invalide est silencieusement retirée plutôt que rejetée — "
|
||||
"compare le nombre de `rules` renvoyées dans le résultat à ce que tu as "
|
||||
@@ -316,7 +375,7 @@ TOOLS = [
|
||||
"description": (
|
||||
"Génère une image (fond ou objet 2D UNIQUEMENT, JAMAIS un "
|
||||
"personnage/sprite — la génération d'image n'est pas fiable pour "
|
||||
"ça) via Scenario, l'ajoute à \"Mes assets\" et la pose "
|
||||
'ça) via Scenario, l\'ajoute à "Mes assets" et la pose '
|
||||
"automatiquement sur l'écran en cours."
|
||||
),
|
||||
"input_schema": {
|
||||
@@ -370,9 +429,9 @@ TOOLS = [
|
||||
"description": (
|
||||
"Retire UNE action précise (leaf_id) d'un déclencheur d'OBJET, où "
|
||||
"qu'elle soit dans l'arbre (feuille directe, maillon d'une chaîne "
|
||||
"\"then\", sous-action d'un \"interagir\", ou branche d'une "
|
||||
"\"condition\"). Si c'était la SEULE action de la règle, la règle "
|
||||
"entière disparaît ; si elle avait une suite \"then\", celle-ci "
|
||||
'"then", sous-action d\'un "interagir", ou branche d\'une '
|
||||
'"condition"). Si c\'était la SEULE action de la règle, la règle '
|
||||
'entière disparaît ; si elle avait une suite "then", celle-ci '
|
||||
"prend sa place."
|
||||
),
|
||||
"input_schema": {
|
||||
@@ -397,7 +456,7 @@ TOOLS = [
|
||||
"name": "move_trigger_action",
|
||||
"description": (
|
||||
"Déplace une action précise (leaf_id) d'un cran vers le haut "
|
||||
"(\"up\") ou le bas (\"down\") DANS SA PROPRE CHAÎNE \"then\", pour "
|
||||
'("up") ou le bas ("down") DANS SA PROPRE CHAÎNE "then", pour '
|
||||
"un déclencheur d'OBJET — jamais au-delà de sa branche de "
|
||||
"condition/sous-action interagir."
|
||||
),
|
||||
@@ -426,7 +485,9 @@ TOOLS = [
|
||||
]
|
||||
|
||||
|
||||
def _clamp_to_camera(slug, screen_id, kind, x, y, width, height):
|
||||
def _clamp_to_camera(
|
||||
slug: str, screen_id: int, kind: str | None, x: float, y: float, width: float, height: float
|
||||
) -> tuple[float, float, str | None]:
|
||||
"""Le placement précis par coordonnées s'est montré peu fiable pour
|
||||
l'IA malgré des instructions explicites ET un avertissement dans le
|
||||
résultat de l'outil (bug observé à répétition — l'IA n'arrivait
|
||||
@@ -456,7 +517,15 @@ def _clamp_to_camera(slug, screen_id, kind, x, y, width, height):
|
||||
return clamped_x, clamped_y, note
|
||||
|
||||
|
||||
def _dispatch_add_scene_object(slug, screen_id, user_id, kind, forge_character=None, background_slug=None, image_url=None):
|
||||
def _dispatch_add_scene_object(
|
||||
slug: str,
|
||||
screen_id: int,
|
||||
user_id: int,
|
||||
kind: str,
|
||||
forge_character: str | None = None,
|
||||
background_slug: str | None = None,
|
||||
image_url: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
# Même garde que la galerie manuelle (core/sprite_gate.py) — un
|
||||
# compte non-admin ne doit pas pouvoir poser un sprite/fond
|
||||
# admin-only via l'IA alors que la galerie de l'éditeur ne les lui
|
||||
@@ -481,59 +550,90 @@ def _dispatch_add_scene_object(slug, screen_id, user_id, kind, forge_character=N
|
||||
if background_slug and background_slug not in screens.BACKGROUND_LIBRARY:
|
||||
raise ValueError(f"background_slug inconnu : {background_slug!r} (voir la liste enum du tool)")
|
||||
object_id = screens.add_scene_object(
|
||||
slug, screen_id, kind=kind, forge_character=forge_character,
|
||||
background_slug=background_slug, image_url=image_url,
|
||||
slug,
|
||||
screen_id,
|
||||
kind=kind,
|
||||
forge_character=forge_character,
|
||||
background_slug=background_slug,
|
||||
image_url=image_url,
|
||||
)
|
||||
obj = screens.get_scene_object(slug, object_id)
|
||||
assert obj is not None, ( # nosec B101 # noqa: S101 - narrowing de type, sur par construction
|
||||
"object_id vient d'etre cree par screens.add_scene_object juste au-dessus"
|
||||
)
|
||||
# Position par défaut (100,100, voir ensure_scene_schema.py) déjà
|
||||
# posée par screens.add_scene_object — reste passée par le même
|
||||
# garde-fou pour rester correcte même si ce défaut changeait un jour.
|
||||
clamped_x, clamped_y, note = _clamp_to_camera(slug, screen_id, kind, obj["x"], obj["y"], obj["width"], obj["height"])
|
||||
clamped_x, clamped_y, note = _clamp_to_camera(
|
||||
slug, screen_id, kind, obj["x"], obj["y"], obj["width"], obj["height"]
|
||||
)
|
||||
if note:
|
||||
screens.update_scene_object_geometry(slug, object_id, clamped_x, clamped_y, obj["width"], obj["height"])
|
||||
result = {"object_id": object_id}
|
||||
result: dict[str, Any] = {"object_id": object_id}
|
||||
if note:
|
||||
result["note"] = note
|
||||
return result
|
||||
|
||||
|
||||
def _dispatch_set_object_geometry(slug, screen_id, user_id, object_id, x, y, width, height):
|
||||
def _dispatch_set_object_geometry(
|
||||
slug: str, screen_id: int, user_id: int, object_id: int, x: float, y: float, width: float, height: float
|
||||
) -> dict[str, Any]:
|
||||
obj = screens.get_scene_object(slug, object_id)
|
||||
x, y, note = _clamp_to_camera(slug, screen_id, obj["kind"] if obj else None, x, y, width, height)
|
||||
screens.update_scene_object_geometry(slug, object_id, x, y, width, height)
|
||||
result = {"ok": True}
|
||||
result: dict[str, Any] = {"ok": True}
|
||||
if note:
|
||||
result["note"] = note
|
||||
return result
|
||||
|
||||
|
||||
def _dispatch_set_object_name(slug, screen_id, user_id, object_id, name):
|
||||
def _dispatch_set_object_name(slug: str, screen_id: int, user_id: int, object_id: int, name: str) -> dict[str, Any]:
|
||||
screens.set_scene_object_name(slug, object_id, name)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _dispatch_set_object_role(slug, screen_id, user_id, object_id, role):
|
||||
def _dispatch_set_object_role(slug: str, screen_id: int, user_id: int, object_id: int, role: str) -> dict[str, Any]:
|
||||
screens.set_scene_object_role(slug, object_id, role)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _dispatch_set_object_collision(slug, screen_id, user_id, object_id, enabled=True, shape="rectangle",
|
||||
width=None, height=None, offset_x=0, offset_y=0):
|
||||
def _dispatch_set_object_collision(
|
||||
slug: str,
|
||||
screen_id: int,
|
||||
user_id: int,
|
||||
object_id: int,
|
||||
enabled: bool = True,
|
||||
shape: str = "rectangle",
|
||||
width: float | None = None,
|
||||
height: float | None = None,
|
||||
offset_x: float = 0,
|
||||
offset_y: float = 0,
|
||||
) -> dict[str, Any]:
|
||||
# Même forme que routes/scenes/scene_object_collision.py (remplacement
|
||||
# complet des réglages, jamais un merge partiel).
|
||||
settings = {
|
||||
settings: dict[str, Any] = {
|
||||
"enabled": bool(enabled),
|
||||
"shape": shape if shape in _COLLISION_SHAPES else "rectangle",
|
||||
"width": width, "height": height,
|
||||
"offset_x": offset_x or 0, "offset_y": offset_y or 0,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"offset_x": offset_x or 0,
|
||||
"offset_y": offset_y or 0,
|
||||
}
|
||||
screens.set_scene_object_collision(slug, object_id, settings)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _dispatch_set_quiz_box_config(slug, screen_id, user_id, object_id, fullscreen=False,
|
||||
timer_mode="aucun", timer_seconds=20,
|
||||
dialog_template="defaut", page_template="classique"):
|
||||
def _dispatch_set_quiz_box_config(
|
||||
slug: str,
|
||||
screen_id: int,
|
||||
user_id: int,
|
||||
object_id: int,
|
||||
fullscreen: bool = False,
|
||||
timer_mode: str = "aucun",
|
||||
timer_seconds: int = 20,
|
||||
dialog_template: str = "defaut",
|
||||
page_template: str = "classique",
|
||||
) -> dict[str, Any]:
|
||||
# Même forme que screens.set_scene_object_quiz_config (remplacement
|
||||
# complet des réglages, jamais un merge partiel) — screens.py sanitize
|
||||
# de toute façon toute valeur hors énumération, ce garde-fou ici sert
|
||||
@@ -541,7 +641,7 @@ def _dispatch_set_quiz_box_config(slug, screen_id, user_id, object_id, fullscree
|
||||
# dialog_template/page_template sont conservés INDÉPENDAMMENT (voir
|
||||
# screens/rendering/quiz_box_config.py) : Ruby peut en régler un seul
|
||||
# sans jamais écraser l'autre.
|
||||
config = {
|
||||
config: dict[str, Any] = {
|
||||
"fullscreen": bool(fullscreen),
|
||||
"timer_mode": timer_mode if timer_mode in _QUIZ_BOX_TIMER_MODES else "aucun",
|
||||
"timer_seconds": timer_seconds or 20,
|
||||
@@ -552,54 +652,76 @@ def _dispatch_set_quiz_box_config(slug, screen_id, user_id, object_id, fullscree
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _dispatch_create_global_variable(slug, screen_id, user_id, name, var_type="texte", default_value="", per_player=True):
|
||||
variable_id = db.create_global_variable(slug, name, var_type=var_type, default_value=default_value, per_player=per_player)
|
||||
def _dispatch_create_global_variable(
|
||||
slug: str,
|
||||
screen_id: int,
|
||||
user_id: int,
|
||||
name: str,
|
||||
var_type: str = "texte",
|
||||
default_value: str = "",
|
||||
per_player: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
variable_id = db.create_global_variable(
|
||||
slug, name, var_type=var_type, default_value=default_value, per_player=per_player
|
||||
)
|
||||
return {"variable_id": variable_id}
|
||||
|
||||
|
||||
def _dispatch_set_collision_rules(slug, screen_id, user_id, object_id, rules):
|
||||
def _dispatch_set_collision_rules(
|
||||
slug: str, screen_id: int, user_id: int, object_id: int, rules: Any
|
||||
) -> dict[str, Any]:
|
||||
sanitized = screens.sanitize_collision_rules(rules)
|
||||
screens.set_scene_object_collision_rules(slug, object_id, sanitized)
|
||||
return {"ok": True, "rules": sanitized}
|
||||
|
||||
|
||||
def _dispatch_append_action_to_trigger(slug, screen_id, user_id, object_id, after_id, action):
|
||||
def _dispatch_append_action_to_trigger(
|
||||
slug: str, screen_id: int, user_id: int, object_id: int, after_id: str, action: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
ok = screens.append_action_to_trigger(slug, object_id, after_id, action)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_set_screen_triggers(slug, screen_id, user_id, rules):
|
||||
def _dispatch_set_screen_triggers(slug: str, screen_id: int, user_id: int, rules: Any) -> dict[str, Any]:
|
||||
sanitized = screens.sanitize_screen_triggers(rules)
|
||||
screens.set_screen_triggers(slug, screen_id, sanitized)
|
||||
return {"ok": True, "rules": sanitized}
|
||||
|
||||
|
||||
def _dispatch_append_action_to_screen_trigger(slug, screen_id, user_id, after_id, action):
|
||||
def _dispatch_append_action_to_screen_trigger(
|
||||
slug: str, screen_id: int, user_id: int, after_id: str, action: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
ok = screens.append_action_to_screen_trigger(slug, screen_id, after_id, action)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_remove_trigger_action(slug, screen_id, user_id, object_id, leaf_id):
|
||||
def _dispatch_remove_trigger_action(
|
||||
slug: str, screen_id: int, user_id: int, object_id: int, leaf_id: str
|
||||
) -> dict[str, Any]:
|
||||
ok = screens.remove_action_from_trigger(slug, object_id, leaf_id)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_remove_screen_trigger_action(slug, screen_id, user_id, leaf_id):
|
||||
def _dispatch_remove_screen_trigger_action(slug: str, screen_id: int, user_id: int, leaf_id: str) -> dict[str, Any]:
|
||||
ok = screens.remove_action_from_screen_trigger(slug, screen_id, leaf_id)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_move_trigger_action(slug, screen_id, user_id, object_id, leaf_id, direction):
|
||||
def _dispatch_move_trigger_action(
|
||||
slug: str, screen_id: int, user_id: int, object_id: int, leaf_id: str, direction: str
|
||||
) -> dict[str, Any]:
|
||||
ok = screens.move_action_in_trigger(slug, object_id, leaf_id, direction)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_move_screen_trigger_action(slug, screen_id, user_id, leaf_id, direction):
|
||||
def _dispatch_move_screen_trigger_action(
|
||||
slug: str, screen_id: int, user_id: int, leaf_id: str, direction: str
|
||||
) -> dict[str, Any]:
|
||||
ok = screens.move_action_in_screen_trigger(slug, screen_id, leaf_id, direction)
|
||||
return {"ok": ok}
|
||||
|
||||
|
||||
def _dispatch_add_generated_image(slug, screen_id, user_id, kind, prompt):
|
||||
def _dispatch_add_generated_image(slug: str, screen_id: int, user_id: int, kind: str, prompt: str) -> dict[str, Any]:
|
||||
"""Scenario -> "Mes assets" (auth.save_user_asset, source="ia") ->
|
||||
posée sur l'écran via LE MÊME chemin que Phase 1
|
||||
(screens.add_scene_object(image_url=...)) — jamais un chemin
|
||||
@@ -615,7 +737,7 @@ def _dispatch_add_generated_image(slug, screen_id, user_id, kind, prompt):
|
||||
return {"object_id": object_id, "asset_id": asset_id}
|
||||
|
||||
|
||||
_DISPATCH = {
|
||||
_DISPATCH: dict[str, Callable[..., dict[str, Any]]] = {
|
||||
"add_scene_object": _dispatch_add_scene_object,
|
||||
"set_object_geometry": _dispatch_set_object_geometry,
|
||||
"set_object_name": _dispatch_set_object_name,
|
||||
@@ -635,7 +757,9 @@ _DISPATCH = {
|
||||
}
|
||||
|
||||
|
||||
def dispatch_tool(slug, screen_id, user_id, tool_name, tool_input):
|
||||
def dispatch_tool(
|
||||
slug: str, screen_id: int, user_id: int, tool_name: str, tool_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Point d'entrée UNIQUE utilisé par la boucle tool-use (Phase 2) —
|
||||
lève KeyError pour un nom d'outil inconnu (jamais silencieux : un tool
|
||||
annoncé par TOOLS mais absent d'ici serait un bug de ce module, pas
|
||||
|
||||
Reference in New Issue
Block a user