Files
Forge-Engine/routes/scenes/scene_edit_view.py
T
williamandClaude Sonnet 5 2347a70c45
Build and deploy / test-python (push) Successful in 11m32s
Build and deploy / test-js (push) Successful in 1m12s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
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>
2026-09-03 11:20:58 +02:00

148 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from flask import render_template, request, abort, g
import db
import screens
def render_scene_edit(slug, screen_id, game):
"""Vue de l'éditeur de scène 2D — appelée par routes/screens/
screen_edit.py (le dispatcher, selon screen["kind"]), PAS une route
Flask elle-même : même URL/endpoint "screen_edit" pour les deux types
d'écran. Réutilise TEL QUEL tout ce qui est déjà générique côté logique
de flow/animation/variables/événements (voir screens/screens_repo/
ensure_schema.py — le type d'écran est une propriété PAR ÉCRAN) —
seuls les objets de scène et la palette de nœuds proposée diffèrent de
l'éditeur document."""
screen = screens.get_screen(slug, screen_id)
if not screen:
abort(404)
objects = screens.list_scene_objects(slug, screen_id)
for o in objects:
o["rendered_html"] = screens.render_scene_object(o)
selected_id = request.args.get("selected", type=int)
personnage_data = None
personnage_commands = None
personnage_role = None
collision_settings = None
dialogue_box_style = None
selected_obj = next((o for o in objects if o["id"] == selected_id), None) if selected_id else None
if selected_obj:
if selected_obj["kind"] != "dialogue_box":
collision_settings = screens.resolve_collision_settings(selected_obj)
if selected_obj["kind"] == "personnage":
personnage_data = screens.resolve_personnage_data(selected_obj)
personnage_commands = screens.resolve_personnage_commands(selected_obj)
personnage_role = screens.resolve_personnage_role(selected_obj)
elif selected_obj["kind"] == "dialogue_box":
dialogue_box_style = screens.resolve_dialogue_box_style(selected_obj)
_DEFAULT_OBJECT_LABELS = {"personnage": "Personnage", "fond": "Fond"}
object_labels = {
o["id"]: (o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"))
for o in objects
}
element_animations_map = {
o["id"]: screens.resolve_personnage_animations(o) for o in objects if o["kind"] == "personnage"
}
# "🧩 Collision" (voir templates/scene_edit.html) : une carte par objet
# ÉLIGIBLE (jamais un "fond", jamais le personnage "joueur" — ce sont
# TOUJOURS les AUTRES objets qui réagissent à SA présence, voir
# screens/rendering/collision_rules.py) — construit ici plutôt que
# réutiliser `objects` tel quel, pour ne poser resolve_personnage_role
# qu'une fois par objet personnage (déjà lu ci-dessus pour l'objet
# SÉLECTIONNÉ, mais nécessaire ici pour TOUS les personnages).
collision_rule_objects = []
for o in objects:
if o["kind"] in ("fond", "dialogue_box"):
continue
role = screens.resolve_personnage_role(o) if o["kind"] == "personnage" else None
if role == "joueur":
continue
collision_rule_objects.append({
"id": o["id"],
"name": o.get("name") or _DEFAULT_OBJECT_LABELS.get(o["kind"], "Décor"),
"kind": o["kind"],
"role": role,
"thumbnail_html": o["rendered_html"],
"rules": screens.resolve_collision_rules(o),
})
flow_blocks = screens.list_flow_blocks(slug, screen_id)
flow_nodes = screens.list_flow_nodes(slug, screen_id)
flow_edges = screens.list_flow_edges(slug, screen_id)
animation_clips = screens.list_animation_clips(slug, screen_id)
global_variables = db.list_global_variables(slug)
custom_events = db.list_custom_events(slug)
custom_event_usages = screens.list_custom_event_usages(slug)
custom_events_map = {e["id"]: {"name": e["name"]} for e in custom_events}
all_screens = screens.list_screens(slug)
screens_map = {s["id"]: s["name"] for s in all_screens}
blocks_view = screens.blocks_view(flow_blocks, flow_nodes, object_labels)
# Sprites animaux CraftPix (screens.ADMIN_SPRITE_LIBRARY) : réservés au
# rôle admin (licence CraftPix, voir core/sprite_gate.py et le plan
# "Bibliothèque de sprites animaux CraftPix").
sprite_library = screens.SPRITE_LIBRARY if g.current_user["role"] == "admin" else screens.PUBLIC_SPRITE_LIBRARY
sprite_library_families = screens.sprite_gallery_families(sprite_library)
# Images de fond CraftPix (screens.BACKGROUND_LIBRARY) : même règle
# d'accès que les sprites animaux — réservées au rôle admin.
background_library = screens.BACKGROUND_LIBRARY if g.current_user["role"] == "admin" else {}
return render_template(
"scene_edit.html",
game=game, screen=screen, all_screens=all_screens,
objects=objects, selected_id=selected_id, personnage_data=personnage_data,
personnage_commands=personnage_commands or screens.DEFAULT_PERSONNAGE_COMMANDS,
personnage_role=personnage_role or screens.DEFAULT_PERSONNAGE_ROLE,
personnage_role_choices=screens.PERSONNAGE_ROLES, personnage_role_labels=screens.PERSONNAGE_ROLE_LABELS,
collision_settings=collision_settings or screens.DEFAULT_COLLISION_SETTINGS,
dialogue_box_style=dialogue_box_style or screens.DEFAULT_DIALOGUE_BOX_STYLE,
dialogue_box_font_weight_choices=screens.FONT_WEIGHT_CHOICES,
sprite_library=sprite_library, sprite_library_families=sprite_library_families,
sprite_library_json=json.dumps(sprite_library),
background_library=background_library,
element_animations_map_json=json.dumps(element_animations_map),
element_orientation_values=screens.ELEMENT_ORIENTATION_VALUES,
element_orientation_labels_json=json.dumps(screens.ELEMENT_ORIENTATION_LABELS),
element_visibility_values=screens.ELEMENT_VISIBILITY_VALUES,
element_visibility_labels_json=json.dumps(screens.ELEMENT_VISIBILITY_LABELS),
trigger_events=screens.TRIGGER_EVENTS_2D, action_type_labels=screens.ACTION_TYPE_LABELS_2D,
object_action_properties=screens.OBJECT_ACTION_PROPERTIES,
element_action_property_labels_json=json.dumps(dict(screens.OBJECT_ACTION_PROPERTIES)),
condition_operators=screens.CONDITION_OPERATORS,
data_operations=screens.DATA_OPERATIONS, data_operation_labels=screens.DATA_OPERATION_LABELS,
score_status_choices=db.SCORE_STATUS_CHOICES,
score_status_labels_json=json.dumps(db.SCORE_STATUS_LABELS),
data_operation_labels_json=json.dumps(screens.DATA_OPERATION_LABELS),
condition_operator_labels_json=json.dumps(screens.CONDITION_OPERATOR_LABELS),
action_type_labels_json=json.dumps(screens.ACTION_TYPE_LABELS_2D),
elements_labels_json=json.dumps(object_labels), screens_map_json=json.dumps(screens_map),
flow_nodes=flow_nodes, flow_edges=flow_edges,
flow_nodes_json=json.dumps(flow_nodes), flow_edges_json=json.dumps(flow_edges),
flow_blocks_json=json.dumps(flow_blocks), blocks_view=blocks_view,
animation_clips=animation_clips, animation_clips_json=json.dumps(animation_clips),
animate_css_catalog=screens.ANIMATE_CSS_CATALOG, animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
global_variables=global_variables,
custom_events=custom_events, custom_event_usages=custom_event_usages,
custom_events_map_json=json.dumps(custom_events_map),
definitions=db.list_definitions(slug),
definitions_json=json.dumps({str(d["id"]): screens.data_definition_options(slug, d["id"]) for d in db.list_definitions(slug)}),
definitions_names_json=json.dumps({d["id"]: d["name"] for d in db.list_definitions(slug)}),
collision_rule_objects_json=json.dumps(collision_rule_objects),
collision_action_types=screens.ACTION_TYPES, collision_trigger_types=screens.TRIGGER_TYPES,
collision_default_perimetre_px=screens.DEFAULT_PERIMETRE_PX,
# Éditeur de quêtes (voir db/quests/) : préchargées ici pour que
# collisionRuleActionSummary (collision-rules-editor.js) affiche
# tout de suite le TITRE de la quête référencée par une règle
# existante, sans attendre l'ouverture de l'assistant "+ Action"
# (seul moment où questPickerHtml irait la chercher sinon).
quests_json=json.dumps(db.list_quests(slug)),
# "qui parle" dans le dialogue d'une quête (voir "ℹ️ Informations",
# static/js/quests/quest-editor.js) : les noms donnés aux
# personnages, toutes scènes du jeu confondues.
personnage_names_json=json.dumps(screens.list_personnage_names(slug)),
)