Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
Supprime le concept de "quête" au profit d'un onglet unique "Déclencheurs"
portant toute la logique (dialogue, condition, marquage terminé) directement
sur l'objet de scène. Ajoute une nouvelle action "Modifier une variable"
(réutilisant le vocabulaire du graphe de flow) utilisable après une
collision, une interaction ou une branche de condition, ainsi qu'un
chaînage d'actions ("then") permettant d'enchaîner plusieurs actions à la
suite et d'étendre un déclencheur déjà posé sans le recréer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c504ace167
commit
d664ed5637
+2
-14
@@ -84,17 +84,7 @@ from .custom_events.get_custom_event import get_custom_event
|
||||
from .custom_events.create_custom_event import create_custom_event
|
||||
from .custom_events.update_custom_event import update_custom_event
|
||||
|
||||
from .quests.constants import (
|
||||
QUEST_STATUS_CHOICES, QUEST_STATUS_LABELS, QUEST_RESULT_CHOICES, QUEST_RESULT_LABELS,
|
||||
)
|
||||
from .quests.list_quests import list_quests
|
||||
from .quests.get_quest import get_quest
|
||||
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, QUESTION_REWARD_TYPES
|
||||
from .quests.reward_budget import quest_dialogues_total_reward
|
||||
from .dialogue_lines import sanitize_dialogue_lines, QUESTION_REWARD_TYPES, sum_question_rewards
|
||||
|
||||
__all__ = [
|
||||
"PROJECTS_DIR", "FIELD_TYPES", "GLOBAL_VARIABLE_TYPES", "PLAYER_SHARED",
|
||||
@@ -113,7 +103,5 @@ __all__ = [
|
||||
"update_global_variable_value", "update_global_variable", "delete_global_variable",
|
||||
"delete_global_variable_by_id",
|
||||
"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", "QUESTION_REWARD_TYPES", "quest_dialogues_total_reward",
|
||||
"sanitize_dialogue_lines", "QUESTION_REWARD_TYPES", "sum_question_rewards",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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) :
|
||||
# Question à choix (voir "❓ Question", static/js/triggers/trigger-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_*/
|
||||
@@ -61,22 +59,31 @@ def _sanitize_line(raw_line):
|
||||
return _sanitize_dialogue_line(raw_line)
|
||||
|
||||
|
||||
def sanitize_quest_dialogues(raw_dialogues):
|
||||
"""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
|
||||
for status in QUEST_STATUS_CHOICES:
|
||||
raw_lines = raw_dialogues.get(status)
|
||||
if not isinstance(raw_lines, list):
|
||||
continue
|
||||
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
|
||||
def sanitize_dialogue_lines(raw_lines):
|
||||
"""Sanitize une LISTE 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. Les lignes d'UN
|
||||
dialogue embarqué dans une action "dialogue" (voir
|
||||
screens/rendering/collision_rules.py) : chaque occurrence de
|
||||
"Déclencher ce dialogue" porte le sien."""
|
||||
if not isinstance(raw_lines, list):
|
||||
return []
|
||||
cleaned = [_sanitize_line(raw_line) for raw_line in raw_lines[:_MAX_LINES_PER_COLUMN]]
|
||||
return [line for line in cleaned if line]
|
||||
|
||||
|
||||
def sum_question_rewards(lines):
|
||||
"""Somme des récompenses de toutes les questions ("❓ Question" ci-
|
||||
dessus) d'UNE liste de répliques/questions — utilisée pour calculer le
|
||||
score max possible du quiz à l'export SCORM, sur TOUTES les lignes de
|
||||
TOUS les dialogues du jeu (voir screens.collect_all_dialogue_lines,
|
||||
publish/build_scorm_package.py)."""
|
||||
total = 0
|
||||
for line in (lines or []):
|
||||
if isinstance(line, dict) and line.get("type") == "question":
|
||||
total += line.get("reward_amount", 0) or 0
|
||||
return total
|
||||
@@ -1,4 +0,0 @@
|
||||
QUEST_STATUS_CHOICES = ["nouvelle", "en_cours", "terminee"]
|
||||
QUEST_STATUS_LABELS = {"nouvelle": "Nouvelle", "en_cours": "En cours", "terminee": "Terminée"}
|
||||
QUEST_RESULT_CHOICES = ["reussi", "echoue"]
|
||||
QUEST_RESULT_LABELS = {"reussi": "Réussi", "echoue": "Échoué"}
|
||||
@@ -1,18 +0,0 @@
|
||||
from ..connection import connect
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
from .get_quest import get_quest
|
||||
|
||||
|
||||
def create_quest(slug, title="Nouvelle quête"):
|
||||
"""Titre par défaut plutôt qu'un refus — créée d'un clic depuis la
|
||||
liste (voir l'onglet "🗺️ Quêtes", templates/scene_edit.html) ou depuis l'assistant "+ Action"
|
||||
de l'éditeur de collision (voir collision-rules-editor.js), avant
|
||||
même que l'auteur ait choisi un titre."""
|
||||
ensure_quests_schema(slug)
|
||||
title = (title or "").strip() or "Nouvelle quête"
|
||||
conn = connect(slug)
|
||||
conn.execute("INSERT INTO _quests (title) VALUES (?)", (title,))
|
||||
new_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return get_quest(slug, new_id)
|
||||
@@ -1,10 +0,0 @@
|
||||
from ..connection import connect
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
|
||||
|
||||
def delete_quest(slug, quest_id):
|
||||
ensure_quests_schema(slug)
|
||||
conn = connect(slug)
|
||||
conn.execute("DELETE FROM _quests WHERE id = ?", (quest_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -1,29 +0,0 @@
|
||||
from ..connection import connect
|
||||
|
||||
|
||||
def ensure_quests_schema(slug):
|
||||
"""Migration légère (même principe que ensure_custom_events_schema.py) :
|
||||
crée _quests si absente. Une quête vit pour TOUT le jeu (pas par écran),
|
||||
référencée par id technique depuis l'action "quete" d'une règle de
|
||||
collision (voir screens/rendering/collision_rules.py, quete_id). Les
|
||||
dialogues (voir static/js/quests/quest-editor.js — 3 colonnes
|
||||
nouvelle/en_cours/terminee) sont stockés en JSON plutôt qu'une table à
|
||||
part : jamais interrogés/filtrés côté SQL, toujours lus/réécrits en
|
||||
bloc pour UNE quête à la fois (même convention que _scene_objects.attributes)."""
|
||||
conn = connect(slug)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS _quests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
objectif TEXT DEFAULT '',
|
||||
recompense_score INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'nouvelle',
|
||||
resultat TEXT,
|
||||
dialogues_json TEXT DEFAULT '{}',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -1,11 +0,0 @@
|
||||
from ..connection import connect
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
from .list_quests import _row_to_quest
|
||||
|
||||
|
||||
def get_quest(slug, quest_id):
|
||||
ensure_quests_schema(slug)
|
||||
conn = connect(slug)
|
||||
row = conn.execute("SELECT * FROM _quests WHERE id = ?", (quest_id,)).fetchone()
|
||||
conn.close()
|
||||
return _row_to_quest(row) if row else None
|
||||
@@ -1,22 +0,0 @@
|
||||
import json
|
||||
|
||||
from ..connection import connect
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
|
||||
|
||||
def _row_to_quest(row):
|
||||
quest = dict(row)
|
||||
try:
|
||||
quest["dialogues"] = json.loads(quest.pop("dialogues_json") or "{}")
|
||||
except (TypeError, ValueError):
|
||||
quest["dialogues"] = {}
|
||||
quest.pop("dialogues_json", None)
|
||||
return quest
|
||||
|
||||
|
||||
def list_quests(slug):
|
||||
ensure_quests_schema(slug)
|
||||
conn = connect(slug)
|
||||
rows = conn.execute("SELECT * FROM _quests ORDER BY created_at DESC, id DESC").fetchall()
|
||||
conn.close()
|
||||
return [_row_to_quest(r) for r in rows]
|
||||
@@ -1,15 +0,0 @@
|
||||
def quest_dialogues_total_reward(dialogues):
|
||||
"""Somme des récompenses de toutes les questions ("❓ Question", voir
|
||||
sanitize_quest_dialogues.py), TOUTES colonnes/statuts confondus — une
|
||||
quête n'a qu'UNE SEULE récompense totale (quest["recompense_score"]),
|
||||
jamais une par statut. Utilisée pour empêcher d'enregistrer plus de
|
||||
points répartis entre les questions que la quête n'en offre (voir
|
||||
routes/quests/quest_dialogues.py et quest_update.py)."""
|
||||
total = 0
|
||||
for lines in (dialogues or {}).values():
|
||||
if not isinstance(lines, list):
|
||||
continue
|
||||
for line in lines:
|
||||
if isinstance(line, dict) and line.get("type") == "question":
|
||||
total += line.get("reward_amount", 0) or 0
|
||||
return total
|
||||
@@ -1,21 +0,0 @@
|
||||
import json
|
||||
|
||||
from ..connection import connect
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
from .get_quest import get_quest
|
||||
from .sanitize_quest_dialogues import sanitize_quest_dialogues
|
||||
|
||||
|
||||
def set_quest_dialogues(slug, quest_id, raw_dialogues):
|
||||
"""Toujours les 3 colonnes ENTIÈRES (nouvelle/en_cours/terminee) en un
|
||||
bloc — même convention "replace-all" que set_scene_object_collision_rules.py,
|
||||
jamais une réplique isolée."""
|
||||
ensure_quests_schema(slug)
|
||||
if get_quest(slug, quest_id) is None:
|
||||
return None
|
||||
dialogues = sanitize_quest_dialogues(raw_dialogues)
|
||||
conn = connect(slug)
|
||||
conn.execute("UPDATE _quests SET dialogues_json = ? WHERE id = ?", (json.dumps(dialogues), quest_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return dialogues
|
||||
@@ -1,42 +0,0 @@
|
||||
from ..connection import connect
|
||||
from .constants import QUEST_STATUS_CHOICES, QUEST_RESULT_CHOICES
|
||||
from .ensure_quests_schema import ensure_quests_schema
|
||||
from .get_quest import get_quest
|
||||
|
||||
|
||||
def update_quest(slug, quest_id, fields):
|
||||
"""Met à jour uniquement les clés reconnues et valides de `fields` —
|
||||
ignore tout le reste plutôt que de lever, même esprit que
|
||||
sanitize_collision_rules (jamais confiance dans ce qu'envoie le
|
||||
client). title vide => ignoré (garde l'ancien plutôt qu'un titre vide)."""
|
||||
ensure_quests_schema(slug)
|
||||
if get_quest(slug, quest_id) is None:
|
||||
return None
|
||||
updates = {}
|
||||
if "title" in fields:
|
||||
title = (fields["title"] or "").strip()
|
||||
if title:
|
||||
updates["title"] = title
|
||||
if "objectif" in fields:
|
||||
updates["objectif"] = (fields["objectif"] or "").strip()
|
||||
if "recompense_score" in fields:
|
||||
try:
|
||||
updates["recompense_score"] = int(fields["recompense_score"])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if "status" in fields and fields["status"] in QUEST_STATUS_CHOICES:
|
||||
updates["status"] = fields["status"]
|
||||
if "resultat" in fields:
|
||||
resultat = fields["resultat"]
|
||||
if resultat in QUEST_RESULT_CHOICES:
|
||||
updates["resultat"] = resultat
|
||||
elif resultat is None:
|
||||
updates["resultat"] = None
|
||||
if not updates:
|
||||
return get_quest(slug, quest_id)
|
||||
conn = connect(slug)
|
||||
set_clause = ", ".join(f"{key} = ?" for key in updates)
|
||||
conn.execute(f"UPDATE _quests SET {set_clause} WHERE id = ?", (*updates.values(), quest_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return get_quest(slug, quest_id)
|
||||
Reference in New Issue
Block a user