Files
Forge-Engine/templates/play.html
T
williamandClaude Sonnet 5 727c3c97ba
Build and deploy / test-python (push) Successful in 1m28s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 3 : déclencheur clavier + minuteur récurrent
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>
2026-08-30 17:05:55 +02:00

126 lines
6.5 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ game.name }} — Jouer</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='branding/forge-favicon.svg') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script>window.CSRF_TOKEN = {{ csrf_token()|tojson }};</script>
<script src="{{ url_for('static', filename='csrf_fetch.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fonts/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/animate.min.css') }}"><style>
body{ margin:0; background:#000; }
/* ---------- Zone de jeu à proportion fixe (voir #playFrame) ----------
Chaque écran est conçu avec un format précis (Portrait/Paysage/Carré,
choisi dans l'éditeur — voir preview_aspect). Positionner les éléments
en % de la fenêtre du NAVIGATEUR (dont la forme varie selon l'appareil
du joueur) les déformait : un carré dessiné en Portrait pouvait
s'afficher étiré/écrasé sur un écran de proportion différente. #playFrame
garde donc toujours la même proportion que l'écran en cours (mise à
jour dans showScreen ci-dessous), quelle que soit la fenêtre réelle —
avec des bandes noires (letterboxing) si besoin, comme un lecteur
vidéo — pour que les % restent fidèles à ce qui a été conçu. */
#playRoot{
position:relative; width:100vw; height:100vh; overflow:hidden; background:#000;
display:flex; align-items:center; justify-content:center;
}
#playFrame{
position:relative; overflow:hidden; background:#0b0d12;
width: min(100vw, calc(100vh * var(--ratio-w, 9) / var(--ratio-h, 16)));
height: min(100vh, calc(100vw * var(--ratio-h, 16) / var(--ratio-w, 9)));
}
.playScreen{ position:absolute; inset:0; display:none; }
.playElement{
position:absolute; box-sizing:border-box; font-family:system-ui,sans-serif;
}
.playElement.clickable{ cursor:pointer; }
.playElementInner{
width:100%; height:100%;
display:flex; align-items:center; justify-content:center;
}
.playElement input{ width:90%; height:70%; box-sizing:border-box; border:1px solid #2b3140; border-radius:4px; padding:0 8px; }
#emptyState{ color:var(--forge-text-muted, #8b95a7); font-family:system-ui,sans-serif; text-align:center; padding-top:40vh; }
/* 3.3 (Confort) — surbrillance générique dynamique : liseré clignotant,
posé/enlevé par une action "Modifier un élément → Surbrillance",
applicable à n'importe quel élément (pas seulement les boutons). */
@keyframes forgeHighlightPulse {
0%, 100% { box-shadow: 0 0 0 3px rgba(255, 196, 0, 0.9); }
50% { box-shadow: 0 0 0 3px rgba(255, 196, 0, 0.15); }
}
.forgeHighlight{ animation: forgeHighlightPulse 1.1s ease-in-out infinite; }
/* 3.5 (Confort) — verrouillage d'un élément après décision : grisé +
inerte, posé/enlevé par une action "Modifier un élément → Désactivé". */
.forgeDisabled{ opacity:0.45; filter:grayscale(60%); pointer-events:none; }
</style>
</head>
<body>
<div id="playRoot">
<div id="playFrame">
{% for s in screens_data %}
<div class="playScreen" data-screen-id="{{ s.id }}" data-aspect="{{ s.preview_aspect or '9/16' }}">
{% for el in s.elements if not el.parent_id %}
<div class="playElement" data-el-id="{{ el.id }}" data-anim-target="{{ el.id }}" style="{{ el|elstyle }}">
<div class="playElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% if not screens_data %}
<div id="emptyState">Aucun écran défini pour l'instant — retourne dans l'éditeur pour en créer un.</div>
{% endif %}
</div>
</div>
<script>
// Données injectées par Jinja (côté serveur) — tout le reste du moteur
// vit maintenant dans des fichiers statiques sous static/js/play/ (voir
// plan de modularisation : un fichier = une responsabilité, aussi pour
// le JS), qui ne peuvent pas contenir de code Jinja puisqu'ils ne
// passent jamais par le moteur de templates. FORGE_PLAY_URLS porte donc
// les URLs de routes dont ces fichiers ont besoin (actions.js,
// bindings.js), construites ici une seule fois — passées par la route
// elle-même (runtime_payload_url/flow_node_run_data_url/
// flow_node_run_variable_url) plutôt que construites ici avec des noms
// de endpoint fixes : ce même template sert aussi bien l'aperçu
// créateur (/game/<slug>/play, routes/play/game_play.py) que la partie
// publique par joueur (/jouer/<slug>, routes/public_play/), deux
// familles de routes distinctes (voir état par joueur, Phase 1).
let gameData = {{ payload_json|safe }};
let screensData = gameData.screens;
var FORGE_PLAY_URLS = {
runtimePayload: {{ runtime_payload_url|tojson }},
flowNodeRunData: {{ flow_node_run_data_url|tojson }},
flowNodeRunVariable: {{ flow_node_run_variable_url|tojson }},
};
</script>
<script src="{{ url_for('static', filename='js/play/screens.js') }}"></script>
<script src="{{ url_for('static', filename='js/play/conditions.js') }}"></script>
<script src="{{ url_for('static', filename='js/play/actions.js') }}"></script>
<script src="{{ url_for('static', filename='js/play/triggers.js') }}"></script>
<script src="{{ url_for('static', filename='js/play/bindings.js') }}"></script>
<script src="{{ url_for('static', filename='js/play/flow-engine.js') }}"></script>
<script>
// L'écran affiché au démarrage doit être choisi AVANT le premier
// bindClicks() : la recherche du nœud Déclencheur associé à un élément se
// fait sur l'écran courant (window.currentScreenId), qui doit donc déjà
// être connu au moment où on attache les gestionnaires de clic.
injectCustomKeyframes();
if (screensData.length) showScreen(screensData[0].id);
// [data-element-id] est posé par le moteur de rendu sur CHAQUE élément,
// qu'il soit directement sur l'écran ou imbriqué à l'intérieur d'un
// conteneur/répéteur — donc un bouton posé à l'intérieur d'un conteneur
// déclenche bien son action au clic, exactement comme un bouton posé
// directement sur l'écran.
bindClicks();
bindHoverTexts();
bindHoverTriggers();
// Un seul écouteur clavier pour tout le jeu (Phase 3) — showScreen()
// ci-dessus a déjà démarré les minuteurs de l'écran affiché au premier
// affichage (runScreenTimerTriggers, appelé depuis showScreen()).
bindKeyboardTriggers();
</script>
</body>
</html>