Éditeur de quêtes : titre/objectif/récompense/statut/résultat + dialogue à 3 colonnes
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:
co-authored by
Claude Sonnet 5
parent
4391b36d45
commit
51427b5297
@@ -78,6 +78,17 @@ 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, QUEST_SPEAKER_CHOICES,
|
||||
)
|
||||
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
|
||||
|
||||
__all__ = [
|
||||
"PROJECTS_DIR", "FIELD_TYPES", "GLOBAL_VARIABLE_TYPES", "PLAYER_SHARED",
|
||||
"SCORE_STATUS_CHOICES", "SCORE_STATUS_LABELS", "get_score", "set_score_value", "set_status",
|
||||
@@ -95,4 +106,7 @@ __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", "QUEST_SPEAKER_CHOICES",
|
||||
"list_quests", "get_quest", "create_quest", "update_quest", "delete_quest",
|
||||
"set_quest_dialogues", "sanitize_quest_dialogues",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
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é"}
|
||||
QUEST_SPEAKER_CHOICES = ["joueur", "pnj"]
|
||||
@@ -0,0 +1,18 @@
|
||||
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 templates/quests.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)
|
||||
@@ -0,0 +1,10 @@
|
||||
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()
|
||||
@@ -0,0 +1,29 @@
|
||||
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()
|
||||
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -0,0 +1,22 @@
|
||||
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]
|
||||
@@ -0,0 +1,30 @@
|
||||
from .constants import QUEST_STATUS_CHOICES, QUEST_SPEAKER_CHOICES
|
||||
|
||||
_MAX_LINES_PER_COLUMN = 200
|
||||
|
||||
|
||||
def sanitize_quest_dialogues(raw_dialogues):
|
||||
"""Une réplique = {speaker: 'joueur'|'pnj', text: '...'} — une colonne
|
||||
par statut de quête (voir QUEST_STATUS_CHOICES), la même forme que
|
||||
resolve_collision_rules.sanitize_collision_rules : rejette tout
|
||||
élément invalide plutôt que de lever, ne garde jamais un texte vide."""
|
||||
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 = []
|
||||
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 speaker not in QUEST_SPEAKER_CHOICES:
|
||||
continue
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
continue
|
||||
cleaned.append({"speaker": speaker, "text": text.strip()})
|
||||
result[status] = cleaned
|
||||
return result
|
||||
@@ -0,0 +1,21 @@
|
||||
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
|
||||
@@ -0,0 +1,42 @@
|
||||
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