Files
Forge-Engine/tests/test_dialogue_box.py
T
williamandClaude Sonnet 5 2347a70c45
Build and deploy / test-python (push) Successful in 11m32s
Build and deploy / test-js (push) Successful in 1m12s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Quêtes/collision : personnages nommés, dialogues qui parlent, boîte de dialogue en jeu
- "ℹ️ Informations" (propriétés d'un personnage) : nom éditable, réutilisé
  comme "qui parle" dans l'éditeur de dialogue de quête (menu déroulant,
  plus "Joueur" toujours disponible) — remplace l'ancien choix binaire
  joueur/pnj. db.sanitize_quest_dialogues accepte maintenant un nom libre.

- Nouveau menu "🖥️ Interface" (palette d'objets) avec le premier widget :
  "💬 Boîte de dialogue" (kind="dialogue_box", screens/rendering/
  dialogue_box_style.py) — position/taille comme tout objet de scène,
  panneau "🎨 Style" dédié (police, taille, épaisseur, couleur du texte,
  couleur header/body/footer). Rendu en <div> à 3 zones, jamais soumis à
  la collision ni à l'éditeur de collision (exclu partout : obstacles,
  liste des règles, payload). Rendu côté jeu dans .sceneUI, une couche
  FIXE au viewport (jamais .sceneWorld, qui défile avec la caméra).

- static/js/play/dialogue-box-controller.js : fait le lien moteur entre
  l'action "quete" de l'éditeur de collision et ce widget — affiche la
  réplique en cours du dialogue correspondant au STATUT ACTUEL de la
  quête (gameData.quests, désormais exposé game-wide par
  full_game_payload.py), avance au clic sur "Suivant" (header = qui
  parle, body = texte, footer = bouton), se masque à la dernière réplique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:20:58 +02:00

141 lines
6.8 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.
"""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/personnage_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
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_personnage_names_is_alphabetical_and_deduped_across_scenes(tmp_game_slug_cleanup):
slug = tmp_game_slug_cleanup(db.create_game("pytest_personnage_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") # pas un personnage, ignoré
screens.set_scene_object_name(slug, decor_id, "Un décor nommé")
assert screens.list_personnage_names(slug) == ["Anna", "Zed le forgeron"]