From 76331f928578908b8ca826a2bcb2cb8c84fc2148 Mon Sep 17 00:00:00 2001 From: william Date: Sun, 30 Aug 2026 17:56:49 +0200 Subject: [PATCH] =?UTF-8?q?Phase=206=20:=20son=20(musique=20de=20fond=20pa?= =?UTF-8?q?r=20=C3=A9cran=20+=20action=20"Jouer=20un=20son")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajoute deux mécanismes distincts, à la demande de l'utilisateur qui a précisé qu'un son doit pouvoir être attaché à une SCÈNE (pas seulement joué ponctuellement par une action de flow, comme prévu initialement) : - Musique de fond par écran (nouveau champ background_music_url sur _screens, ALTER TABLE nullable) : réglée dans le panneau gauche de l'éditeur (URL ou envoi de fichier, réutilise la route d'upload générique existante), enregistrée en AJAX au même patron que le format d'aperçu (screen_set_aspect.py). Démarrée en boucle à l'affichage de l'écran et arrêtée au changement d'écran (runScreenBackgroundMusic(), appelée depuis showScreen() dans static/js/play/screens.js) — un seul Audio actif à la fois, jamais cumulé avec une musique restée d'un écran précédent. - Action de flow "jouer_son" (aux côtés des actions existantes) : effet sonore ponctuel, non bouclé, déclenchable sur n'importe quel nœud Déclencheur. Réutilise data_value (déjà un champ texte générique sur le nœud action, comme pour "attendre") plutôt qu'une nouvelle colonne dédiée — fire-and-forget côté client (runActionNode), ne bloque jamais la suite du graphe. Les deux réutilisent le même mécanisme d'upload de fichier déjà en place ailleurs dans l'éditeur (ex. source d'une vidéo), sans nouvelle route. Ceci complète les 6 phases du plan d'extension du moteur (état par joueur, hasard/maths, clavier/minuteur, ajout de ligne, position/ collision, son) : Forge Engine peut désormais couvrir des jeux bien au-delà du narratif/puzzle/quiz (action, arcade, jeux à contrainte de temps). Co-Authored-By: Claude Sonnet 5 --- routes/screens/__init__.py | 2 +- routes/screens/screen_set_background_music.py | 15 +++ screens/__init__.py | 1 + screens/labels/action_type_labels.py | 1 + screens/screens_repo/ensure_schema.py | 7 ++ .../set_screen_background_music.py | 17 ++++ static/js/play/actions.js | 7 ++ static/js/play/screens.js | 21 ++++ static/js/screen_edit/flow-editor.js | 9 ++ static/js/screen_edit/panel-init.js | 63 ++++++++++++ templates/screen_edit.html | 31 ++++++ tests/test_sound.py | 96 +++++++++++++++++++ 12 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 routes/screens/screen_set_background_music.py create mode 100644 screens/screens_repo/set_screen_background_music.py create mode 100644 tests/test_sound.py diff --git a/routes/screens/__init__.py b/routes/screens/__init__.py index adfe0646..6912fe9e 100644 --- a/routes/screens/__init__.py +++ b/routes/screens/__init__.py @@ -1 +1 @@ -from . import screens_new, screen_rename, screen_move, screen_delete, screen_edit, screen_set_aspect +from . import screens_new, screen_rename, screen_move, screen_delete, screen_edit, screen_set_aspect, screen_set_background_music diff --git a/routes/screens/screen_set_background_music.py b/routes/screens/screen_set_background_music.py new file mode 100644 index 00000000..8b8e5676 --- /dev/null +++ b/routes/screens/screen_set_background_music.py @@ -0,0 +1,15 @@ +from flask import request, jsonify + +import screens + +from core.flask_app import app + + +@app.route("/game//screens//background-music", methods=["POST"]) +def screen_set_background_music(slug, screen_id): + """Mémorise l'URL de musique de fond de cet écran, en AJAX — même patron + que screen_set_aspect.""" + body = request.get_json(silent=True) or {} + url = body.get("url", "") + screens.set_screen_background_music(slug, screen_id, url) + return jsonify({"ok": True}) diff --git a/screens/__init__.py b/screens/__init__.py index eecae938..c66966d6 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -71,6 +71,7 @@ from .screens_repo.delete_screen import delete_screen from .screens_repo.move_screen import move_screen from .screens_repo.screens_referencing import screens_referencing from .screens_repo.set_screen_aspect import set_screen_aspect +from .screens_repo.set_screen_background_music import set_screen_background_music from .elements.list_elements import list_elements from .elements.get_element import get_element diff --git a/screens/labels/action_type_labels.py b/screens/labels/action_type_labels.py index aa24a6a1..7200e82e 100644 --- a/screens/labels/action_type_labels.py +++ b/screens/labels/action_type_labels.py @@ -10,5 +10,6 @@ ACTION_TYPE_LABELS = { "attendre": "Attendre quelques secondes avant de continuer", "declencher_evenement": "Déclencher un événement", "ajouter_ligne": "Ajouter une ligne à un objet", + "jouer_son": "Jouer un son", "rien": "Ne rien faire", } diff --git a/screens/screens_repo/ensure_schema.py b/screens/screens_repo/ensure_schema.py index 49dff2bb..6ea6a26c 100644 --- a/screens/screens_repo/ensure_schema.py +++ b/screens/screens_repo/ensure_schema.py @@ -107,6 +107,13 @@ def ensure_schema(slug): # que la fenêtre du joueur n'a pas la même proportion que celle # utilisée à la conception. conn.execute("ALTER TABLE _screens ADD COLUMN preview_aspect TEXT NOT NULL DEFAULT '9/16'") + if "background_music_url" not in screen_cols: + # Phase 6 — musique de fond de cet écran (URL) : démarrée en boucle + # à l'affichage de l'écran, arrêtée au changement d'écran (voir + # runScreenBackgroundMusic() dans static/js/play/screens.js). + # Indépendante de l'action "Jouer un son" (effet sonore ponctuel, + # non bouclé, sur n'importe quel déclencheur). + conn.execute("ALTER TABLE _screens ADD COLUMN background_music_url TEXT") action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()} for col_name, col_def in [ ("target_element_id", "INTEGER REFERENCES _screen_elements(id)"), diff --git a/screens/screens_repo/set_screen_background_music.py b/screens/screens_repo/set_screen_background_music.py new file mode 100644 index 00000000..f1c6eeec --- /dev/null +++ b/screens/screens_repo/set_screen_background_music.py @@ -0,0 +1,17 @@ +import db + +from .ensure_schema import ensure_schema + + +def set_screen_background_music(slug, screen_id, url): + """Mémorise l'URL de musique de fond de cet écran (voir le commentaire + sur la colonne background_music_url dans ensure_schema.py). Chaîne vide + -> NULL (pas de musique), comme un simple champ texte qu'on vide.""" + ensure_schema(slug) + conn = db.connect(slug) + conn.execute( + "UPDATE _screens SET background_music_url = ? WHERE id = ?", + (url or None, screen_id), + ) + conn.commit() + conn.close() diff --git a/static/js/play/actions.js b/static/js/play/actions.js index afddc87e..b4676890 100644 --- a/static/js/play/actions.js +++ b/static/js/play/actions.js @@ -220,6 +220,13 @@ function runActionNode(node) { window.lastInsertedRowId = json.row_id; return refreshRuntimeData(); }); + } else if (node.action_type === 'jouer_son') { + // Phase 6 — effet sonore ponctuel : non bouclé, indépendant de la + // musique de fond de l'écran (runScreenBackgroundMusic() dans + // screens.js, qui elle boucle et s'arrête au changement d'écran). + // Fire-and-forget : ne bloque jamais la suite du graphe. + if (node.data_value) new Audio(node.data_value).play().catch(function() {}); + return Promise.resolve(); } // 'rien' : aucune action volontairement. return Promise.resolve(); diff --git a/static/js/play/screens.js b/static/js/play/screens.js index cfc644ee..66cb69c7 100644 --- a/static/js/play/screens.js +++ b/static/js/play/screens.js @@ -41,6 +41,27 @@ function showScreen(id) { runScreenShowTriggers(id); runScreenTimerTriggers(id); runAnimationTimeline(id); + runScreenBackgroundMusic(id); +} + +// Phase 6 — musique de fond par écran (voir screen-bg-music-url dans +// screen_edit.html) : démarrée en boucle à l'affichage de l'écran, +// arrêtée au changement d'écran — un seul Audio actif à la fois (pas un +// tableau comme activeTimerTriggerIntervals dans triggers.js, une +// musique de fond ne se cumule jamais avec une autre). +var currentBackgroundAudio = null; + +function runScreenBackgroundMusic(screenId) { + if (currentBackgroundAudio) { + currentBackgroundAudio.pause(); + currentBackgroundAudio = null; + } + var screenData = screensData.find(function(s) { return String(s.id) === String(screenId); }); + var url = screenData && screenData.background_music_url; + if (!url) return; + currentBackgroundAudio = new Audio(url); + currentBackgroundAudio.loop = true; + currentBackgroundAudio.play().catch(function() {}); } // Voir le commentaire sur #playFrame dans le