- "ℹ️ 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>
169 lines
7.4 KiB
Python
169 lines
7.4 KiB
Python
"""É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
|