Files
Forge-Engine/tests/test_quests.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

169 lines
7.4 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.
"""Éditeur de quêtes (voir db/quests/, routes/quests/, onglet "🗺️ Quêtes"
de l'éditeur de scène 2D, templates/scene_edit.html) : id, titre, objectif, récompense
(score), statut (nouvelle/en_cours/terminee), résultat (reussi/echoue),
et un dialogue par statut (3 colonnes — voir static/js/quests/
quest-editor.js et sanitize_quest_dialogues). Une quête vit pour TOUT le
jeu, jamais par écran (mêmes conventions que les événements
personnalisés, db/custom_events/)."""
import db
def _make_game(tmp_game_slug_cleanup, name="pytest_quests"):
return tmp_game_slug_cleanup(db.create_game(name))
def test_create_quest_has_sensible_defaults(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
assert quest["title"] == "Trouver la clé"
assert quest["objectif"] == ""
assert quest["recompense_score"] == 0
assert quest["status"] == "nouvelle"
assert quest["resultat"] is None
assert quest["dialogues"] == {}
def test_create_quest_falls_back_to_default_title_when_blank(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, " ")
assert quest["title"] == "Nouvelle quête"
def test_list_quests_orders_most_recent_first(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
first = db.create_quest(slug, "Première")
second = db.create_quest(slug, "Seconde")
ids = [q["id"] for q in db.list_quests(slug)]
assert ids[:2] == [second["id"], first["id"]]
def test_update_quest_only_applies_known_valid_fields(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
updated = db.update_quest(slug, quest["id"], {
"title": "Trouver la clé dorée", "objectif": "Fouiller le donjon",
"recompense_score": 50, "status": "en_cours", "resultat": "reussi",
"unknown_field": "ignoré", "id": 999999,
})
assert updated["title"] == "Trouver la clé dorée"
assert updated["objectif"] == "Fouiller le donjon"
assert updated["recompense_score"] == 50
assert updated["status"] == "en_cours"
assert updated["resultat"] == "reussi"
assert updated["id"] == quest["id"] # "id" dans fields est ignoré, jamais modifiable
def test_update_quest_rejects_invalid_status_and_blank_title(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
updated = db.update_quest(slug, quest["id"], {"status": "en_pause", "title": " "})
assert updated["status"] == "nouvelle" # inchangé, valeur invalide ignorée
assert updated["title"] == "Trouver la clé" # inchangé, titre vide ignoré
def test_update_quest_returns_none_for_missing_quest(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
assert db.update_quest(slug, 999999, {"title": "x"}) is None
def test_delete_quest_removes_it(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
db.delete_quest(slug, quest["id"])
assert db.get_quest(slug, quest["id"]) is None
def test_sanitize_quest_dialogues_keeps_only_valid_lines_per_status():
"""speaker est un nom de PERSONNAGE libre (voir "ℹ️ Informations",
screens/rendering/personnage_names.py) — "Le Sorcier" est donc un nom
valide comme n'importe quel autre, seul un speaker/texte VIDE ou un
élément mal formé est rejeté."""
raw = {
"nouvelle": [{"speaker": "Le Sorcier", "text": "Bonjour !"}, {"speaker": "Joueur", "text": " "}],
"en_cours": [{"speaker": " ", "text": "sans nom"}, {"speaker": 42, "text": "nom non textuel"}, "pas un objet"],
"terminee": "pas une liste",
"statut_inconnu": [{"speaker": "Le Sorcier", "text": "ignoré"}],
}
cleaned = db.sanitize_quest_dialogues(raw)
assert cleaned == {
"nouvelle": [{"speaker": "Le Sorcier", "text": "Bonjour !"}],
"en_cours": [],
"terminee": [],
}
def test_set_quest_dialogues_persists_all_three_columns(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
dialogues = {
"nouvelle": [{"speaker": "pnj", "text": "Peux-tu m'aider ?"}, {"speaker": "joueur", "text": "Bien sûr !"}],
"en_cours": [{"speaker": "pnj", "text": "Alors, cette clé ?"}],
"terminee": [{"speaker": "pnj", "text": "Merci beaucoup !"}],
}
db.set_quest_dialogues(slug, quest["id"], dialogues)
reloaded = db.get_quest(slug, quest["id"])
assert reloaded["dialogues"] == dialogues
def test_set_quest_dialogues_returns_none_for_missing_quest(tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
assert db.set_quest_dialogues(slug, 999999, {}) is None
# ---------- Routes ----------
def test_quest_new_route_creates_and_returns_json(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
resp = client.post(f"/game/{slug}/quests/new", data='{"title": "Nouvelle quête du donjon"}', content_type="application/json")
assert resp.status_code == 200
quest = resp.get_json()["quest"]
assert quest["title"] == "Nouvelle quête du donjon"
assert db.get_quest(slug, quest["id"]) is not None
def test_quests_list_json_route_returns_all_quests(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
db.create_quest(slug, "A")
db.create_quest(slug, "B")
resp = client.get(f"/game/{slug}/quests/list.json")
titles = {q["title"] for q in resp.get_json()["quests"]}
assert titles == {"A", "B"}
def test_quest_update_route_persists_fields(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
resp = client.post(f"/game/{slug}/quests/{quest['id']}", data='{"recompense_score": 100, "status": "terminee"}', content_type="application/json")
assert resp.get_json()["quest"]["recompense_score"] == 100
assert resp.get_json()["quest"]["status"] == "terminee"
def test_quest_update_route_404s_for_missing_quest(client, game):
resp = client.post(f"/game/{game}/quests/999999", data="{}", content_type="application/json")
assert resp.status_code == 404
def test_quest_delete_route_removes_it(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
resp = client.post(f"/game/{slug}/quests/{quest['id']}/delete")
assert resp.get_json() == {"ok": True}
assert db.get_quest(slug, quest["id"]) is None
def test_quest_dialogues_route_sanitizes_and_persists(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
quest = db.create_quest(slug, "Trouver la clé")
resp = client.post(
f"/game/{slug}/quests/{quest['id']}/dialogues",
data='{"dialogues": {"nouvelle": [{"speaker": "pnj", "text": "Salut !"}], "en_cours": [], "terminee": []}}',
content_type="application/json",
)
assert resp.get_json()["dialogues"]["nouvelle"] == [{"speaker": "pnj", "text": "Salut !"}]
assert db.get_quest(slug, quest["id"])["dialogues"]["nouvelle"] == [{"speaker": "pnj", "text": "Salut !"}]
def test_quest_dialogues_route_404s_for_missing_quest(client, game):
resp = client.post(f"/game/{game}/quests/999999/dialogues", data='{"dialogues": {}}', content_type="application/json")
assert resp.status_code == 404