Éditeur de quêtes : titre/objectif/récompense/statut/résultat + dialogue à 3 colonnes
Build and deploy / test-python (push) Successful in 6m14s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Nouvel espace "🗺️ Quêtes" (game-wide, comme les événements
personnalisés) : liste de cartes + "+ Nouvelle quête", chaque carte
ouvrant une modale d'arbre de dialogue en bulles de théâtre
("speaker: texte"), une colonne par statut de quête (nouvelle/en
cours/terminée) puisque le dialogue joué dépend de l'état de la
quête au moment où le joueur parle au PNJ. Bulles alternées
joueur/PNJ, couleur différente selon qui parle.

L'action "Déclencher une quête" de l'éditeur de collision n'est plus
un champ texte libre : elle ouvre directement CETTE MÊME modale
(picker de quêtes existantes + création à la volée), et finalise la
règle avec l'id numérique de la quête choisie — collision_rules.py
migré en conséquence (quete_id devient un entier, comme evenement_id).

Corrige aussi la miniature d'objet de la carte "🧩 Collision", 3× plus
grande (120px) comme demandé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 20:24:57 +02:00
co-authored by Claude Sonnet 5
parent 4391b36d45
commit 51427b5297
28 changed files with 881 additions and 29 deletions
+7 -5
View File
@@ -7,6 +7,7 @@ construit l'assistant "+ Action" ; ce fichier teste la couche serveur
(défauts, validation, exposition dans full_game_payload)."""
import re
import db
import screens
@@ -68,16 +69,17 @@ def test_rules_route_persists_a_perimeter_trigger_with_custom_radius(client, tmp
assert rules == [{"trigger": "perimetre", "perimetre_px": 40.0, "action": {"type": "attaque"}}]
def test_rules_route_persists_a_quest_action_with_free_form_id(client, tmp_game_slug_cleanup):
def test_rules_route_persists_a_quest_action_referencing_a_quest_id(client, tmp_game_slug_cleanup):
slug, screen_id = _create_jeu2d_game(client, tmp_game_slug_cleanup)
pnj_id = _add_personnage(client, slug, screen_id, role="pnj")
quest = db.create_quest(slug, "Trouver la clé")
resp = client.post(
f"/game/{slug}/scene-objects/{pnj_id}/collision-rules",
data='{"rules": [{"trigger": "collision", "action": {"type": "quete", "quete_id": "quete-001"}}]}',
data='{"rules": [{"trigger": "collision", "action": {"type": "quete", "quete_id": ' + str(quest["id"]) + '}}]}',
content_type="application/json",
)
assert resp.get_json()["rules"] == [{"trigger": "collision", "action": {"type": "quete", "quete_id": "quete-001"}}]
assert resp.get_json()["rules"] == [{"trigger": "collision", "action": {"type": "quete", "quete_id": quest["id"]}}]
def test_rules_route_persists_an_interagir_action_with_one_level_of_nesting(client, tmp_game_slug_cleanup):
@@ -89,11 +91,11 @@ def test_rules_route_persists_an_interagir_action_with_one_level_of_nesting(clie
resp = client.post(
f"/game/{slug}/scene-objects/{pnj_id}/collision-rules",
data='{"rules": [{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": "q1"}}}]}',
data='{"rules": [{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": 1}}}]}',
content_type="application/json",
)
assert resp.get_json()["rules"] == [
{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": "q1"}}},
{"trigger": "collision", "action": {"type": "interagir", "sub_action": {"type": "quete", "quete_id": 1}}},
]
+172
View File
@@ -0,0 +1,172 @@
"""Éditeur de quêtes (voir db/quests/, routes/quests/, nouveau lien
"🗺️ Quêtes" dans templates/base.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():
raw = {
"nouvelle": [{"speaker": "pnj", "text": "Bonjour !"}, {"speaker": "joueur", "text": " "}],
"en_cours": [{"speaker": "sorcier", "text": "invalide"}, "pas un objet"],
"terminee": "pas une liste",
"statut_inconnu": [{"speaker": "pnj", "text": "ignoré"}],
}
cleaned = db.sanitize_quest_dialogues(raw)
assert cleaned == {
"nouvelle": [{"speaker": "pnj", "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_quests_view_route_renders_the_list(client, tmp_game_slug_cleanup):
slug = _make_game(tmp_game_slug_cleanup)
db.create_quest(slug, "Trouver la clé")
resp = client.get(f"/game/{slug}/quests")
assert resp.status_code == 200
assert b"Trouver la cl" in resp.data
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