Quêtes/collision : personnages nommés, dialogues qui parlent, boîte de dialogue en jeu
- "ℹ️ 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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
390ee831ca
commit
2347a70c45
@@ -88,6 +88,8 @@ from .rendering.collision_rules import (
|
||||
ACTION_TYPES, TRIGGER_TYPES, DEFAULT_PERIMETRE_PX,
|
||||
)
|
||||
from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS, sanitize_extra_commands
|
||||
from .rendering.personnage_names import list_personnage_names
|
||||
from .rendering.dialogue_box_style import resolve_dialogue_box_style, sanitize_dialogue_box_style, DEFAULT_DIALOGUE_BOX_STYLE, FONT_WEIGHT_CHOICES
|
||||
|
||||
from .screens_repo.ensure_schema import ensure_schema
|
||||
from .screens_repo.list_screens import list_screens
|
||||
@@ -125,6 +127,8 @@ from .scenes.set_scene_object_commands import set_scene_object_commands
|
||||
from .scenes.set_scene_object_role import set_scene_object_role
|
||||
from .scenes.set_scene_object_collision import set_scene_object_collision
|
||||
from .scenes.set_scene_object_collision_rules import set_scene_object_collision_rules
|
||||
from .scenes.set_scene_object_name import set_scene_object_name
|
||||
from .scenes.set_scene_object_dialogue_box_style import set_scene_object_dialogue_box_style
|
||||
from .scenes.render_scene_object import render_scene_object
|
||||
from .scenes.flow_palette import TRIGGER_EVENTS_2D, ACTION_TYPE_LABELS_2D, OBJECT_ACTION_PROPERTIES
|
||||
from .elements.element_label import element_label
|
||||
|
||||
@@ -12,6 +12,7 @@ from ..rendering.personnage_commands import resolve_personnage_commands
|
||||
from ..rendering.personnage_role import resolve_personnage_role
|
||||
from ..rendering.collision_settings import resolve_collision_settings
|
||||
from ..rendering.collision_rules import resolve_collision_rules
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
from ..scenes.render_scene_object import render_scene_object
|
||||
|
||||
@@ -57,6 +58,13 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
if o["kind"] == "personnage":
|
||||
o["personnage_commandes"] = resolve_personnage_commands(o)
|
||||
o["personnage_role"] = resolve_personnage_role(o)
|
||||
if o["kind"] == "dialogue_box":
|
||||
# Widget d'interface (voir "🖥️ Interface",
|
||||
# screens/rendering/dialogue_box_style.py) : ni
|
||||
# collision, ni cible/source de règle — juste son
|
||||
# style, lu par static/js/play/dialogue-box-controller.js.
|
||||
o["dialogue_box_style"] = resolve_dialogue_box_style(o)
|
||||
continue
|
||||
# Boîte de collision (voir static/js/play/conditions.js::
|
||||
# elementsOverlap) : posée sur TOUT objet de scène, pas
|
||||
# seulement "personnage" — un décor/fond peut aussi servir
|
||||
@@ -198,9 +206,18 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED):
|
||||
# ci-dessus sont déjà indexés par id, jamais par nom.
|
||||
definition_names = {str(d["id"]): d["name"] for d in definitions}
|
||||
|
||||
# Quêtes (voir db/quests/, onglet "🗺️ Quêtes") : GAME-WIDE, jamais
|
||||
# rattachées à un écran comme "elements" ci-dessus — nécessaires ici
|
||||
# pour que static/js/play/dialogue-box-controller.js puisse retrouver,
|
||||
# à l'exécution d'une action "quete" (collision-rules-controller.js),
|
||||
# le dialogue de la colonne correspondant au STATUT ACTUEL de la
|
||||
# quête (nouvelle/en_cours/terminee) sans aller-retour serveur.
|
||||
quests = db.list_quests(slug)
|
||||
|
||||
return {
|
||||
"screens": payload_screens, "flows": flows, "animations": animations,
|
||||
"element_types": element_types, "data": data, "fields_meta": fields_meta,
|
||||
"variables": variables, "personnage_animations": personnage_animations,
|
||||
"definition_names": definition_names, "element_type_templates": element_type_templates,
|
||||
"quests": quests,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import json
|
||||
|
||||
# Widget d'interface "💬 Boîte de dialogue" (voir "🖥️ Interface",
|
||||
# templates/scene_edit.html, screens/scenes/add_scene_object.py kind=
|
||||
# "dialogue_box") : affiche AUTOMATIQUEMENT, en jeu, la réplique en cours
|
||||
# d'une quête déclenchée par une règle de collision "quete" (voir
|
||||
# static/js/play/dialogue-box-controller.js) — header (nom du
|
||||
# personnage qui parle), corps (texte), pied (bouton "Suivant"),
|
||||
# entièrement stylisable par l'auteur.
|
||||
DEFAULT_DIALOGUE_BOX_STYLE = {
|
||||
"font_family": "inherit",
|
||||
"font_size": 14,
|
||||
"font_weight": "normal",
|
||||
"text_color": "#ffffff",
|
||||
"header_bg": "#2f6fed",
|
||||
"body_bg": "#1a1d24",
|
||||
"footer_bg": "#11141b",
|
||||
}
|
||||
|
||||
FONT_WEIGHT_CHOICES = ["normal", "bold"]
|
||||
|
||||
|
||||
def sanitize_dialogue_box_style(raw_style):
|
||||
style = dict(DEFAULT_DIALOGUE_BOX_STYLE)
|
||||
if not isinstance(raw_style, dict):
|
||||
return style
|
||||
if isinstance(raw_style.get("font_family"), str) and raw_style["font_family"].strip():
|
||||
style["font_family"] = raw_style["font_family"].strip()[:60]
|
||||
try:
|
||||
style["font_size"] = max(8, min(48, int(raw_style.get("font_size"))))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if raw_style.get("font_weight") in FONT_WEIGHT_CHOICES:
|
||||
style["font_weight"] = raw_style["font_weight"]
|
||||
for key in ("text_color", "header_bg", "body_bg", "footer_bg"):
|
||||
value = raw_style.get(key)
|
||||
if isinstance(value, str) and value.strip():
|
||||
style[key] = value.strip()[:20]
|
||||
return style
|
||||
|
||||
|
||||
def resolve_dialogue_box_style(obj):
|
||||
raw = (obj.get("attributes") or {}).get("_dialogue_box_style")
|
||||
if not raw:
|
||||
return dict(DEFAULT_DIALOGUE_BOX_STYLE)
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return dict(DEFAULT_DIALOGUE_BOX_STYLE)
|
||||
return sanitize_dialogue_box_style(parsed)
|
||||
@@ -0,0 +1,19 @@
|
||||
from ..screens_repo.list_screens import list_screens
|
||||
from ..scenes.list_scene_objects import list_scene_objects
|
||||
|
||||
|
||||
def list_personnage_names(slug):
|
||||
"""Noms distincts de tous les personnages nommés (voir "ℹ️
|
||||
Informations", templates/scene_edit.html), toutes scènes 2D du jeu
|
||||
confondues — une quête (voir db/quests/) est GAME-WIDE, son dialogue
|
||||
peut donc mettre en scène un PNJ posé sur une autre scène que celle
|
||||
où la règle de collision se déclenche. Ordre alphabétique, jamais de
|
||||
doublon."""
|
||||
names = set()
|
||||
for s in list_screens(slug):
|
||||
if s["kind"] != "jeu_2d":
|
||||
continue
|
||||
for o in list_scene_objects(slug, s["id"]):
|
||||
if o["kind"] == "personnage" and o.get("name"):
|
||||
names.add(o["name"])
|
||||
return sorted(names)
|
||||
@@ -5,7 +5,11 @@ import db
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
_DEFAULT_PERSONNAGE_DATA = {"source": "forge", "forge_character": "male-adventurer"}
|
||||
_KINDS = {"personnage", "decor", "fond"}
|
||||
_KINDS = {"personnage", "decor", "fond", "dialogue_box"}
|
||||
# Widget d'interface (voir "🖥️ Interface", screens/rendering/dialogue_box_style.py) :
|
||||
# assez grand pour lire une réplique sans redimensionner, jamais confondu
|
||||
# visuellement avec un personnage/décor (128×128 par défaut).
|
||||
_DIALOGUE_BOX_DEFAULT_SIZE = (320, 150)
|
||||
|
||||
|
||||
def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, background_slug=None):
|
||||
@@ -40,6 +44,8 @@ def add_scene_object(slug, scene_id, kind="personnage", forge_character=None, ba
|
||||
if background:
|
||||
attributes["src"] = background["url"]
|
||||
width, height = background["width"], background["height"]
|
||||
elif kind == "dialogue_box":
|
||||
width, height = _DIALOGUE_BOX_DEFAULT_SIZE
|
||||
conn = db.connect(slug)
|
||||
if kind == "fond":
|
||||
z_index = -1000
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import html as html_lib
|
||||
|
||||
from ..rendering.personnage_data import resolve_personnage_idle_frame
|
||||
from ..rendering.dialogue_box_style import resolve_dialogue_box_style
|
||||
|
||||
|
||||
def render_scene_object(obj):
|
||||
@@ -39,6 +40,31 @@ def render_scene_object(obj):
|
||||
# remplirait quand même son cadre (width/height:100%, voir la règle
|
||||
# `.canvasElementInner > img` dans scene_edit.html) mais DÉFORMÉE,
|
||||
# rendant le personnage minuscule/écrasé dans sa boîte de sélection.
|
||||
if obj["kind"] == "dialogue_box":
|
||||
# Widget d'interface (voir "🖥️ Interface" + screens/rendering/
|
||||
# dialogue_box_style.py) : un <div> à 3 zones, pas un <img> — voir
|
||||
# static/js/play/dialogue-box-controller.js pour comment header/
|
||||
# body/footer sont remplis EN JEU (data-dialogue-role sert de
|
||||
# sélecteur), masqué par défaut au jeu (voir static/style.css,
|
||||
# `.playScreen .dialogueBoxWidget`) jusqu'à ce qu'une règle de
|
||||
# collision "quete" ait un dialogue à afficher. Toujours VISIBLE
|
||||
# dans l'éditeur (avec un texte d'exemple) pour pouvoir le
|
||||
# positionner/styliser.
|
||||
style = resolve_dialogue_box_style(obj)
|
||||
box_style = (
|
||||
f"position:absolute; left:{obj['x']}px; top:{obj['y']}px; "
|
||||
f"width:{obj['width']}px; height:{obj['height']}px; z-index:{obj['z_index']}; "
|
||||
f"font-family:{html_lib.escape(style['font_family'])}; font-size:{style['font_size']}px; "
|
||||
f"font-weight:{style['font_weight']}; color:{html_lib.escape(style['text_color'])};"
|
||||
)
|
||||
return (
|
||||
f'<div class="dialogueBoxWidget" data-object-id="{obj["id"]}" data-element-id="{obj["id"]}" style="{box_style}">'
|
||||
f'<div class="dialogueBoxHeader" data-dialogue-role="header" style="background:{html_lib.escape(style["header_bg"])}">Nom du personnage</div>'
|
||||
f'<div class="dialogueBoxBody" data-dialogue-role="body" style="background:{html_lib.escape(style["body_bg"])}">Le texte du dialogue s\'affiche ici.</div>'
|
||||
f'<div class="dialogueBoxFooter" style="background:{html_lib.escape(style["footer_bg"])}">'
|
||||
f'<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" onclick="forgeDialogueBoxAdvance({obj["id"]})">Suivant →</button>'
|
||||
f'</div></div>'
|
||||
)
|
||||
if obj["kind"] == "personnage":
|
||||
src = resolve_personnage_idle_frame(obj)
|
||||
src_attr = f' src="{html_lib.escape(src)}"' if src else ""
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import json
|
||||
|
||||
import db
|
||||
|
||||
from ..rendering.dialogue_box_style import sanitize_dialogue_box_style
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_dialogue_box_style(slug, object_id, raw_style):
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT attributes FROM _scene_objects WHERE id = ?", (object_id,)).fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return None
|
||||
attributes = json.loads(row["attributes"] or "{}")
|
||||
style = sanitize_dialogue_box_style(raw_style)
|
||||
attributes["_dialogue_box_style"] = json.dumps(style)
|
||||
conn.execute("UPDATE _scene_objects SET attributes = ? WHERE id = ?", (json.dumps(attributes), object_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return style
|
||||
@@ -0,0 +1,18 @@
|
||||
import db
|
||||
|
||||
from .ensure_scene_schema import ensure_scene_schema
|
||||
|
||||
|
||||
def set_scene_object_name(slug, object_id, name):
|
||||
"""Enregistre le nom d'un objet de scène (colonne `name`, voir
|
||||
ensure_scene_schema.py — déjà utilisée comme LIBELLÉ d'affichage
|
||||
partout ailleurs, mais jamais éditable avant le panneau "ℹ️
|
||||
Informations", templates/scene_edit.html). Pour un personnage, ce nom
|
||||
est aussi celui proposé comme "qui parle" dans l'éditeur de dialogue
|
||||
de quête (voir screens/rendering/personnage_names.py, static/js/
|
||||
quests/quest-editor.js)."""
|
||||
ensure_scene_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute("UPDATE _scene_objects SET name = ? WHERE id = ?", ((name or "").strip() or None, object_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user