Quêtes : bulle "❓ Question" à choix multiples, à côté de "+ Réplique"
Nouveau type de ligne dans un dialogue de quête (voir
db/quests/sanitize_quest_dialogues.py) : une bulle JAUNE, dans la même
chaîne verticale que les répliques (même alignement, reliée par un
trait) — header : nombre de choix (2 à 8) + récompense (score, montant
en points) ; body : la question, ses choix et un bouton radio pour
désigner la bonne réponse. Aucune limite au nombre de questions par
colonne, comme pour une réplique.
Les lignes de dialogue portent maintenant explicitement
{"type": "dialogue", ...} (au lieu d'un objet sans type) pour
distinguer les deux formes — migration de forme, tests mis à jour.
Portée de ce commit : l'ÉDITEUR (créer/modifier une question). Le jeu
lui-même (afficher la question au joueur, vérifier la réponse,
attribuer la récompense) reste à construire — le widget "💬 Boîte de
dialogue" ne sait aujourd'hui afficher qu'une réplique.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f2ed602459
commit
e972acf9e6
+2
-2
@@ -87,7 +87,7 @@ from .quests.create_quest import create_quest
|
||||
from .quests.update_quest import update_quest
|
||||
from .quests.delete_quest import delete_quest
|
||||
from .quests.set_quest_dialogues import set_quest_dialogues
|
||||
from .quests.sanitize_quest_dialogues import sanitize_quest_dialogues
|
||||
from .quests.sanitize_quest_dialogues import sanitize_quest_dialogues, QUESTION_REWARD_TYPES
|
||||
|
||||
__all__ = [
|
||||
"PROJECTS_DIR", "FIELD_TYPES", "GLOBAL_VARIABLE_TYPES", "PLAYER_SHARED",
|
||||
@@ -108,5 +108,5 @@ __all__ = [
|
||||
"list_custom_events", "get_custom_event", "create_custom_event", "update_custom_event",
|
||||
"QUEST_STATUS_CHOICES", "QUEST_STATUS_LABELS", "QUEST_RESULT_CHOICES", "QUEST_RESULT_LABELS",
|
||||
"list_quests", "get_quest", "create_quest", "update_quest", "delete_quest",
|
||||
"set_quest_dialogues", "sanitize_quest_dialogues",
|
||||
"set_quest_dialogues", "sanitize_quest_dialogues", "QUESTION_REWARD_TYPES",
|
||||
]
|
||||
|
||||
@@ -3,18 +3,73 @@ from .constants import QUEST_STATUS_CHOICES
|
||||
_MAX_LINES_PER_COLUMN = 200
|
||||
_MAX_SPEAKER_LENGTH = 60
|
||||
|
||||
# Question à choix (voir "❓ Question", static/js/quests/quest-editor.js) :
|
||||
# une bulle jaune posée dans le même enchaînement qu'une réplique de
|
||||
# dialogue — plusieurs choix, une seule bonne réponse, une récompense.
|
||||
# Un seul type de récompense pour l'instant (le score, voir db.SCORE_STATUS_*/
|
||||
# db.set_score_value) — liste plutôt qu'un booléen pour pouvoir en ajouter
|
||||
# d'autres plus tard sans changer la forme des données.
|
||||
QUESTION_REWARD_TYPES = ["score"]
|
||||
_MIN_CHOICES = 2
|
||||
_MAX_CHOICES = 8
|
||||
|
||||
|
||||
def _sanitize_dialogue_line(raw_line):
|
||||
speaker = raw_line.get("speaker")
|
||||
text = raw_line.get("text")
|
||||
if not isinstance(speaker, str) or not speaker.strip():
|
||||
return None
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return None
|
||||
return {"type": "dialogue", "speaker": speaker.strip()[:_MAX_SPEAKER_LENGTH], "text": text.strip()}
|
||||
|
||||
|
||||
def _sanitize_question_line(raw_line):
|
||||
text = raw_line.get("text")
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return None
|
||||
raw_choices = raw_line.get("choices")
|
||||
if not isinstance(raw_choices, list):
|
||||
return None
|
||||
choices = [c.strip() for c in raw_choices if isinstance(c, str) and c.strip()][:_MAX_CHOICES]
|
||||
if len(choices) < _MIN_CHOICES:
|
||||
return None
|
||||
try:
|
||||
correct_index = int(raw_line.get("correct_index"))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not (0 <= correct_index < len(choices)):
|
||||
return None
|
||||
reward_type = raw_line.get("reward_type")
|
||||
if reward_type not in QUESTION_REWARD_TYPES:
|
||||
reward_type = QUESTION_REWARD_TYPES[0]
|
||||
try:
|
||||
reward_amount = max(0, int(raw_line.get("reward_amount", 0)))
|
||||
except (TypeError, ValueError):
|
||||
reward_amount = 0
|
||||
return {
|
||||
"type": "question", "text": text.strip(), "choices": choices,
|
||||
"correct_index": correct_index, "reward_type": reward_type, "reward_amount": reward_amount,
|
||||
}
|
||||
|
||||
|
||||
def _sanitize_line(raw_line):
|
||||
if not isinstance(raw_line, dict):
|
||||
return None
|
||||
if raw_line.get("type") == "question":
|
||||
return _sanitize_question_line(raw_line)
|
||||
return _sanitize_dialogue_line(raw_line)
|
||||
|
||||
|
||||
def sanitize_quest_dialogues(raw_dialogues):
|
||||
"""Une réplique = {speaker: '<nom du personnage>', text: '...'} — le
|
||||
nom vient du champ "ℹ️ Informations" d'un objet de scène kind=
|
||||
"personnage" (voir screens/rendering/scene_object_names.py), ou
|
||||
"Joueur" (toujours proposé, voir static/js/quests/quest-editor.js) :
|
||||
texte libre côté serveur (jamais un enum fixe — n'importe quel nom
|
||||
donné à un personnage doit être accepté), juste borné en longueur.
|
||||
Une colonne par statut de quête (voir QUEST_STATUS_CHOICES), même
|
||||
esprit que resolve_collision_rules.sanitize_collision_rules : rejette
|
||||
tout élément invalide plutôt que de lever, ne garde jamais un texte
|
||||
ou un nom vide."""
|
||||
"""Une colonne (voir QUEST_STATUS_CHOICES) contient une suite de
|
||||
répliques de dialogue ({type: "dialogue", speaker, text} — le nom
|
||||
vient du champ "ℹ️ Informations" d'un objet de scène, ou "Joueur",
|
||||
voir screens/rendering/scene_object_names.py) ET/OU de questions à
|
||||
choix ({type: "question", text, choices, correct_index, reward_type,
|
||||
reward_amount}, voir QUESTION_REWARD_TYPES ci-dessus) — même esprit
|
||||
que resolve_collision_rules.sanitize_collision_rules : rejette tout
|
||||
élément invalide plutôt que de lever."""
|
||||
result = {status: [] for status in QUEST_STATUS_CHOICES}
|
||||
if not isinstance(raw_dialogues, dict):
|
||||
return result
|
||||
@@ -22,16 +77,6 @@ def sanitize_quest_dialogues(raw_dialogues):
|
||||
raw_lines = raw_dialogues.get(status)
|
||||
if not isinstance(raw_lines, list):
|
||||
continue
|
||||
cleaned = []
|
||||
for raw_line in raw_lines[:_MAX_LINES_PER_COLUMN]:
|
||||
if not isinstance(raw_line, dict):
|
||||
continue
|
||||
speaker = raw_line.get("speaker")
|
||||
text = raw_line.get("text")
|
||||
if not isinstance(speaker, str) or not speaker.strip():
|
||||
continue
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
continue
|
||||
cleaned.append({"speaker": speaker.strip()[:_MAX_SPEAKER_LENGTH], "text": text.strip()})
|
||||
result[status] = cleaned
|
||||
cleaned = [_sanitize_line(raw_line) for raw_line in raw_lines[:_MAX_LINES_PER_COLUMN]]
|
||||
result[status] = [line for line in cleaned if line]
|
||||
return result
|
||||
|
||||
@@ -231,14 +231,66 @@ function questDialogueBubbleHtml(line, status, index) {
|
||||
);
|
||||
}
|
||||
|
||||
function questEscAttr(str) {
|
||||
return (str || '').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// "❓ Question" — une bulle JAUNE dans la même chaîne qu'une réplique de
|
||||
// dialogue (même colonne, même alignement vertical relié par un trait,
|
||||
// voir .questBubbleRow/.questBubble ci-dessus et static/style.css) mais
|
||||
// un type de ligne différent (voir db/quests/sanitize_quest_dialogues.py) :
|
||||
// header = nombre de choix + récompense (type + montant), body = la
|
||||
// question et ses choix, avec la bonne réponse cochée. Aucune limite au
|
||||
// nombre de questions par colonne (voir questAddQuestion), comme pour
|
||||
// une réplique.
|
||||
function questQuestionChoiceRowHtml(choice, choiceIndex, correctIndex, status, index) {
|
||||
var radioName = 'questCorrect-' + status + '-' + index;
|
||||
return (
|
||||
'<div class="questChoiceRow">' +
|
||||
'<input type="radio" name="' + radioName + '" class="questChoiceRadio" title="Bonne réponse" ' +
|
||||
(choiceIndex === correctIndex ? 'checked' : '') +
|
||||
' onchange="questSetQuestionCorrectIndex(\'' + status + '\', ' + index + ', ' + choiceIndex + ')">' +
|
||||
'<input type="text" class="questChoiceInput" placeholder="Choix ' + (choiceIndex + 1) + '" value="' + questEscAttr(choice) + '" ' +
|
||||
'onchange="questSetQuestionChoiceText(\'' + status + '\', ' + index + ', ' + choiceIndex + ', this.value)">' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function questQuestionBubbleHtml(line, status, index) {
|
||||
var choicesHtml = line.choices.map(function (c, i) { return questQuestionChoiceRowHtml(c, i, line.correct_index, status, index); }).join('');
|
||||
return (
|
||||
'<div class="questBubbleRow">' +
|
||||
'<div class="questBubble questBubble-question">' +
|
||||
'<div class="questBubbleHeader">' +
|
||||
'<label class="questQuestionHeaderField" title="Nombre de choix">🔢 <input type="number" min="2" max="8" value="' + line.choices.length + '" ' +
|
||||
'onchange="questSetQuestionChoiceCount(\'' + status + '\', ' + index + ', this.value)"></label>' +
|
||||
'<label class="questQuestionHeaderField" title="Récompense (points de score)">🏆 <input type="number" min="0" value="' + line.reward_amount + '" ' +
|
||||
'onchange="questSetQuestionRewardAmount(\'' + status + '\', ' + index + ', this.value)"> pts</label>' +
|
||||
'<button type="button" class="questBubbleDelete" title="Supprimer" onclick="questDeleteBubble(\'' + status + '\', ' + index + ')">✕</button>' +
|
||||
'</div>' +
|
||||
'<div class="questBubbleBody">' +
|
||||
'<input type="text" class="questQuestionText" placeholder="Question..." value="' + questEscAttr(line.text) + '" ' +
|
||||
'onchange="questUpdateQuestionText(\'' + status + '\', ' + index + ', this.value)">' +
|
||||
'<div class="questChoicesList">' + choicesHtml + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
|
||||
function questDialogueColumnHtml(quest, status) {
|
||||
var lines = (quest.dialogues && quest.dialogues[status]) || [];
|
||||
var bubblesHtml = lines.map(function (line, i) { return questDialogueBubbleHtml(line, status, i); }).join('');
|
||||
var bubblesHtml = lines.map(function (line, i) {
|
||||
return line.type === 'question' ? questQuestionBubbleHtml(line, status, i) : questDialogueBubbleHtml(line, status, i);
|
||||
}).join('');
|
||||
return (
|
||||
'<div class="questDialogueColumn">' +
|
||||
'<h4 class="questDialogueColumnTitle">' + QUEST_STATUS_LABELS[status] + '</h4>' +
|
||||
'<div class="questDialogueColumnBubbles" id="questDialogueColumn-' + status + '">' + bubblesHtml + '</div>' +
|
||||
'<div class="questDialogueColumnButtons">' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn" onclick="questAddBubble(\'' + status + '\')">✚ Réplique</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn questQuestionAddBtn" onclick="questAddQuestion(\'' + status + '\')">❓ Question</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
@@ -351,11 +403,69 @@ function questAddBubble(status) {
|
||||
quest.dialogues = quest.dialogues || {};
|
||||
var lines = quest.dialogues[status] || (quest.dialogues[status] = []);
|
||||
var lastSpeaker = lines.length ? lines[lines.length - 1].speaker : 'PNJ';
|
||||
lines.push({ speaker: questNextDefaultSpeaker(lastSpeaker), text: '' });
|
||||
lines.push({ type: 'dialogue', speaker: questNextDefaultSpeaker(lastSpeaker), text: '' });
|
||||
renderQuestDialogueModalBody(quest.id);
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
// "❓ Question" — voir questQuestionBubbleHtml ci-dessus. Défauts : 2
|
||||
// choix vides, la 1ère cochée bonne réponse, récompense "score" à 0 pt
|
||||
// (l'auteur règle le montant dans le header de la bulle).
|
||||
function questAddQuestion(status) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
quest.dialogues = quest.dialogues || {};
|
||||
var lines = quest.dialogues[status] || (quest.dialogues[status] = []);
|
||||
lines.push({ type: 'question', text: '', choices: ['', ''], correct_index: 0, reward_type: 'score', reward_amount: 0 });
|
||||
renderQuestDialogueModalBody(quest.id);
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
function questUpdateQuestionText(status, index, text) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
quest.dialogues[status][index].text = text;
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
// Ajuste le NOMBRE de choix (2 à 8, voir db/quests/sanitize_quest_dialogues.py
|
||||
// ::_MIN_CHOICES/_MAX_CHOICES) : ajoute des choix vides ou retire les
|
||||
// derniers, sans jamais toucher aux choix déjà remplis qui restent dans
|
||||
// la nouvelle taille. Recadre la bonne réponse sur le premier choix si
|
||||
// elle pointait vers un choix retiré.
|
||||
function questSetQuestionChoiceCount(status, index, value) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
var line = quest.dialogues[status][index];
|
||||
var count = Math.max(2, Math.min(8, parseInt(value, 10) || 2));
|
||||
while (line.choices.length < count) line.choices.push('');
|
||||
while (line.choices.length > count) line.choices.pop();
|
||||
if (line.correct_index >= line.choices.length) line.correct_index = 0;
|
||||
renderQuestDialogueModalBody(quest.id);
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
function questSetQuestionChoiceText(status, index, choiceIndex, text) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
quest.dialogues[status][index].choices[choiceIndex] = text;
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
function questSetQuestionCorrectIndex(status, index, choiceIndex) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
quest.dialogues[status][index].correct_index = choiceIndex;
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
function questSetQuestionRewardAmount(status, index, value) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
quest.dialogues[status][index].reward_amount = Math.max(0, parseInt(value, 10) || 0);
|
||||
questSaveDialogues(quest);
|
||||
}
|
||||
|
||||
function questSetBubbleSpeaker(status, index, speaker) {
|
||||
var quest = questById(_currentQuestModalId());
|
||||
if (!quest) return;
|
||||
|
||||
+28
-1
@@ -854,7 +854,9 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
CETTE zone défile (flex:1 + overflow-y:auto) — le titre de colonne et
|
||||
le bouton "+ Réplique" restent fixes, en haut/en bas. */
|
||||
.questDialogueColumnBubbles{ display:flex; flex-direction:column; align-items:center; flex:1; min-height:0; overflow-y:auto; padding-top:4px; }
|
||||
.questDialogueAddBtn{ flex:0 0 auto; align-self:center; padding:6px 14px; font-size:12px; margin-top:4px; }
|
||||
.questDialogueColumnButtons{ flex:0 0 auto; display:flex; gap:8px; justify-content:center; margin-top:4px; }
|
||||
.questDialogueAddBtn{ align-self:center; padding:6px 14px; font-size:12px; }
|
||||
.questQuestionAddBtn{ background:linear-gradient(135deg, #e0b93d, #b8890f); }
|
||||
|
||||
.questBubbleRow{ display:flex; justify-content:center; width:100%; position:relative; }
|
||||
.questBubbleRow:not(:first-child)::before{
|
||||
@@ -874,6 +876,31 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
.questBubble-pnj .questBubbleBody{ background:rgba(47,111,237,.16); }
|
||||
.questBubble-joueur .questBubbleHeader{ background:#1e9e57; }
|
||||
.questBubble-joueur .questBubbleBody{ background:rgba(46,204,113,.16); }
|
||||
/* "❓ Question" (voir questQuestionBubbleHtml, static/js/quests/
|
||||
quest-editor.js) — bulle JAUNE distincte des répliques de dialogue,
|
||||
même chaîne/alignement vertical (.questBubbleRow). */
|
||||
.questBubble-question .questBubbleHeader{ background:#b8890f; flex-wrap:wrap; row-gap:4px; }
|
||||
.questBubble-question .questBubbleBody{ background:rgba(224,185,61,.14); }
|
||||
.questQuestionHeaderField{
|
||||
display:inline-flex; align-items:center; gap:4px; font-size:11px; font-weight:700; color:#fff; white-space:nowrap;
|
||||
}
|
||||
.questQuestionHeaderField input{
|
||||
width:44px; background:rgba(0,0,0,.25); border:1px solid rgba(255,255,255,.3); border-radius:4px;
|
||||
color:#fff; font:inherit; font-size:11px; padding:2px 4px;
|
||||
}
|
||||
.questQuestionText{
|
||||
width:100%; background:rgba(0,0,0,.2); border:1px solid var(--border); border-radius:6px;
|
||||
color:#fff; font:inherit; font-size:13px; padding:6px 8px; margin-bottom:8px;
|
||||
}
|
||||
.questQuestionText::placeholder{ color:rgba(255,255,255,.5); }
|
||||
.questChoicesList{ display:flex; flex-direction:column; gap:6px; }
|
||||
.questChoiceRow{ display:flex; align-items:center; gap:6px; }
|
||||
.questChoiceRadio{ accent-color:#e0b93d; flex-shrink:0; }
|
||||
.questChoiceInput{
|
||||
flex:1; background:rgba(0,0,0,.2); border:1px solid var(--border); border-radius:6px;
|
||||
color:#fff; font:inherit; font-size:12.5px; padding:4px 8px;
|
||||
}
|
||||
.questChoiceInput::placeholder{ color:rgba(255,255,255,.5); }
|
||||
.questBubbleSpeaker{
|
||||
font-size:11px; font-weight:700; color:#fff; cursor:pointer;
|
||||
background:transparent; border:none; padding:0; font-family:inherit; max-width:100%; flex:1;
|
||||
|
||||
+32
-6
@@ -86,19 +86,45 @@ def test_sanitize_quest_dialogues_keeps_only_valid_lines_per_status():
|
||||
}
|
||||
cleaned = db.sanitize_quest_dialogues(raw)
|
||||
assert cleaned == {
|
||||
"nouvelle": [{"speaker": "Le Sorcier", "text": "Bonjour !"}],
|
||||
"nouvelle": [{"type": "dialogue", "speaker": "Le Sorcier", "text": "Bonjour !"}],
|
||||
"en_cours": [],
|
||||
"terminee": [],
|
||||
}
|
||||
|
||||
|
||||
def test_sanitize_quest_dialogues_accepts_a_valid_question_and_rejects_malformed_ones():
|
||||
""""❓ Question" (voir static/js/quests/quest-editor.js) : une bulle
|
||||
jaune avec plusieurs choix, une seule bonne réponse, une récompense —
|
||||
même chaîne qu'une réplique de dialogue, un autre "type" de ligne."""
|
||||
raw = {
|
||||
"nouvelle": [
|
||||
{"type": "question", "text": "2+2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "reward_type": "score", "reward_amount": 2},
|
||||
{"type": "question", "text": "Une seule option", "choices": ["seule"], "correct_index": 0}, # < 2 choix -> rejetée
|
||||
{"type": "question", "text": "index hors bornes", "choices": ["a", "b"], "correct_index": 5}, # rejetée
|
||||
{"type": "question", "text": " ", "choices": ["a", "b"], "correct_index": 0}, # texte vide -> rejetée
|
||||
{"type": "question", "text": "type de récompense inconnu", "choices": ["a", "b"], "correct_index": 0, "reward_type": "vie"},
|
||||
],
|
||||
"en_cours": [], "terminee": [],
|
||||
}
|
||||
cleaned = db.sanitize_quest_dialogues(raw)
|
||||
assert cleaned["nouvelle"] == [
|
||||
{"type": "question", "text": "2+2 ?", "choices": ["3", "4", "5"], "correct_index": 1, "reward_type": "score", "reward_amount": 2},
|
||||
# reward_type invalide -> repli sur le premier type connu ("score"), jamais rejetée pour ça seul
|
||||
{"type": "question", "text": "type de récompense inconnu", "choices": ["a", "b"], "correct_index": 0, "reward_type": "score", "reward_amount": 0},
|
||||
]
|
||||
|
||||
|
||||
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 !"}],
|
||||
"nouvelle": [
|
||||
{"type": "dialogue", "speaker": "pnj", "text": "Peux-tu m'aider ?"},
|
||||
{"type": "dialogue", "speaker": "joueur", "text": "Bien sûr !"},
|
||||
{"type": "question", "text": "2+2 ?", "choices": ["3", "4"], "correct_index": 1, "reward_type": "score", "reward_amount": 2},
|
||||
],
|
||||
"en_cours": [{"type": "dialogue", "speaker": "pnj", "text": "Alors, cette clé ?"}],
|
||||
"terminee": [{"type": "dialogue", "speaker": "pnj", "text": "Merci beaucoup !"}],
|
||||
}
|
||||
db.set_quest_dialogues(slug, quest["id"], dialogues)
|
||||
reloaded = db.get_quest(slug, quest["id"])
|
||||
@@ -159,8 +185,8 @@ def test_quest_dialogues_route_sanitizes_and_persists(client, tmp_game_slug_clea
|
||||
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 !"}]
|
||||
assert resp.get_json()["dialogues"]["nouvelle"] == [{"type": "dialogue", "speaker": "pnj", "text": "Salut !"}]
|
||||
assert db.get_quest(slug, quest["id"])["dialogues"]["nouvelle"] == [{"type": "dialogue", "speaker": "pnj", "text": "Salut !"}]
|
||||
|
||||
|
||||
def test_quest_dialogues_route_404s_for_missing_quest(client, game):
|
||||
|
||||
Reference in New Issue
Block a user