- Bug corrigé : les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" sont en position:fixed (hors du flux flex de .builder3) — ils flottaient PAR-DESSUS le canevas sans jamais réduire sa largeur, rendant sa partie droite/gauche inaccessible au clic/glisser tant qu'un panneau restait ouvert. .builderCanvasArea réserve maintenant leur largeur (marge) dès qu'un panneau est ouvert (:has()). - Boîte à quiz : header = "Quête : <titre>" (au lieu du texte de la question), corps = la question ET ses choix ensemble. - Mauvaise réponse : ne bloque plus JAMAIS la progression (bug signalé : "je suis obligé de bien répondre sinon j'avance pas") — surligne la bonne réponse en vert (le choix cliqué en rouge s'il était faux) puis avance automatiquement après un court délai, sans jamais octroyer de points. Un second clic pendant la révélation est ignoré. - Animations : la boîte à quiz rejoue une entrée (pop-in) à CHAQUE nouvelle question, la bonne réponse pulse en vert, une mauvaise réponse "secoue" en rouge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
219 lines
11 KiB
Python
219 lines
11 KiB
Python
"""Widget d'interface "🖥️ Interface -> 💬 Boîte de dialogue" (voir
|
||
screens/rendering/dialogue_box_style.py, screens/scenes/add_scene_object.py
|
||
kind="dialogue_box") : affiche automatiquement, en jeu, la réplique en
|
||
cours d'une quête déclenchée par une règle de collision "quete" (voir
|
||
static/js/play/dialogue-box-controller.js) — header/body/footer
|
||
stylisables. Et le nom d'un personnage ("ℹ️ Informations",
|
||
screens/scenes/set_scene_object_name.py), utilisé comme "qui parle" dans
|
||
l'éditeur de dialogue de quête (screens/rendering/scene_object_names.py)."""
|
||
import db
|
||
import screens
|
||
|
||
|
||
def _create_jeu2d_game(tmp_game_slug_cleanup, name="pytest_dialogue_box"):
|
||
slug = tmp_game_slug_cleanup(db.create_game(name))
|
||
screen_id = screens.create_screen(slug, "Scène 1", kind="jeu_2d")
|
||
return slug, screen_id
|
||
|
||
|
||
def test_add_dialogue_box_has_sensible_default_size(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
assert obj["kind"] == "dialogue_box"
|
||
assert obj["width"] == 320
|
||
assert obj["height"] == 150
|
||
|
||
|
||
def test_dialogue_box_default_style(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
style = screens.resolve_dialogue_box_style(obj)
|
||
assert style == screens.DEFAULT_DIALOGUE_BOX_STYLE
|
||
|
||
|
||
def test_render_dialogue_box_has_header_body_footer(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
html = screens.render_scene_object(obj)
|
||
assert "dialogueBoxHeader" in html
|
||
assert "dialogueBoxBody" in html
|
||
assert "dialogueBoxFooter" in html
|
||
assert f'data-object-id="{obj_id}"' in html
|
||
# Bug corrigé : le footer n'avait PAS data-dialogue-role="footer" (seul
|
||
# son bouton "Suivant" interne l'avait, sous "next-btn") — le JS
|
||
# (forgeShowQuestOffer, static/js/play/dialogue-box-controller.js) le
|
||
# cherche par ce sélecteur pour y injecter Accepter/Refuser en fin de
|
||
# dialogue d'une quête "nouvelle" ; sans lui, l'écran d'offre gardait
|
||
# silencieusement le bouton "Suivant" au lieu des deux nouveaux boutons.
|
||
assert 'data-dialogue-role="header"' in html
|
||
assert 'data-dialogue-role="body"' in html
|
||
assert 'data-dialogue-role="footer"' in html
|
||
|
||
|
||
def test_set_scene_object_dialogue_box_style_persists_and_sanitizes(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
style = screens.set_scene_object_dialogue_box_style(slug, obj_id, {
|
||
"font_family": "Georgia", "font_size": 999, "font_weight": "bold",
|
||
"text_color": "#ff0000", "header_bg": "#000000", "unknown_key": "ignored",
|
||
})
|
||
assert style["font_family"] == "Georgia"
|
||
assert style["font_size"] == 48 # bornée au maximum autorisé
|
||
assert style["font_weight"] == "bold"
|
||
assert style["text_color"] == "#ff0000"
|
||
assert style["header_bg"] == "#000000"
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
assert screens.resolve_dialogue_box_style(obj) == style
|
||
|
||
|
||
def test_dialogue_box_route_persists_style(client, tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
resp = client.post(
|
||
f"/game/{slug}/scene-objects/{obj_id}/dialogue-box-style",
|
||
data='{"style": {"header_bg": "#123456"}}', content_type="application/json",
|
||
)
|
||
assert resp.get_json()["style"]["header_bg"] == "#123456"
|
||
|
||
|
||
def test_dialogue_box_route_404s_for_missing_object(client, game):
|
||
resp = client.post(f"/game/{game}/scene-objects/999999/dialogue-box-style", data='{"style": {}}', content_type="application/json")
|
||
assert resp.status_code == 404
|
||
|
||
|
||
def test_dialogue_box_excluded_from_collision_rules_payload(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
screens.add_scene_object(slug, screen_id, kind="dialogue_box")
|
||
payload = screens.full_game_payload(slug)
|
||
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
|
||
box = next(o for o in scene["elements"] if o["kind"] == "dialogue_box")
|
||
assert "collision_rules" not in box
|
||
assert "collision" not in box
|
||
assert box["dialogue_box_style"] == screens.DEFAULT_DIALOGUE_BOX_STYLE
|
||
|
||
|
||
def test_full_game_payload_exposes_quests_game_wide(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
db.create_quest(slug, "Trouver la clé")
|
||
payload = screens.full_game_payload(slug)
|
||
assert [q["title"] for q in payload["quests"]] == ["Trouver la clé"]
|
||
|
||
|
||
# ---------- Nom d'un personnage ("ℹ️ Informations") ----------
|
||
|
||
def test_scene_object_name_is_none_by_default(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
assert obj["name"] is None
|
||
|
||
|
||
def test_set_scene_object_name_persists_and_trims(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
|
||
screens.set_scene_object_name(slug, obj_id, " Le Sorcier ")
|
||
assert screens.get_scene_object(slug, obj_id)["name"] == "Le Sorcier"
|
||
|
||
|
||
def test_set_scene_object_name_blank_clears_it(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
|
||
screens.set_scene_object_name(slug, obj_id, "Le Sorcier")
|
||
screens.set_scene_object_name(slug, obj_id, " ")
|
||
assert screens.get_scene_object(slug, obj_id)["name"] is None
|
||
|
||
|
||
def test_scene_object_name_route_persists(client, tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="personnage")
|
||
resp = client.post(f"/game/{slug}/scene-objects/{obj_id}/name", data='{"name": "Le Sorcier"}', content_type="application/json")
|
||
assert resp.status_code == 200
|
||
assert screens.get_scene_object(slug, obj_id)["name"] == "Le Sorcier"
|
||
|
||
|
||
def test_list_named_scene_object_names_is_alphabetical_deduped_and_includes_any_kind(tmp_game_slug_cleanup):
|
||
""""il peut sélectionner le nom du personnage qui parle" -> étendu à
|
||
N'IMPORTE QUEL objet de scène nommé (pas seulement un personnage),
|
||
voir screens/rendering/scene_object_names.py."""
|
||
slug = tmp_game_slug_cleanup(db.create_game("pytest_scene_object_names"))
|
||
scene_a = screens.create_screen(slug, "Scène A", kind="jeu_2d")
|
||
scene_b = screens.create_screen(slug, "Scène B", kind="jeu_2d")
|
||
zed_id = screens.add_scene_object(slug, scene_a, kind="personnage")
|
||
screens.set_scene_object_name(slug, zed_id, "Zed le forgeron")
|
||
anna_id = screens.add_scene_object(slug, scene_b, kind="personnage")
|
||
screens.set_scene_object_name(slug, anna_id, "Anna")
|
||
unnamed_id = screens.add_scene_object(slug, scene_a, kind="personnage") # sans nom, ignoré
|
||
decor_id = screens.add_scene_object(slug, scene_a, kind="decor") # nommé -> INCLUS (pas réservé au personnage)
|
||
screens.set_scene_object_name(slug, decor_id, "Un coffre")
|
||
dialogue_box_id = screens.add_scene_object(slug, scene_a, kind="dialogue_box") # widget d'UI, jamais un "qui parle"
|
||
screens.set_scene_object_name(slug, dialogue_box_id, "Boîte nommée par erreur")
|
||
|
||
assert screens.list_named_scene_object_names(slug) == ["Anna", "Un coffre", "Zed le forgeron"]
|
||
|
||
|
||
# ---------- Widgets "❓ Boîte à quiz" / "🏆 Score" ----------
|
||
|
||
def test_add_quiz_box_has_sensible_default_size(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
assert obj["kind"] == "quiz_box"
|
||
assert obj["width"] == 340
|
||
assert obj["height"] == 240
|
||
|
||
|
||
def test_add_score_widget_has_sensible_default_size(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="score_widget")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
assert obj["kind"] == "score_widget"
|
||
assert obj["width"] == 140
|
||
assert obj["height"] == 50
|
||
|
||
|
||
def test_render_quiz_box_has_header_and_choices(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
html = screens.render_scene_object(obj)
|
||
assert 'data-quiz-role="header"' in html # "Quête : <titre>", rempli en jeu
|
||
assert 'data-quiz-role="question"' in html # le texte de la question, dans le corps
|
||
assert 'data-quiz-role="choices"' in html
|
||
assert f'data-object-id="{obj_id}"' in html
|
||
assert "quizBoxFooter" not in html # pas de pied, une question se résout au clic
|
||
|
||
|
||
def test_render_score_widget_shows_zero_by_default(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
obj_id = screens.add_scene_object(slug, screen_id, kind="score_widget")
|
||
obj = screens.get_scene_object(slug, obj_id)
|
||
html = screens.render_scene_object(obj)
|
||
assert 'data-score-role="value"' in html
|
||
assert f'data-object-id="{obj_id}"' in html
|
||
|
||
|
||
def test_quiz_box_and_score_widget_share_the_same_style_system_as_dialogue_box(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
quiz_id = screens.add_scene_object(slug, screen_id, kind="quiz_box")
|
||
style = screens.set_scene_object_dialogue_box_style(slug, quiz_id, {"header_bg": "#abcdef"})
|
||
assert style["header_bg"] == "#abcdef"
|
||
obj = screens.get_scene_object(slug, quiz_id)
|
||
assert screens.resolve_dialogue_box_style(obj)["header_bg"] == "#abcdef"
|
||
|
||
|
||
def test_quiz_box_and_score_widget_excluded_from_collision_rules_payload(tmp_game_slug_cleanup):
|
||
slug, screen_id = _create_jeu2d_game(tmp_game_slug_cleanup)
|
||
screens.add_scene_object(slug, screen_id, kind="quiz_box")
|
||
screens.add_scene_object(slug, screen_id, kind="score_widget")
|
||
payload = screens.full_game_payload(slug)
|
||
scene = next(s for s in payload["screens"] if s["id"] == screen_id)
|
||
quiz = next(o for o in scene["elements"] if o["kind"] == "quiz_box")
|
||
score = next(o for o in scene["elements"] if o["kind"] == "score_widget")
|
||
for widget in (quiz, score):
|
||
assert "collision_rules" not in widget
|
||
assert "collision" not in widget
|
||
assert widget["dialogue_box_style"] == screens.DEFAULT_DIALOGUE_BOX_STYLE
|