Répond au manque signalé par l'utilisateur : le déclencheur "clavier"
existant (keydown) ne se déclenche qu'UNE FOIS par appui, insuffisant
pour "maintenir une touche fait avancer/sauter le personnage en continu".
- Deux nouveaux déclencheurs (screens/flow/constants.py,
screens/scenes/flow_palette.py) : "Tant qu'une touche est maintenue"
(se répète ~20 fois/seconde tant que la touche reste enfoncée,
runScreenHeldKeyTriggers() dans triggers.js — même patron PAR ÉCRAN que
"minuteur", arrêté au changement d'écran) et "Au relâchement d'une
touche" (un seul déclenchement, scan global comme "clavier"). Combinés
à l'action existante "Modifier un objet de scène → Déplacer de... px
(relatif)", ça permet un vrai déplacement continu.
- preventDefault() sur toute touche que le jeu écoute réellement
(isGameKey(), triggers.js) : Espace/Flèches font défiler la page par
défaut, et Espace réactive en plus le bouton actuellement focus (souvent
le bouton "Jouer" qui garde le focus après l'ouverture de l'aperçu) —
ça pouvait donner l'impression qu'une touche du jeu ne faisait rien.
- Le personnage pouvait sortir du cadre de la scène en se déplaçant :
applyObjectProperty()/clampSceneObjectPosition() (static/js/play/actions.js)
bornent maintenant toute position (absolue ou relative) à
[0, scene_width/height − la taille de l'objet].
- Vitesse d'animation par défaut adaptée au nombre d'images : la valeur
fixe (8 i/s) venait d'un formulaire pensé pour les cycles Kenney (8
images) — un cycle CraftPix (walk=30 images) au même 8 i/s prenait
~4 secondes, "très lent". Le choix d'une animation dans la galerie
calcule maintenant une vitesse par défaut proportionnelle à son nombre
d'images (flow-editor.js, animation-timeline.js).
- Priorité d'animation (bug : "je ne peux pas me déplacer et sauter") :
un déclencheur de déplacement (touche maintenue) redemande "marche" à
chaque tick, écrasant aussitôt une animation ponctuelle ("sauter")
démarrée entre-temps avant qu'elle ait pu s'afficher. runSpriteAnimation()
(actions.js) laisse maintenant une animation NON BOUCLÉE en cours
(même à une seule frame, ex. une pose Kenney figée) aller jusqu'au bout
avant qu'une autre demande puisse l'interrompre.
Nouveaux tests : static/js/play/__tests__/{actions,triggers}.test.js
(idempotence + priorité d'animation, bornage aux limites de la scène,
isGameKey) ; tests/test_scene_edit_view.py (persistance d'un nœud
"touche_maintenue").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
screens/flow/constants.py : TRIGGER_EVENTS += "clavier" (À l'appui sur
une touche) et "minuteur" (Toutes les X millisecondes) — ni élément ni
écran précis pour les deux, comme "evenement" déjà en place.
FLOW_NODE_FIELDS += trigger_key/trigger_interval_ms. ensure_flow_schema.py :
ALTER TABLE pour les 2 colonnes (patron trigger_custom_event_id).
templates/screen_edit.html + static/js/screen_edit/flow-editor.js :
- "clavier" : un champ "Touche à surveiller" qui capture lui-même la
touche pressée (onkeydown sur l'input, captureFlowTriggerKey()) plutôt
que de faire deviner la syntaxe attendue (ev.key du navigateur, ex.
"ArrowUp", "a", " " pour Espace).
- "minuteur" : un simple champ numérique (millisecondes).
- nodeLabel() affiche "⌨️ Touche « X »"/"⏱️ Toutes les N ms" sur le nœud.
static/js/play/triggers.js (moteur de jeu) :
- bindKeyboardTriggers() : UN SEUL window.addEventListener('keydown', ...)
posé une fois pour tout le jeu (voir l'amorçage en fin de
templates/play.html) — même patron de scan global que
dispatchGameEvent() pour "Sur un événement personnalisé".
- runScreenTimerTriggers(screenId) : géré PAR ÉCRAN (appelé depuis
showScreen(), static/js/play/screens.js) — démarre les setInterval des
nœuds "minuteur" de l'écran affiché, arrête d'abord tous ceux de
l'affichage précédent (même principe que runAnimationTimeline) pour
ne jamais accumuler des minuteurs sur des écrans quittés.
Vérifié : 255 tests passent (5 nouveaux, dont un qui verrouille que la
touche Espace — très probablement utilisée en jeu — n'est pas filtrée
comme une valeur "vide" par add_flow_node.py), 13 tests node:test
toujours au vert, syntaxe JS validée sur tous les fichiers de
static/js/play/ et static/js/screen_edit/. Comme le reste du graphe de
logique côté client, le comportement RÉEL d'un keydown/setInterval n'est
pas testable sans navigateur — test manuel recommandé (touche assignée
à un saut, minuteur faisant avancer un compteur).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Première moitié de la fonctionnalité "événements" (NEED_ACTION et
autres) : une entité game-wide (nom, description, "a un paramètre
élément" oui/non), déclenchable comme nouvelle action du graphe de
logique depuis n'importe quelle scène/modèle, et écoutable comme
nouveau type de déclencheur depuis n'importe quel autre. L'UI (nouvel
onglet "Événements" dans screen_edit.html, formulaires de nœud, exécution
côté client dans play.html) suit dans un commit séparé.
- db/custom_events/ (calqué sur db/global_vars/) : CRUD de la table
_custom_events (nom unique, description, has_element_param).
create_custom_event est idempotent par nom (même convention que
create_global_variable) — sans risque en cas de double soumission.
- screens/flow/ : 3 nouvelles colonnes sur _flow_nodes
(trigger_custom_event_id/target_custom_event_id : quel événement un
nœud écoute/déclenche ; target_element_from_event : indicateur
réutilisable par n'importe quel nœud Action utilisant déjà
target_element_id, pour résoudre "l'élément transmis par l'événement
en cours" au lieu d'une cible fixe — contourne la contrainte de clé
étrangère de target_element_id, qui empêche d'y stocker un sentinel
comme EVENT_ROW_ID directement). Nouveau trigger_event "evenement" et
action_type "declencher_evenement".
- screens/custom_events/ (PAS dans db/, même séparation que
screens/elements/delete_element.py) : delete_custom_event, la SEULE
suppression d'entité game-wide du moteur à vraiment cascader (demande
explicite) — supprime tous les nœuds/arêtes qui référencent
l'événement, sur TOUTES les scènes ET tous les modèles à la fois
(aucun filtre screen_id nécessaire : un modèle est un écran caché,
même table _flow_nodes). list_custom_event_usages : où un événement
est écouté/déclenché, pour l'onglet Événements à venir.
- routes/custom_events/ : CRUD monté sous /game/<slug>/events/...,
redirige vers l'éditeur de scène/modèle d'origine (screen_id transmis
par le formulaire) avec l'onglet "events" à ouvrir.
tests/test_custom_events.py (nouveau) : idempotence à la création,
usages détectés sur deux écrans différents, suppression qui retire bien
les DEUX nœuds (un sur une vraie scène, un sur un modèle/écran caché)
en une seule opération, sans toucher aux écrans eux-mêmes. 207 tests au
total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add "survol"/"fin_survol" trigger events (mouseenter/mouseleave,
same anti-doublon pattern as bindClicks()) so hovering an element can
run flow nodes, instead of the old static hover-text-only control
removed from the properties panel. Also add "Contenu" as a settable
"Modifier un élément" property so an action can display a defined
text on another element on hover — the concrete use case that
motivated this.
Verified live with Playwright: a "Au survol" trigger on one element
correctly updates another element's text via the action, and text
stays put with no "Fin du survol" wired (explicit, no implicit
revert, consistent with "Au clic").