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>
243 lines
12 KiB
Python
243 lines
12 KiB
Python
"""Crée un jeu de démo "Démo — Dialogues & Surbrillance" qui exerce, bout
|
|
en bout, les trois mécanismes de guidage du joueur déjà présents dans le
|
|
moteur (voir README, section "Survol, séquences temporisées, surbrillance,
|
|
overlay, verrouillage") :
|
|
|
|
- Écran 1 "Tutoriel" : une boîte de dialogue (widget "superposition")
|
|
s'ouvre TOUTE SEULE à l'affichage de l'écran (déclencheur "affichage"),
|
|
accueille le joueur et lui indique quoi faire. Son bouton "OK" la
|
|
referme ET met un autre bouton en surbrillance (liseré clignotant) pour
|
|
guider le clic suivant. Cliquer ce bouton met fin à la surbrillance, le
|
|
désactive (grisé, ne peut plus être recliqué) et ouvre une DEUXIÈME
|
|
boîte de dialogue "Bravo" en réaction au clic — fermable à son tour.
|
|
- Écran 2 "Message différé" : une boîte de dialogue "arrive toute seule"
|
|
3 secondes après l'affichage de l'écran (déclencheur "affichage" ->
|
|
action "attendre" -> action "Modifier un élément → Visibilité"), sans
|
|
aucune action du joueur — utile pour un message de mentor/notification.
|
|
|
|
Ce script utilise le VRAI client de test Flask contre les VRAIES routes de
|
|
l'appli (mêmes routes que celles appelées par le navigateur), et crée un
|
|
jeu PERSISTANT sous projects/ (contrairement aux tests pytest, il n'est
|
|
PAS supprimé à la fin) — pensé pour être ouvert et joué dans le navigateur
|
|
après coup.
|
|
|
|
Couleur du texte posée explicitement (#f5f6fa) sur chaque titre/texte à
|
|
l'intérieur d'une boîte de dialogue : le moteur ne fige JAMAIS de couleur
|
|
par défaut à la création d'un élément (voir
|
|
default_style_for_widget.py — "la figer dès la création ferait apparaître
|
|
une couleur que personne n'a demandée"), un titre/texte fraîchement posé
|
|
retombe donc sur le texte sombre par défaut de Bulma, pensé pour un fond
|
|
clair — illisible sur le fond sombre d'une boîte de dialogue tant que
|
|
personne n'a choisi une couleur claire à la main dans le panneau de
|
|
propriétés. Sans ce réglage explicite ici, la démo donnait l'impression
|
|
trompeuse que "la boîte de dialogue est assombrie" (elle ne l'est pas :
|
|
seul son texte, invisible par défaut sur fond sombre, l'était). Valeur
|
|
volontairement DIFFÉRENTE de la couleur suggérée par défaut dans le
|
|
panneau (#e8eaf0) : save_element_controls.py ignore délibérément un
|
|
réglage renvoyé identique à sa valeur par défaut tant qu'il n'a jamais été
|
|
personnalisé (même logique anti-figeage), donc envoyer #e8eaf0 telle
|
|
quelle ici n'aurait eu AUCUN effet, silencieusement.
|
|
|
|
Usage : python3 scripts/game/build_demo_dialogues.py
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from typing import Any
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from flask.testing import FlaskClient
|
|
from werkzeug.test import TestResponse
|
|
|
|
import db
|
|
import routes # noqa: F401 - enregistre toutes les routes
|
|
from core import jinja_filters # noqa: F401 - enregistre les filtres Jinja
|
|
from core.flask_app import app
|
|
|
|
|
|
def _redirect_id(resp: TestResponse, pattern: str) -> int:
|
|
match = re.search(pattern, resp.headers["Location"]) # NOSONAR S8371 - Location attendue, KeyError = echec clair
|
|
match = db.assert_not_none(
|
|
match,
|
|
f"motif {pattern!r} absent de Location: {resp.headers['Location']!r}", # NOSONAR S8371 - idem
|
|
)
|
|
return int(match.group(1))
|
|
|
|
|
|
def _create_screen(client: FlaskClient, slug: str, name: str) -> int:
|
|
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
|
return _redirect_id(resp, r"/screens/(\d+)/edit")
|
|
|
|
|
|
def _add_element(client: FlaskClient, slug: str, screen_id: int, widget: str) -> int:
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False
|
|
)
|
|
return _redirect_id(resp, r"selected=(\d+)")
|
|
|
|
|
|
def _add_child(client: FlaskClient, slug: str, parent_id: int, widget: str) -> int:
|
|
resp = client.post(
|
|
f"/game/{slug}/elements/{parent_id}/children/add", data={"widget": widget}, follow_redirects=False
|
|
)
|
|
return _redirect_id(resp, r"selected=(\d+)")
|
|
|
|
|
|
def _save(client: FlaskClient, slug: str, element_id: int, **ctrl: Any) -> None:
|
|
data = {f"ctrl_{k}": v for k, v in ctrl.items()}
|
|
client.post(f"/game/{slug}/elements/{element_id}/save", data=data)
|
|
|
|
|
|
def _add_node(client: FlaskClient, slug: str, screen_id: int, **fields: Any) -> int:
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({**fields}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200, ( # nosec B101 # noqa: S101 - script d'usage unique
|
|
resp.get_data(as_text=True)
|
|
)
|
|
result: int = resp.get_json()["id"]
|
|
return result
|
|
|
|
|
|
def _add_edge(client: FlaskClient, slug: str, screen_id: int, from_id: int, to_id: int, from_port: str = "out") -> None:
|
|
resp = client.post(
|
|
f"/game/{slug}/screens/{screen_id}/flow/edges/add",
|
|
data=json.dumps({"from_node_id": from_id, "from_port": from_port, "to_node_id": to_id}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200, ( # nosec B101 # noqa: S101 - script d'usage unique
|
|
resp.get_data(as_text=True)
|
|
)
|
|
|
|
|
|
def _modifier_element(client: FlaskClient, slug: str, screen_id: int, target_id: int, prop: str, value: str) -> int:
|
|
return _add_node(
|
|
client,
|
|
slug,
|
|
screen_id,
|
|
node_type="action",
|
|
action_type="modifier_element",
|
|
target_element_id=target_id,
|
|
element_property=prop,
|
|
element_value=value,
|
|
)
|
|
|
|
|
|
def build() -> None:
|
|
app.config["TESTING"] = True
|
|
with app.test_client() as client:
|
|
resp = client.post("/games/new", data={"name": "Demo Dialogues Surbrillance"}, follow_redirects=False)
|
|
assert resp.status_code == 302, ( # nosec B101 # noqa: S101 - script d'usage unique
|
|
resp.get_data(as_text=True)
|
|
)
|
|
slug = resp.headers["Location"].rstrip("/").split("/")[-1]
|
|
print(f"Jeu créé : slug={slug}")
|
|
|
|
# ---------------- Écran 1 : Tutoriel ----------------
|
|
screen1 = _create_screen(client, slug, "Tutoriel")
|
|
|
|
overlay_bienvenue = _add_element(client, slug, screen1, "superposition")
|
|
_save(client, slug, overlay_bienvenue, couleur_boite="#20263a", arrondi="16")
|
|
t1 = _add_child(client, slug, overlay_bienvenue, "titre")
|
|
_save(client, slug, t1, content="Bienvenue !", color="#f5f6fa")
|
|
t2 = _add_child(client, slug, overlay_bienvenue, "texte")
|
|
_save(
|
|
client,
|
|
slug,
|
|
t2,
|
|
content=(
|
|
"Ce petit tutoriel te montre comment fonctionnent les dialogues et la "
|
|
"surbrillance. Clique sur OK pour commencer."
|
|
),
|
|
color="#f5f6fa",
|
|
)
|
|
btn_ok = _add_child(client, slug, overlay_bienvenue, "bouton")
|
|
_save(client, slug, btn_ok, content="OK, c'est parti")
|
|
|
|
btn_target = _add_element(client, slug, screen1, "bouton")
|
|
_save(client, slug, btn_target, content="Clique-moi !")
|
|
|
|
overlay_bravo = _add_element(client, slug, screen1, "superposition")
|
|
_save(client, slug, overlay_bravo, couleur_boite="#1f3a24", arrondi="16")
|
|
t3 = _add_child(client, slug, overlay_bravo, "titre")
|
|
_save(client, slug, t3, content="Bravo !", color="#f5f6fa")
|
|
t4 = _add_child(client, slug, overlay_bravo, "texte")
|
|
_save(client, slug, t4, content="Tu as cliqué le bon bouton. Le tutoriel est terminé.", color="#f5f6fa")
|
|
btn_close_bravo = _add_child(client, slug, overlay_bravo, "bouton")
|
|
_save(client, slug, btn_close_bravo, content="Fermer")
|
|
|
|
# Déclencheur "affichage" (écran entier) -> ouvre le dialogue de bienvenue.
|
|
trig_affichage = _add_node(client, slug, screen1, node_type="trigger", trigger_event="affichage")
|
|
act_open_bienvenue = _modifier_element(client, slug, screen1, overlay_bienvenue, "visibilite", "visible")
|
|
_add_edge(client, slug, screen1, trig_affichage, act_open_bienvenue)
|
|
|
|
# Clic sur "OK" -> ferme le dialogue de bienvenue ET met le bouton cible en surbrillance.
|
|
trig_ok = _add_node(client, slug, screen1, node_type="trigger", trigger_element_id=btn_ok, trigger_event="clic")
|
|
act_close_bienvenue = _modifier_element(client, slug, screen1, overlay_bienvenue, "visibilite", "masque")
|
|
act_highlight_on = _modifier_element(client, slug, screen1, btn_target, "surbrillance", "on")
|
|
_add_edge(client, slug, screen1, trig_ok, act_close_bienvenue)
|
|
_add_edge(client, slug, screen1, trig_ok, act_highlight_on)
|
|
|
|
# Clic sur le bouton cible -> arrête la surbrillance, le désactive, ouvre "Bravo".
|
|
trig_target = _add_node(
|
|
client, slug, screen1, node_type="trigger", trigger_element_id=btn_target, trigger_event="clic"
|
|
)
|
|
act_highlight_off = _modifier_element(client, slug, screen1, btn_target, "surbrillance", "off")
|
|
act_lock_target = _modifier_element(client, slug, screen1, btn_target, "desactive", "on")
|
|
act_open_bravo = _modifier_element(client, slug, screen1, overlay_bravo, "visibilite", "visible")
|
|
_add_edge(client, slug, screen1, trig_target, act_highlight_off)
|
|
_add_edge(client, slug, screen1, trig_target, act_lock_target)
|
|
_add_edge(client, slug, screen1, trig_target, act_open_bravo)
|
|
|
|
# Clic sur "Fermer" (dans "Bravo") -> referme "Bravo".
|
|
trig_close_bravo = _add_node(
|
|
client, slug, screen1, node_type="trigger", trigger_element_id=btn_close_bravo, trigger_event="clic"
|
|
)
|
|
act_close_bravo = _modifier_element(client, slug, screen1, overlay_bravo, "visibilite", "masque")
|
|
_add_edge(client, slug, screen1, trig_close_bravo, act_close_bravo)
|
|
|
|
# ---------------- Écran 2 : Message différé ----------------
|
|
screen2 = _create_screen(client, slug, "Message différé")
|
|
|
|
overlay_mentor = _add_element(client, slug, screen2, "superposition")
|
|
_save(client, slug, overlay_mentor, couleur_boite="#2a2440", arrondi="16")
|
|
t5 = _add_child(client, slug, overlay_mentor, "titre")
|
|
_save(client, slug, t5, content="Message du mentor", color="#f5f6fa")
|
|
t6 = _add_child(client, slug, overlay_mentor, "texte")
|
|
_save(
|
|
client,
|
|
slug,
|
|
t6,
|
|
content="Ce message est apparu tout seul, 3 secondes après l'arrivée sur cet écran — sans aucun clic.",
|
|
color="#f5f6fa",
|
|
)
|
|
btn_close_mentor = _add_child(client, slug, overlay_mentor, "bouton")
|
|
_save(client, slug, btn_close_mentor, content="Fermer")
|
|
|
|
info2 = _add_element(client, slug, screen2, "texte")
|
|
_save(client, slug, info2, content="Attends 3 secondes sans rien faire : un message va apparaître tout seul.")
|
|
|
|
trig_affichage2 = _add_node(client, slug, screen2, node_type="trigger", trigger_event="affichage")
|
|
act_wait = _add_node(client, slug, screen2, node_type="action", action_type="attendre", data_value="3")
|
|
act_open_mentor = _modifier_element(client, slug, screen2, overlay_mentor, "visibilite", "visible")
|
|
_add_edge(client, slug, screen2, trig_affichage2, act_wait)
|
|
_add_edge(client, slug, screen2, act_wait, act_open_mentor)
|
|
|
|
trig_close_mentor = _add_node(
|
|
client, slug, screen2, node_type="trigger", trigger_element_id=btn_close_mentor, trigger_event="clic"
|
|
)
|
|
act_close_mentor = _modifier_element(client, slug, screen2, overlay_mentor, "visibilite", "masque")
|
|
_add_edge(client, slug, screen2, trig_close_mentor, act_close_mentor)
|
|
|
|
print(f"Terminé. Ouvre http://127.0.0.1:5050/game/{slug}/play pour tester.")
|
|
print(f"Éditeur : http://127.0.0.1:5050/game/{slug}/screens/{screen1}/edit")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
build()
|