From 74f7ca396f787c930b5b6ceb91f2166bd90d2450 Mon Sep 17 00:00:00 2001 From: william Date: Wed, 2 Sep 2026 11:26:37 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9placement/animation=20automatiques=20d'u?= =?UTF-8?q?n=20personnage=20(sc=C3=A8ne=202D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplification demandée pour l'éditeur de jeu RPG : un personnage posé sur une scène 2D se déplace et s'anime TOUT SEUL avec ZQSD (+ E pour interagir) dès qu'il est posé — plus besoin de poser le moindre nœud de flow pour un mouvement de base. Le panneau "🕹️ Commandes" du personnage permet de remapper les 4 touches de déplacement et la touche d'interaction, et de bloquer un axe (horizontal/vertical seulement). Entièrement client (static/js/play/personnage-controller.js), réutilise heldKeys (triggers.js) et applyObjectProperty/clampSceneObjectPosition/ runSpriteAnimation (actions.js) — aucune logique dupliquée, et ça fonctionne aussi bien en ligne que dans l'export Web/SCORM (aucune requête serveur). "walk"/"idle"/"interact" (poses déjà présentes pour tout personnage Forge/CraftPix) sont utilisées telles quelles ; une pose absente dégrade silencieusement (déplacement sans animation) plutôt que de planter. Co-Authored-By: Claude Sonnet 5 --- routes/scenes/__init__.py | 1 + routes/scenes/scene_edit_view.py | 3 + routes/scenes/scene_object_commands.py | 37 ++++++ screens/__init__.py | 2 + screens/payload/full_game_payload.py | 8 ++ screens/rendering/personnage_commands.py | 32 +++++ screens/scenes/set_scene_object_commands.py | 26 ++++ .../__tests__/personnage-controller.test.js | 123 ++++++++++++++++++ static/js/play/personnage-controller.js | 119 +++++++++++++++++ static/js/play/screens.js | 4 + static/js/scenes/scene-editor.js | 32 +++++ templates/play.html | 1 + templates/scene_edit.html | 40 ++++++ tests/test_personnage_commands.py | 111 ++++++++++++++++ 14 files changed, 539 insertions(+) create mode 100644 routes/scenes/scene_object_commands.py create mode 100644 screens/rendering/personnage_commands.py create mode 100644 screens/scenes/set_scene_object_commands.py create mode 100644 static/js/play/__tests__/personnage-controller.test.js create mode 100644 static/js/play/personnage-controller.js create mode 100644 tests/test_personnage_commands.py diff --git a/routes/scenes/__init__.py b/routes/scenes/__init__.py index 216ef0bc..fea9a00d 100644 --- a/routes/scenes/__init__.py +++ b/routes/scenes/__init__.py @@ -3,4 +3,5 @@ from . import ( scene_object_geometry, scene_object_delete, scene_object_personnage_data, + scene_object_commands, ) diff --git a/routes/scenes/scene_edit_view.py b/routes/scenes/scene_edit_view.py index d9ae09cb..30bb8e53 100644 --- a/routes/scenes/scene_edit_view.py +++ b/routes/scenes/scene_edit_view.py @@ -23,9 +23,11 @@ def render_scene_edit(slug, screen_id, game): o["rendered_html"] = screens.render_scene_object(o) selected_id = request.args.get("selected", type=int) personnage_data = None + personnage_commands = None selected_obj = next((o for o in objects if o["id"] == selected_id), None) if selected_id else None if selected_obj and selected_obj["kind"] == "personnage": personnage_data = screens.resolve_personnage_data(selected_obj) + personnage_commands = screens.resolve_personnage_commands(selected_obj) object_labels = { o["id"]: (o.get("name") or ("Personnage" if o["kind"] == "personnage" else "Décor")) @@ -57,6 +59,7 @@ def render_scene_edit(slug, screen_id, game): "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, sprite_library=sprite_library, sprite_library_families=sprite_library_families, sprite_library_json=json.dumps(sprite_library), element_animations_map_json=json.dumps(element_animations_map), diff --git a/routes/scenes/scene_object_commands.py b/routes/scenes/scene_object_commands.py new file mode 100644 index 00000000..6ffcf361 --- /dev/null +++ b/routes/scenes/scene_object_commands.py @@ -0,0 +1,37 @@ +from flask import request, jsonify + +import screens + +from core.flask_app import app + + +@app.route("/game//scene-objects//commandes", methods=["POST"]) +def scene_object_commands(slug, object_id): + """Touches de déplacement/interaction + blocage d'axe d'un personnage + (voir templates/scene_edit.html, section "Commandes" du panneau + Personnage, et static/js/play/personnage-controller.js pour leur + usage au jeu) — mirror de scene_object_personnage_data.py, un + attribut JSON distinct.""" + body = request.get_json(silent=True) or {} + obj = screens.get_scene_object(slug, object_id) + if not obj: + return jsonify({"error": "objet introuvable"}), 404 + + defaults = screens.DEFAULT_PERSONNAGE_COMMANDS + commands = {} + for key in ("haut", "bas", "gauche", "droite", "interagir"): + # Jamais de .strip() ici : " " (Espace) est une valeur ev.key + # parfaitement valide côté client (voir personnage-controller.js) + # pour "interagir" — un strip() la réduirait à tort à "" et la + # ferait retomber sur "e" par défaut. + value = body.get(key) + commands[key] = value if isinstance(value, str) and value else defaults[key] + commands["axe"] = body.get("axe") if body.get("axe") in ("libre", "horizontal", "vertical") else "libre" + try: + vitesse = float(body.get("vitesse", defaults["vitesse"])) + except (TypeError, ValueError): + vitesse = defaults["vitesse"] + commands["vitesse"] = max(1, min(20, vitesse)) + + screens.set_scene_object_commands(slug, object_id, commands) + return jsonify({"ok": True, "commands": commands}) diff --git a/screens/__init__.py b/screens/__init__.py index 55997ef0..4d069b7a 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -76,6 +76,7 @@ from .flow.constants import ( from .rendering.render_element_html import render_element_html from .rendering.trigger_for import _trigger_for from .rendering.personnage_data import resolve_personnage_data, resolve_personnage_animations, resolve_personnage_idle_frame +from .rendering.personnage_commands import resolve_personnage_commands, DEFAULT_PERSONNAGE_COMMANDS from .screens_repo.ensure_schema import ensure_schema from .screens_repo.list_screens import list_screens @@ -109,6 +110,7 @@ from .scenes.get_scene_object import get_scene_object from .scenes.update_scene_object_geometry import update_scene_object_geometry from .scenes.delete_scene_object import delete_scene_object from .scenes.set_scene_object_personnage_data import set_scene_object_personnage_data +from .scenes.set_scene_object_commands import set_scene_object_commands 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 diff --git a/screens/payload/full_game_payload.py b/screens/payload/full_game_payload.py index 376af29b..3dac0424 100644 --- a/screens/payload/full_game_payload.py +++ b/screens/payload/full_game_payload.py @@ -8,6 +8,7 @@ from ..animations.list_animation_clips import list_animation_clips from ..element_types.list_element_types import list_element_types from ..element_types.load_template_tree import _load_template_tree from ..rendering.personnage_data import resolve_personnage_animations +from ..rendering.personnage_commands import resolve_personnage_commands from ..scenes.list_scene_objects import list_scene_objects from ..scenes.render_scene_object import render_scene_object @@ -44,6 +45,13 @@ def full_game_payload(slug, player_id=db.PLAYER_SHARED): objects = list_scene_objects(slug, s["id"]) for o in objects: o["rendered_html"] = render_scene_object(o) + # Déplacement/animation automatiques (voir + # static/js/play/personnage-controller.js) : chaque + # personnage porte ses touches déjà résolues (défauts + # comblés) pour que le runtime client n'ait aucune logique + # de repli à dupliquer. + if o["kind"] == "personnage": + o["personnage_commandes"] = resolve_personnage_commands(o) payload_screens.append({**s, "elements": objects}) else: elements = list_elements(slug, s["id"], enforce_visibility=True, player_id=player_id) diff --git a/screens/rendering/personnage_commands.py b/screens/rendering/personnage_commands.py new file mode 100644 index 00000000..fc06e097 --- /dev/null +++ b/screens/rendering/personnage_commands.py @@ -0,0 +1,32 @@ +import json + +# ZQSD (convention AZERTY, comme le reste de l'éditeur) + E pour interagir +# — un objet de scène kind="personnage" se déplace et s'anime TOUT SEUL +# avec ces touches dès qu'il est posé sur la scène (voir +# static/js/play/personnage-controller.js), sans le moindre nœud de flow +# à poser. "axe" ("libre"/"horizontal"/"vertical") permet de bloquer un +# axe de déplacement (ex. un personnage qui ne se déplace que de gauche à +# droite). "vitesse" est en pixels par tick (50ms, voir +# personnage-controller.js — même cadence que touche_maintenue). +DEFAULT_PERSONNAGE_COMMANDS = { + "haut": "z", "bas": "s", "gauche": "q", "droite": "d", + "interagir": "e", "axe": "libre", "vitesse": 4, +} + + +def resolve_personnage_commands(el): + """Décode attributes._personnage_commandes (voir + screens/scenes/set_scene_object_commands.py) en dict Python, jamais + None — un attribut absent/corrompu retombe sur les touches par défaut + ci-dessus (comportement inchangé pour tout personnage déjà posé avant + l'ajout de ce réglage).""" + raw = (el.get("attributes") or {}).get("_personnage_commandes") + commands = dict(DEFAULT_PERSONNAGE_COMMANDS) + if raw: + try: + parsed = json.loads(raw) + except (TypeError, ValueError): + parsed = None + if isinstance(parsed, dict): + commands.update({k: v for k, v in parsed.items() if k in DEFAULT_PERSONNAGE_COMMANDS}) + return commands diff --git a/screens/scenes/set_scene_object_commands.py b/screens/scenes/set_scene_object_commands.py new file mode 100644 index 00000000..0358e4e8 --- /dev/null +++ b/screens/scenes/set_scene_object_commands.py @@ -0,0 +1,26 @@ +import json + +import db + +from .ensure_scene_schema import ensure_scene_schema + + +def set_scene_object_commands(slug, object_id, commands): + """Enregistre les touches de déplacement/interaction et le blocage + d'axe d'un objet de scène kind="personnage" (voir + screens/rendering/personnage_commands.py pour les valeurs par défaut, + et static/js/play/personnage-controller.js pour leur usage au jeu) — + même patron que set_scene_object_personnage_data.py, mais un attribut + JSON DISTINCT (_personnage_commandes) pour ne jamais entrer en + conflit avec _personnage_data (choix du personnage/de ses animations).""" + 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 + attributes = json.loads(row["attributes"] or "{}") + attributes["_personnage_commandes"] = json.dumps(commands) + conn.execute("UPDATE _scene_objects SET attributes = ? WHERE id = ?", (json.dumps(attributes), object_id)) + conn.commit() + conn.close() diff --git a/static/js/play/__tests__/personnage-controller.test.js b/static/js/play/__tests__/personnage-controller.test.js new file mode 100644 index 00000000..13ac010c --- /dev/null +++ b/static/js/play/__tests__/personnage-controller.test.js @@ -0,0 +1,123 @@ +// Déplacement/animation automatiques d'un personnage (voir +// static/js/play/personnage-controller.js) : ZQSD + E par défaut, sans +// le moindre nœud de flow à poser — réutilise applyObjectProperty/ +// clampSceneObjectPosition/runSpriteAnimation/resolveSpriteFrames +// (actions.js), heldKeys (triggers.js). +const test = require('node:test'); +const assert = require('node:assert/strict'); + +global.window = global.window || {}; +global.gameData = global.gameData || {}; +global.screensData = []; +global.heldKeys = new Set(); + +const actionsMod = require('../actions.js'); +Object.assign(global, actionsMod); + +const { + forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, + forgeRunPersonnageControllerTick, +} = require('../personnage-controller.js'); + +function fakeSceneEl() { + const classes = new Set(); + return { + style: { left: '100px', top: '100px', width: '20px', height: '20px' }, + classList: { + add: c => classes.add(c), remove: c => classes.delete(c), + toggle: c => (classes.has(c) ? classes.delete(c) : classes.add(c)), + contains: c => classes.has(c), + }, + }; +} + +function withPersonnageObject(obj, elByObjectId, fn) { + const previousDoc = global.document; + global.document = { querySelector: sel => elByObjectId[sel.match(/"(\d+)"/)[1]] }; + global.screensData = [{ id: 1, scene_width: 500, scene_height: 500 }]; + global.window.currentScreenId = 1; + global.gameData = { personnage_animations: { [obj.id]: { walk: ['/w0.png'], idle: ['/i0.png'] } } }; + try { fn(); } finally { global.document = previousDoc; stopAllSpriteAnimations(); } +} +const { stopAllSpriteAnimations } = actionsMod; + +test('forgePersonnageMovementAnim/forgePersonnageIdleAnim — repli silencieux si la pose est absente', () => { + global.gameData = { personnage_animations: { 5: { idle: ['/i0.png'] } } }; + assert.equal(forgePersonnageMovementAnim(5), null); // pas de "walk" pour ce personnage + assert.equal(forgePersonnageIdleAnim(5), 'idle'); + assert.equal(forgePersonnageAnimExists(5, 'interact'), false); +}); + +test('tick — touche "droite" enfoncée : déplace vers la droite et retire le miroir', () => { + const obj = { id: 10, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } }; + const el = fakeSceneEl(); + withPersonnageObject(obj, { 10: el }, () => { + global.heldKeys = new Set(['d']); + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.left, '104px'); + assert.equal(el.classList.contains('forgeFlipped'), false); + }); +}); + +test('tick — touche "gauche" enfoncée : déplace vers la gauche et applique le miroir', () => { + const obj = { id: 11, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } }; + const el = fakeSceneEl(); + withPersonnageObject(obj, { 11: el }, () => { + global.heldKeys = new Set(['q']); + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.left, '96px'); + assert.equal(el.classList.contains('forgeFlipped'), true); + }); +}); + +test('tick — axe "horizontal" : ignore haut/bas même si la touche est enfoncée', () => { + const obj = { id: 12, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'horizontal', vitesse: 4 } }; + const el = fakeSceneEl(); + withPersonnageObject(obj, { 12: el }, () => { + global.heldKeys = new Set(['z']); // "haut", bloqué par l'axe + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.top, '100px'); // inchangé + }); +}); + +test('tick — axe "vertical" : ignore gauche/droite même si la touche est enfoncée', () => { + const obj = { id: 13, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'vertical', vitesse: 4 } }; + const el = fakeSceneEl(); + withPersonnageObject(obj, { 13: el }, () => { + global.heldKeys = new Set(['d']); // "droite", bloqué par l'axe + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.left, '100px'); // inchangé + }); +}); + +test('tick — bornage aux limites de la scène (ne sort jamais du cadre)', () => { + const obj = { id: 14, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 50 } }; + const el = fakeSceneEl(); + el.style.left = '490px'; // 500 (scène) - 20 (largeur) = 480 max + withPersonnageObject(obj, { 14: el }, () => { + global.heldKeys = new Set(['d']); + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.left, '480px'); + }); +}); + +test('tick — aucune touche enfoncée : ne bouge pas (l\'appel reste sans effet)', () => { + const obj = { id: 15, kind: 'personnage', personnage_commandes: { haut: 'z', bas: 's', gauche: 'q', droite: 'd', interagir: 'e', axe: 'libre', vitesse: 4 } }; + const el = fakeSceneEl(); + withPersonnageObject(obj, { 15: el }, () => { + global.heldKeys = new Set(); + forgeRunPersonnageControllerTick({ elements: [obj] }); + assert.equal(el.style.left, '100px'); + assert.equal(el.style.top, '100px'); + }); +}); + +test('tick — ignore tout objet qui n\'est pas kind="personnage" ou sans commandes résolues', () => { + const decor = { id: 16, kind: 'decor' }; + withPersonnageObject(decor, {}, () => { + global.heldKeys = new Set(['d']); + // Ne doit pas lever (aucun élément DOM trouvé pour cet objet, mais on + // ne devrait même pas tenter de le chercher vu qu'il n'est pas un personnage). + assert.doesNotThrow(() => forgeRunPersonnageControllerTick({ elements: [decor] })); + }); +}); diff --git a/static/js/play/personnage-controller.js b/static/js/play/personnage-controller.js new file mode 100644 index 00000000..066a11bf --- /dev/null +++ b/static/js/play/personnage-controller.js @@ -0,0 +1,119 @@ +// ---------- Personnage jouable — déplacement/animation automatiques ---------- +// N'importe quel objet de scène kind="personnage" se déplace et s'anime +// TOUT SEUL avec ses touches (ZQSD + E par défaut — voir "Commandes" du +// panneau de propriétés, templates/scene_edit.html) dès qu'il est posé sur +// la scène, sans le moindre nœud de flow à poser — contrairement au reste +// du moteur, entièrement piloté par le graphe de logique. Réutilise +// heldKeys (déjà tenu à jour par bindHeldKeyTriggers, triggers.js) pour +// savoir quelles touches sont enfoncées, et +// applyObjectProperty/clampSceneObjectPosition/runSpriteAnimation/ +// resolveSpriteFrames (actions.js) pour le déplacement/l'animation — même +// mécanique que "Modifier un objet de scène"/"Jouer une animation", +// jamais dupliquée. + +var activePersonnageControllerInterval = null; +var activePersonnageInteractListener = null; + +function forgePersonnageAnimExists(elementId, animName) { + var byAnim = (gameData.personnage_animations || {})[elementId]; + return !!(byAnim && byAnim[animName] && byAnim[animName].length); +} + +// "walk" est la SEULE animation de déplacement du catalogue Forge/CraftPix +// (voir screens/labels/sprite_library.py — aucune variante directionnelle +// walk_haut/walk_bas...) : le sens se voit via l'inversion miroir +// (orientation, déjà existante — Phase 7), pas une frame différente. +// Absente (personnage sans cette pose) : le déplacement continue sans +// animation plutôt que de planter, comme le reste du moteur. +function forgePersonnageMovementAnim(elementId) { + return forgePersonnageAnimExists(elementId, 'walk') ? 'walk' : null; +} + +function forgePersonnageIdleAnim(elementId) { + return forgePersonnageAnimExists(elementId, 'idle') ? 'idle' : null; +} + +function forgeStopPersonnageControllers() { + if (activePersonnageControllerInterval) { + clearInterval(activePersonnageControllerInterval); + activePersonnageControllerInterval = null; + } + if (activePersonnageInteractListener) { + window.removeEventListener('keydown', activePersonnageInteractListener); + activePersonnageInteractListener = null; + } +} + +// 50ms (20 fois/seconde) — même cadence que runScreenHeldKeyTriggers +// (triggers.js), pour un déplacement perçu comme continu. +function forgeRunPersonnageControllerTick(screenData) { + (screenData.elements || []).forEach(function (obj) { + if (obj.kind !== 'personnage' || !obj.personnage_commandes) return; + var cmd = obj.personnage_commandes; + var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]'); + if (!targetEl) return; + + var dx = 0, dy = 0; + if (cmd.axe !== 'vertical') { + if (heldKeys.has(cmd.gauche)) dx -= cmd.vitesse; + if (heldKeys.has(cmd.droite)) dx += cmd.vitesse; + } + if (cmd.axe !== 'horizontal') { + if (heldKeys.has(cmd.haut)) dy -= cmd.vitesse; + if (heldKeys.has(cmd.bas)) dy += cmd.vitesse; + } + + if (dx === 0 && dy === 0) { + var idleAnim = forgePersonnageIdleAnim(obj.id); + if (idleAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: idleAnim, fps: 4, loop: true })); + return; + } + if (dx !== 0) { + applyObjectProperty(targetEl, 'orientation', dx < 0 ? 'gauche' : 'droite'); + applyObjectProperty(targetEl, 'pos_x_relatif', dx); + } + if (dy !== 0) applyObjectProperty(targetEl, 'pos_y_relatif', dy); + var moveAnim = forgePersonnageMovementAnim(obj.id); + if (moveAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: moveAnim, fps: 8, loop: true })); + }); +} + +// Ponctuel (keydown, pas heldKeys) — contrairement au déplacement, +// "interagir" est une action instantanée (ouvrir un coffre, parler à un +// PNJ...), jamais un mouvement continu. +function forgeStartPersonnageInteractListener(screenData) { + activePersonnageInteractListener = function (ev) { + (screenData.elements || []).forEach(function (obj) { + if (obj.kind !== 'personnage' || !obj.personnage_commandes) return; + if (obj.personnage_commandes.interagir !== ev.key) return; + var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]'); + if (!targetEl) return; + var interactAnim = forgePersonnageAnimExists(obj.id, 'interact') ? 'interact' : null; + if (interactAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: interactAnim, fps: 8, loop: false })); + }); + }; + window.addEventListener('keydown', activePersonnageInteractListener); +} + +// Même patron PAR ÉCRAN que runScreenHeldKeyTriggers/runScreenTimerTriggers +// (triggers.js/screens.js) : n'existe que tant que l'écran est affiché, +// réinitialisé à chaque changement d'écran (voir showScreen()). +function forgeStartPersonnageControllers(screenData) { + forgeStopPersonnageControllers(); + if (!screenData || screenData.kind !== 'jeu_2d') return; + var hasPersonnage = (screenData.elements || []).some(function (o) { return o.kind === 'personnage'; }); + if (!hasPersonnage) return; + activePersonnageControllerInterval = setInterval(function () { forgeRunPersonnageControllerTick(screenData); }, 50); + forgeStartPersonnageInteractListener(screenData); +} + +// static/js/play/__tests__/ (node:test) — pas de risque en navigateur : +// `module` n'existe pas là-bas, cette branche ne s'exécute jamais côté +// client. +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, + forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers, + forgeStartPersonnageInteractListener, + }; +} diff --git a/static/js/play/screens.js b/static/js/play/screens.js index 4f1d0220..d6c8880b 100644 --- a/static/js/play/screens.js +++ b/static/js/play/screens.js @@ -49,6 +49,10 @@ function showScreen(id) { runScreenHeldKeyTriggers(id); runAnimationTimeline(id); runScreenBackgroundMusic(id); + // Déplacement/animation automatiques de tout personnage posé sur cet + // écran (voir static/js/play/personnage-controller.js) — n'a d'effet + // que sur un écran "jeu_2d" contenant au moins un objet kind="personnage". + forgeStartPersonnageControllers(screensData.find(function(s) { return s.id === id; })); } // Phase 6 — musique de fond par écran (voir screen-bg-music-url dans diff --git a/static/js/scenes/scene-editor.js b/static/js/scenes/scene-editor.js index ab50cbed..ce6fdc60 100644 --- a/static/js/scenes/scene-editor.js +++ b/static/js/scenes/scene-editor.js @@ -143,3 +143,35 @@ function swapSceneObjectCharacter(forgeCharacter) { } }); } + +// "Commandes" (voir static/js/play/personnage-controller.js) : touches de +// déplacement/interaction + blocage d'axe d'un personnage — un seul +// formulaire, enregistré en AJAX à chaque changement de champ, même +// patron que swapSceneObjectCharacter() ci-dessus (POST JSON vers +// routes/scenes/scene_object_commands.py, aucun rechargement de page). +function saveSceneObjectCommands() { + var body = document.getElementById('personnageCommandsBody'); + if (!body) return; + var objectId = parseInt(body.dataset.elementId, 10); + fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/commandes', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + haut: document.getElementById('cmd-haut').value, + bas: document.getElementById('cmd-bas').value, + gauche: document.getElementById('cmd-gauche').value, + droite: document.getElementById('cmd-droite').value, + interagir: document.getElementById('cmd-interagir').value, + axe: document.getElementById('cmd-axe').value, + vitesse: document.getElementById('cmd-vitesse').value, + }), + }); +} + +(function () { + var body = document.getElementById('personnageCommandsBody'); + if (!body) return; + ['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach(function (id) { + var el = document.getElementById(id); + if (el) el.addEventListener('change', saveSceneObjectCommands); + }); +})(); diff --git a/templates/play.html b/templates/play.html index 309a434c..fc54cf3e 100644 --- a/templates/play.html +++ b/templates/play.html @@ -147,6 +147,7 @@ {% endif %} +