Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
Build and deploy / test-python (push) Successful in 10m59s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance,
  video, son, visibilite, indication, attendre comme actions, utilisables
  aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par
  Ruby (IA), avec blocs deplacables/supprimables dans une chaine.
- Corrige plusieurs variantes du bug "impossible de poser un objet hors
  du champ de la camera" (troncature du chainage d'actions a 4 maillons,
  fond importe pose a 128x128 au lieu de sa taille reelle, decalage du
  fond au vrai glisser-depose, redimensionnement manuel jamais propage
  au monde).
- Ajoute un vrai glisser-depose depuis la galerie vers la scene, la
  gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/
  Videos, suppression, reclassement fond<->decor sans re-upload).
- Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement,
  limite 5 min), avec validation de la duree reelle du fichier, et une
  replique audio optionnelle dans une bulle de dialogue.
- Fixe la taille de pose d'un objet/decor importe a 200x200 avec une
  boite de collision de 150x150.
- Filtre le selecteur de fichier des actions son/video par type reel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-12 00:28:46 +02:00
co-authored by Claude Sonnet 5
parent b07b231a61
commit 559331f9cf
67 changed files with 4739 additions and 306 deletions
+66 -13
View File
@@ -37,25 +37,61 @@ _SYSTEM_PROMPT = (
"— jamais pour un personnage ou un sprite animé, la génération "
"d'image n'est pas fiable pour ça. "
"\n\n"
"CE QUE TU NE PEUX PAS FAIRE : tes outils ne pilotent QUE la logique "
"\"déclencheur de collision\" d'un objet de scène (dialogue, "
"variable, interagir, condition). Forge Engine a un AUTRE système, "
"séparé, de \"flow\" (nœuds/liaisons) pour changer d'écran, les "
"minuteurs, jouer un son, une animation de sprite — tu n'as AUCUN "
"CE QUE TU NE PEUX PAS FAIRE : tes outils pilotent la logique "
"\"déclencheur -> action\" d'un objet de scène OU de l'écran entier "
"(voir DÉCLENCHEURS/ACTIONS ci-dessous). Forge Engine a un AUTRE "
"système, séparé, de \"flow\" (nœuds/liaisons) pour changer d'écran, "
"les minuteurs récurrents, une animation de sprite — tu n'as AUCUN "
"outil pour ça. Si une demande a besoin de cette partie-là (\"passer "
"à l'écran suivant\", \"jouer un son\", \"après 5 secondes\"...), fais "
"quand même tout ce que tes outils permettent, puis DIS CLAIREMENT "
"dans ta réponse texte ce que tu n'as pas pu faire et pourquoi "
"(cette partie doit être ajoutée à la main dans l'onglet \"Flow\") — "
"ne t'acharne JAMAIS à répéter des appels d'outils pour une chose "
"qu'aucun outil ne permet, ça n'aboutira jamais."
"à l'écran suivant\", \"après 5 secondes\"...), fais quand même tout "
"ce que tes outils permettent, puis DIS CLAIREMENT dans ta réponse "
"texte ce que tu n'as pas pu faire et pourquoi (cette partie doit "
"être ajoutée à la main dans l'onglet \"Flow\") — ne t'acharne JAMAIS "
"à répéter des appels d'outils pour une chose qu'aucun outil ne "
"permet, ça n'aboutira jamais."
"\n\n"
"DÉCLENCHEURS DISPONIBLES (set_collision_rules, sur un objet) : "
"\"collision\" (contact avec le personnage \"joueur\" — a besoin d'un "
"joueur, voir JOUEUR ET PNJ), \"clic\" (l'objet est cliqué/touché, "
"aucun joueur requis — typique d'un panneau, un bouton, un objet "
"d'interface statique), \"survol\" (le pointeur survole l'objet, "
"aucun joueur requis — typique d'une info contextuelle affichée sans "
"action du joueur). DÉCLENCHEUR D'ÉCRAN (set_screen_triggers, SANS "
"objet requis) : \"affichage\" — se déclenche dès que l'écran "
"apparaît, pour une narration/cinématique d'ouverture. "
"RÈGLE IMPORTANTE : si le créateur ne précise PAS explicitement quel "
"déclencheur utiliser pour un élément donné (ex. \"ajoute un panneau "
"d'information\" sans dire si c'est au clic, au survol, ou dès "
"l'affichage), NE CHOISIS PAS toute seule — pose la question dans ta "
"réponse texte avant d'agir. "
"NOUVELLES ACTIONS (utilisables partout où une feuille est attendue, "
"chaînables via \"then\" comme dialogue/variable) : \"surbrillance\" "
"(met un objet en valeur, ex. pour guider l'attention du joueur vers "
"la suite), \"visibilite\" (affiche/masque un objet, ex. débloquer un "
"élément), \"son\" (effet sonore ponctuel), \"video\" (joue une vidéo "
"de \"Mes assets\", en plein écran ou en incrustation — bloque la "
"suite de la chaîne jusqu'à la fin, comme un dialogue), \"indication\" "
"(bulle de texte courte près d'un objet, pour un conseil ponctuel — "
"PAS pour une réplique de personnage, utilise \"dialogue\" pour ça), "
"\"attendre\" (suspend la chaîne \"then\" pendant data_value SECONDES "
"avant de continuer — utile pour laisser un temps de lecture après "
"une narration à l'affichage de l'écran, avant d'enchaîner sur un "
"dialogue/une autre action)."
"\n\n"
"ÉTAT DE LA SCÈNE : le message système de chaque tour te donne les "
"dimensions de la caméra, la liste des objets déjà posés sur CET "
"écran, et les variables globales déjà existantes — relis-les avant "
"d'agir plutôt que de deviner (ne recrée jamais un objet ou une "
"variable qui existe déjà, corrige/complète l'existant avec "
"set_object_geometry/set_object_name/set_object_role/set_collision_rules)."
"set_object_geometry/set_object_name/set_object_role/set_collision_rules). "
"Pour retirer ou réordonner UNE action précise d'une chaîne déjà posée "
"(sans reconstruire toute la règle), utilise remove_trigger_action/"
"move_trigger_action (objet) ou remove_screen_trigger_action/"
"move_screen_trigger_action (écran) avec le leaf_id concerné. "
"append_action_to_trigger/append_action_to_screen_trigger peuvent "
"INSÉRER une action à N'IMPORTE QUEL niveau d'une chaîne (pas "
"seulement à la toute fin) : after_id désigne le bloc juste AVANT "
"l'endroit où insérer."
"\n\n"
"CAMÉRA ET POSITIONNEMENT : les dimensions indiquées sont la zone "
"VISIBLE (coin haut-gauche à (0,0)) — place tout objet important "
@@ -84,7 +120,14 @@ _SYSTEM_PROMPT = (
"vers un PNJ, etc.), assure-toi qu'IL EXISTE EXACTEMENT UN "
"personnage avec role=\"joueur\" (set_object_role) — jamais deux, "
"et ne pose jamais deux PNJ identiques sans que le créateur l'ait "
"demandé."
"demandé. "
"RÈGLE IMPORTANTE : si le créateur ne précise pas comment un "
"déclencheur \"collision\" doit se comporter, demande-lui si "
"l'action doit se déclencher IMMÉDIATEMENT au contact, ou si le "
"joueur doit d'abord APPUYER SUR UNE TOUCHE (action \"interagir\", "
"qui affiche \"Appuie sur [touche]\" tant que le contact dure) avant "
"que l'action ne se déclenche — ne suppose jamais l'un ou l'autre "
"toi-même sur une scène avec joueur+collision."
"\n\n"
"QUAND UTILISER QUOI : une VARIABLE globale sert à mémoriser une "
"donnée consultée plus tard (progression, un choix du joueur, un "
@@ -161,6 +204,16 @@ def _describe_scene_state(slug, screen_id):
desc += " [déclencheur déjà configuré]"
lines.append(desc)
screen_triggers = screens.resolve_screen_triggers(screen)
if screen_triggers:
lines.append(
f"{len(screen_triggers)} déclencheur(s) D'ÉCRAN déjà configuré(s) sur cet écran "
"(voir set_screen_triggers) — relis-les avant d'en ajouter un nouveau plutôt que "
"de dupliquer une narration d'ouverture déjà posée."
)
else:
lines.append("Aucun déclencheur d'écran (narration à l'affichage) configuré pour l'instant.")
variables = db.list_global_variables(slug)
if variables:
lines.append("Variables globales déjà existantes dans ce jeu (jamais en recréer une du même nom) :")
+189 -10
View File
@@ -28,8 +28,12 @@ import auth
import db
import screens
from db.dialogue_lines import QUESTION_REWARD_TYPES
from screens.rendering.collision_rules import ACTION_TYPES, LEAF_ACTION_TYPES, CONDITION_OPERATOR_KEYS
from screens.rendering.collision_rules import ACTION_TYPES, LEAF_ACTION_TYPES, CONDITION_OPERATOR_KEYS, TRIGGER_TYPES
from screens.rendering.screen_triggers import TRIGGER_TYPES_SCREEN
from screens.labels.data_operations import DATA_OPERATION_LABELS
from screens.labels.element_visibility import ELEMENT_VISIBILITY_LABELS
from screens.labels.surbrillance_values import SURBRILLANCE_LABELS
from screens.labels.video_modes import VIDEO_MODE_LABELS
from .scenario_client import generate_image_url
_CONDITION_OPERATORS = sorted(CONDITION_OPERATOR_KEYS)
@@ -76,7 +80,30 @@ _LEAF_ACTION_SCHEMA = {
"mark_completed": {"type": "boolean"},
"target_variable": {"type": "string", "description": "Pour type=variable : nom d'une variable globale existante."},
"data_operation": {"type": "string", "enum": _DATA_OPERATIONS},
"data_value": {"type": "string", "description": "Absent pour definir_bool_vrai/definir_bool_faux/basculer_bool."},
"data_value": {"type": "string", "description": "Pour type=variable : absent pour definir_bool_vrai/definir_bool_faux/basculer_bool. Pour type=attendre : nombre de SECONDES à attendre avant \"then\" (ex. \"2\", \"1.5\")."},
"object_id": {
"type": "integer",
"description": (
"Pour type=surbrillance/visibilite/indication : id de l'objet CIBLÉ par cette "
"action — n'importe quel objet de l'écran, pas forcément celui qui porte le "
"déclencheur (ex. le joueur touche un interrupteur -> indication près d'une porte)."
),
},
"valeur": {
"type": "string",
"description": (
f"Pour type=surbrillance : une valeur parmi {sorted(SURBRILLANCE_LABELS)!r}. "
f"Pour type=visibilite : une valeur parmi {sorted(ELEMENT_VISIBILITY_LABELS)!r}. "
"Pour type=condition (voir _ACTION_SCHEMA) : la valeur littérale à comparer."
),
},
"asset_url": {
"type": "string",
"description": "Pour type=son/video : URL d'un fichier déjà présent dans \"Mes assets\".",
},
"mode": {"type": "string", "enum": sorted(VIDEO_MODE_LABELS), "description": "Pour type=video."},
"texte": {"type": "string", "description": "Pour type=indication : le texte affiché dans la bulle."},
"duree_ms": {"type": "integer", "description": "Pour type=indication : durée d'affichage en ms (optionnel)."},
"then": {"description": "Feuille suivante (même forme), récursif."},
},
"required": ["type"],
@@ -93,11 +120,16 @@ _ACTION_SCHEMA = {
"target_variable": {"type": "string"},
"data_operation": {"type": "string", "enum": _DATA_OPERATIONS},
"data_value": {"type": "string"},
"then": {"description": "Feuille suivante (dialogue/variable), récursif."},
"object_id": _LEAF_ACTION_SCHEMA["properties"]["object_id"],
"valeur": _LEAF_ACTION_SCHEMA["properties"]["valeur"],
"asset_url": _LEAF_ACTION_SCHEMA["properties"]["asset_url"],
"mode": _LEAF_ACTION_SCHEMA["properties"]["mode"],
"texte": _LEAF_ACTION_SCHEMA["properties"]["texte"],
"duree_ms": _LEAF_ACTION_SCHEMA["properties"]["duree_ms"],
"then": {"description": "Feuille suivante (dialogue/variable/surbrillance/visibilite/son/video/indication), récursif."},
"sub_action": {"description": "Pour type=interagir : une action (pas \"interagir\" à nouveau)."},
"variable": {"type": "string", "description": "Pour type=condition."},
"operateur": {"type": "string", "enum": _CONDITION_OPERATORS},
"valeur": {"type": "string"},
"si_vrai": {"description": "Pour type=condition : null ou une feuille."},
"si_faux": {"description": "Pour type=condition : null ou une feuille."},
},
@@ -196,8 +228,13 @@ TOOLS = [
"name": "set_collision_rules",
"description": (
"Remplace TOUTES les règles \"déclencheur -> action\" d'un objet. "
"Sanitizé côté serveur (screens.sanitize_collision_rules) : toute "
"valeur invalide est silencieusement retirée plutôt que rejetée."
"Déclencheurs disponibles : \"collision\" (contact avec le personnage "
"\"joueur\" — voir set_object_role), \"clic\" (l'objet est cliqué/touché, "
"aucun joueur requis), \"survol\" (le pointeur survole l'objet, aucun "
"joueur requis). Sanitizé côté serveur (screens.sanitize_collision_rules) : "
"toute valeur invalide est silencieusement retirée plutôt que rejetée — "
"compare le nombre de `rules` renvoyées dans le résultat à ce que tu as "
"envoyé pour détecter un retrait silencieux."
),
"input_schema": {
"type": "object",
@@ -208,7 +245,7 @@ TOOLS = [
"items": {
"type": "object",
"properties": {
"trigger": {"type": "string"},
"trigger": {"type": "string", "enum": list(TRIGGER_TYPES)},
"action": _ACTION_SCHEMA,
},
"required": ["trigger", "action"],
@@ -218,6 +255,33 @@ TOOLS = [
"required": ["object_id", "rules"],
},
},
{
"name": "set_screen_triggers",
"description": (
"Remplace TOUTES les règles \"déclencheur d'écran -> action\" de l'écran "
"en cours — un déclencheur d'ÉCRAN se déclenche SANS avoir besoin d'un "
"objet (narration/cinématique dès l'affichage). Seul déclencheur : "
"\"affichage\" (à l'affichage de l'écran). Même sanitisation que "
"set_collision_rules."
),
"input_schema": {
"type": "object",
"properties": {
"rules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"trigger": {"type": "string", "enum": list(TRIGGER_TYPES_SCREEN)},
"action": _ACTION_SCHEMA,
},
"required": ["trigger", "action"],
},
},
},
"required": ["rules"],
},
},
{
"name": "add_generated_image",
"description": (
@@ -238,9 +302,12 @@ TOOLS = [
{
"name": "append_action_to_trigger",
"description": (
"Ajoute une action à la SUITE d'une chaîne déjà posée sur un "
"déclencheur existant, sans reconstruire toute la règle — "
"after_id désigne la dernière feuille de la chaîne."
"Insère une action JUSTE APRÈS un bloc précis d'une chaîne déjà "
"posée sur un déclencheur existant, sans reconstruire toute la "
"règle — after_id peut désigner N'IMPORTE QUEL bloc de la "
"chaîne, pas forcément le dernier : une suite déjà présente "
"après ce bloc passe derrière la nouvelle action plutôt que "
"d'être remplacée."
),
"input_schema": {
"type": "object",
@@ -252,6 +319,81 @@ TOOLS = [
"required": ["object_id", "after_id", "action"],
},
},
{
"name": "append_action_to_screen_trigger",
"description": (
"Équivalent d'append_action_to_trigger pour un déclencheur D'ÉCRAN "
"(voir set_screen_triggers), sur l'écran en cours — after_id peut "
"désigner N'IMPORTE QUEL bloc de la chaîne, pas forcément le "
"dernier."
),
"input_schema": {
"type": "object",
"properties": {
"after_id": {"type": "string"},
"action": _LEAF_ACTION_SCHEMA,
},
"required": ["after_id", "action"],
},
},
{
"name": "remove_trigger_action",
"description": (
"Retire UNE action précise (leaf_id) d'un déclencheur d'OBJET, où "
"qu'elle soit dans l'arbre (feuille directe, maillon d'une chaîne "
"\"then\", sous-action d'un \"interagir\", ou branche d'une "
"\"condition\"). Si c'était la SEULE action de la règle, la règle "
"entière disparaît ; si elle avait une suite \"then\", celle-ci "
"prend sa place."
),
"input_schema": {
"type": "object",
"properties": {
"object_id": {"type": "integer"},
"leaf_id": {"type": "string"},
},
"required": ["object_id", "leaf_id"],
},
},
{
"name": "remove_screen_trigger_action",
"description": "Équivalent de remove_trigger_action pour un déclencheur D'ÉCRAN.",
"input_schema": {
"type": "object",
"properties": {"leaf_id": {"type": "string"}},
"required": ["leaf_id"],
},
},
{
"name": "move_trigger_action",
"description": (
"Déplace une action précise (leaf_id) d'un cran vers le haut "
"(\"up\") ou le bas (\"down\") DANS SA PROPRE CHAÎNE \"then\", pour "
"un déclencheur d'OBJET — jamais au-delà de sa branche de "
"condition/sous-action interagir."
),
"input_schema": {
"type": "object",
"properties": {
"object_id": {"type": "integer"},
"leaf_id": {"type": "string"},
"direction": {"type": "string", "enum": ["up", "down"]},
},
"required": ["object_id", "leaf_id", "direction"],
},
},
{
"name": "move_screen_trigger_action",
"description": "Équivalent de move_trigger_action pour un déclencheur D'ÉCRAN.",
"input_schema": {
"type": "object",
"properties": {
"leaf_id": {"type": "string"},
"direction": {"type": "string", "enum": ["up", "down"]},
},
"required": ["leaf_id", "direction"],
},
},
]
@@ -376,6 +518,37 @@ def _dispatch_append_action_to_trigger(slug, screen_id, user_id, object_id, afte
return {"ok": ok}
def _dispatch_set_screen_triggers(slug, screen_id, user_id, rules):
sanitized = screens.sanitize_screen_triggers(rules)
screens.set_screen_triggers(slug, screen_id, sanitized)
return {"ok": True, "rules": sanitized}
def _dispatch_append_action_to_screen_trigger(slug, screen_id, user_id, after_id, action):
ok = screens.append_action_to_screen_trigger(slug, screen_id, after_id, action)
return {"ok": ok}
def _dispatch_remove_trigger_action(slug, screen_id, user_id, object_id, leaf_id):
ok = screens.remove_action_from_trigger(slug, object_id, leaf_id)
return {"ok": ok}
def _dispatch_remove_screen_trigger_action(slug, screen_id, user_id, leaf_id):
ok = screens.remove_action_from_screen_trigger(slug, screen_id, leaf_id)
return {"ok": ok}
def _dispatch_move_trigger_action(slug, screen_id, user_id, object_id, leaf_id, direction):
ok = screens.move_action_in_trigger(slug, object_id, leaf_id, direction)
return {"ok": ok}
def _dispatch_move_screen_trigger_action(slug, screen_id, user_id, leaf_id, direction):
ok = screens.move_action_in_screen_trigger(slug, screen_id, leaf_id, direction)
return {"ok": ok}
def _dispatch_add_generated_image(slug, screen_id, user_id, kind, prompt):
"""Scenario -> "Mes assets" (auth.save_user_asset, source="ia") ->
posée sur l'écran via LE MÊME chemin que Phase 1
@@ -401,6 +574,12 @@ _DISPATCH = {
"create_global_variable": _dispatch_create_global_variable,
"set_collision_rules": _dispatch_set_collision_rules,
"append_action_to_trigger": _dispatch_append_action_to_trigger,
"set_screen_triggers": _dispatch_set_screen_triggers,
"append_action_to_screen_trigger": _dispatch_append_action_to_screen_trigger,
"remove_trigger_action": _dispatch_remove_trigger_action,
"remove_screen_trigger_action": _dispatch_remove_screen_trigger_action,
"move_trigger_action": _dispatch_move_trigger_action,
"move_screen_trigger_action": _dispatch_move_screen_trigger_action,
"add_generated_image": _dispatch_add_generated_image,
}