Phase 6 : son (musique de fond par écran + action "Jouer un son")
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9e263435e6
commit
76331f9285
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from flask import request, jsonify
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/game/<slug>/screens/<int:screen_id>/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})
|
||||||
@@ -71,6 +71,7 @@ from .screens_repo.delete_screen import delete_screen
|
|||||||
from .screens_repo.move_screen import move_screen
|
from .screens_repo.move_screen import move_screen
|
||||||
from .screens_repo.screens_referencing import screens_referencing
|
from .screens_repo.screens_referencing import screens_referencing
|
||||||
from .screens_repo.set_screen_aspect import set_screen_aspect
|
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.list_elements import list_elements
|
||||||
from .elements.get_element import get_element
|
from .elements.get_element import get_element
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ ACTION_TYPE_LABELS = {
|
|||||||
"attendre": "Attendre quelques secondes avant de continuer",
|
"attendre": "Attendre quelques secondes avant de continuer",
|
||||||
"declencher_evenement": "Déclencher un événement",
|
"declencher_evenement": "Déclencher un événement",
|
||||||
"ajouter_ligne": "Ajouter une ligne à un objet",
|
"ajouter_ligne": "Ajouter une ligne à un objet",
|
||||||
|
"jouer_son": "Jouer un son",
|
||||||
"rien": "Ne rien faire",
|
"rien": "Ne rien faire",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ def ensure_schema(slug):
|
|||||||
# que la fenêtre du joueur n'a pas la même proportion que celle
|
# que la fenêtre du joueur n'a pas la même proportion que celle
|
||||||
# utilisée à la conception.
|
# utilisée à la conception.
|
||||||
conn.execute("ALTER TABLE _screens ADD COLUMN preview_aspect TEXT NOT NULL DEFAULT '9/16'")
|
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()}
|
action_cols = {r["name"] for r in conn.execute("PRAGMA table_info(_actions)").fetchall()}
|
||||||
for col_name, col_def in [
|
for col_name, col_def in [
|
||||||
("target_element_id", "INTEGER REFERENCES _screen_elements(id)"),
|
("target_element_id", "INTEGER REFERENCES _screen_elements(id)"),
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -220,6 +220,13 @@ function runActionNode(node) {
|
|||||||
window.lastInsertedRowId = json.row_id;
|
window.lastInsertedRowId = json.row_id;
|
||||||
return refreshRuntimeData();
|
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.
|
// 'rien' : aucune action volontairement.
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|||||||
@@ -41,6 +41,27 @@ function showScreen(id) {
|
|||||||
runScreenShowTriggers(id);
|
runScreenShowTriggers(id);
|
||||||
runScreenTimerTriggers(id);
|
runScreenTimerTriggers(id);
|
||||||
runAnimationTimeline(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 <style> de play.html : la
|
// Voir le commentaire sur #playFrame dans le <style> de play.html : la
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ function nodeLabel(node) {
|
|||||||
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
|
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
|
||||||
} else if (node.action_type === 'ajouter_ligne') {
|
} else if (node.action_type === 'ajouter_ligne') {
|
||||||
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
|
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
|
||||||
|
} else if (node.action_type === 'jouer_son') {
|
||||||
|
base += ' « ' + (node.data_value || '?') + ' »';
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
@@ -497,7 +499,9 @@ function toggleFlowActionFields() {
|
|||||||
var waitFields = document.getElementById('nf-action-wait-fields');
|
var waitFields = document.getElementById('nf-action-wait-fields');
|
||||||
var eventFields = document.getElementById('nf-action-event-fields');
|
var eventFields = document.getElementById('nf-action-event-fields');
|
||||||
var addRowFields = document.getElementById('nf-action-add-row-fields');
|
var addRowFields = document.getElementById('nf-action-add-row-fields');
|
||||||
|
var soundFields = document.getElementById('nf-action-sound-fields');
|
||||||
var isOnglet = typeSel.value === 'activer_onglet';
|
var isOnglet = typeSel.value === 'activer_onglet';
|
||||||
|
soundFields.style.display = typeSel.value === 'jouer_son' ? 'block' : 'none';
|
||||||
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
||||||
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
||||||
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
||||||
@@ -686,6 +690,11 @@ function submitNodeForm() {
|
|||||||
// plusieurs nœuds "Modifier une donnée" enchaînés, ciblant
|
// plusieurs nœuds "Modifier une donnée" enchaînés, ciblant
|
||||||
// FLOW_LAST_INSERTED_ROW_ID ("➕ Dernière ligne ajoutée").
|
// FLOW_LAST_INSERTED_ROW_ID ("➕ Dernière ligne ajoutée").
|
||||||
payload.target_definition_id = parseInt(document.getElementById('nf-action-add-row-definition').value, 10);
|
payload.target_definition_id = parseInt(document.getElementById('nf-action-add-row-definition').value, 10);
|
||||||
|
} else if (actionType === 'jouer_son') {
|
||||||
|
// Phase 6 — réutilise data_value (déjà un simple champ texte générique
|
||||||
|
// sur le nœud action, comme pour "attendre") plutôt qu'une nouvelle
|
||||||
|
// colonne dédiée.
|
||||||
|
payload.data_value = document.getElementById('nf-action-sound-url').value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function initBuilderPanel() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
bindAspectButtons();
|
bindAspectButtons();
|
||||||
|
bindScreenBackgroundMusicField();
|
||||||
|
|
||||||
document.querySelectorAll('.uploadInput').forEach(function (input) {
|
document.querySelectorAll('.uploadInput').forEach(function (input) {
|
||||||
input.addEventListener('change', function () {
|
input.addEventListener('change', function () {
|
||||||
@@ -46,6 +47,29 @@ function initBuilderPanel() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Phase 6 — champ d'upload DANS le formulaire de nœud (action "Jouer un
|
||||||
|
// son") : contrairement à .uploadInput (panneau de propriétés d'un
|
||||||
|
// élément), il n'y a pas de formulaire à autosauvegarder ici — le champ
|
||||||
|
// texte est simplement rempli, la valeur part avec le reste du nœud au
|
||||||
|
// clic sur "Créer" (voir submitNodeForm()).
|
||||||
|
document.querySelectorAll('.nodeFormUploadInput').forEach(function (input) {
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
if (!input.files.length) return;
|
||||||
|
var status = input.parentElement.querySelector('.uploadStatus');
|
||||||
|
var targetField = document.getElementById(input.dataset.target);
|
||||||
|
status.textContent = 'Envoi en cours...';
|
||||||
|
var data = new FormData();
|
||||||
|
data.append('file', input.files[0]);
|
||||||
|
fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (json) {
|
||||||
|
if (json.url) { targetField.value = json.url; status.textContent = 'Fichier envoyé ✓'; }
|
||||||
|
else { status.textContent = "Échec de l'envoi."; }
|
||||||
|
})
|
||||||
|
.catch(function () { status.textContent = "Échec de l'envoi."; });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
bindPropsAutosave();
|
bindPropsAutosave();
|
||||||
bindJaugeDefinitionSelect();
|
bindJaugeDefinitionSelect();
|
||||||
bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']);
|
bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']);
|
||||||
@@ -389,6 +413,45 @@ function syncAlign(radio) {
|
|||||||
// redemandent ce panneau au serveur, qui doit donc connaître le choix déjà
|
// redemandent ce panneau au serveur, qui doit donc connaître le choix déjà
|
||||||
// fait. Appelé depuis initBuilderPanel() (voir plus haut), une seule fois
|
// fait. Appelé depuis initBuilderPanel() (voir plus haut), une seule fois
|
||||||
// par (re)rendu de ce panneau.
|
// par (re)rendu de ce panneau.
|
||||||
|
// Phase 6 — musique de fond de cet écran : enregistrement en AJAX au
|
||||||
|
// blur/changement (pas d'autosave "au caractère près", même choix que le
|
||||||
|
// format d'aperçu ci-dessous), plus un bouton d'envoi de fichier qui
|
||||||
|
// remplit le champ URL puis déclenche le même enregistrement.
|
||||||
|
function saveScreenBackgroundMusic(url) {
|
||||||
|
return fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/background-music", {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url: url }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindScreenBackgroundMusicField() {
|
||||||
|
var urlInput = document.getElementById('screen-bg-music-url');
|
||||||
|
if (urlInput) {
|
||||||
|
urlInput.addEventListener('change', function () { saveScreenBackgroundMusic(urlInput.value); });
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.screenUploadInput').forEach(function (input) {
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
if (!input.files.length) return;
|
||||||
|
var status = input.parentElement.querySelector('.uploadStatus');
|
||||||
|
var targetField = document.getElementById(input.dataset.target);
|
||||||
|
status.textContent = 'Envoi en cours...';
|
||||||
|
var data = new FormData();
|
||||||
|
data.append('file', input.files[0]);
|
||||||
|
fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (json) {
|
||||||
|
if (json.url) {
|
||||||
|
targetField.value = json.url;
|
||||||
|
status.textContent = 'Fichier envoyé ✓ Enregistrement...';
|
||||||
|
saveScreenBackgroundMusic(json.url).then(function () { status.textContent = 'Fichier envoyé ✓ Enregistré.'; });
|
||||||
|
}
|
||||||
|
else { status.textContent = "Échec de l'envoi."; }
|
||||||
|
})
|
||||||
|
.catch(function () { status.textContent = "Échec de l'envoi."; });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function bindAspectButtons() {
|
function bindAspectButtons() {
|
||||||
document.querySelectorAll('.aspectBtn').forEach(function (btn) {
|
document.querySelectorAll('.aspectBtn').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
|
|||||||
@@ -70,6 +70,22 @@
|
|||||||
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '16/9' else '' }}" data-aspect="16/9">🖥️ Paysage</button>
|
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '16/9' else '' }}" data-aspect="16/9">🖥️ Paysage</button>
|
||||||
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '1/1' else '' }}" data-aspect="1/1">◻️ Carré</button>
|
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '1/1' else '' }}" data-aspect="1/1">◻️ Carré</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Phase 6 — musique de fond de cet écran : démarrée en boucle à
|
||||||
|
l'affichage, arrêtée au changement d'écran (voir
|
||||||
|
runScreenBackgroundMusic() dans static/js/play/screens.js).
|
||||||
|
Enregistrement en AJAX au blur/changement, même patron que le
|
||||||
|
format d'aperçu ci-dessus. -->
|
||||||
|
<div class="field" style="margin-bottom:8px;">
|
||||||
|
<label class="label is-small" for="screen-bg-music-url">🎵 Musique de fond</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="text" class="input is-small" id="screen-bg-music-url" placeholder="URL du fichier audio" value="{{ screen.background_music_url or '' }}">
|
||||||
|
</div>
|
||||||
|
<div class="control" style="margin-top:4px;">
|
||||||
|
<label for="screen-bg-music-upload" style="font-size:12px;">Envoyer un fichier :</label>
|
||||||
|
<input type="file" class="screenUploadInput" id="screen-bg-music-upload" data-target="screen-bg-music-url" accept="audio/*" style="font-size:12px;">
|
||||||
|
<span class="uploadStatus" style="font-size:12px; color:var(--muted);"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr style="border-color:var(--border); margin:10px 0;">
|
<hr style="border-color:var(--border); margin:10px 0;">
|
||||||
<h2 class="subtitle is-6">Éléments de cet écran</h2>
|
<h2 class="subtitle is-6">Éléments de cet écran</h2>
|
||||||
{% if top_level_elements %}
|
{% if top_level_elements %}
|
||||||
@@ -469,6 +485,21 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Phase 6 — effet sonore ponctuel (non bouclé, joué une seule
|
||||||
|
fois), sur n'importe quel déclencheur. Indépendant de la
|
||||||
|
musique de fond par écran (voir screen_edit.html, panneau
|
||||||
|
gauche) qui, elle, boucle et suit le cycle de vie de l'écran. -->
|
||||||
|
<div id="nf-action-sound-fields" style="display:none;">
|
||||||
|
<label>URL du son
|
||||||
|
<input type="text" id="nf-action-sound-url" placeholder="URL du fichier audio">
|
||||||
|
</label>
|
||||||
|
<div style="margin-top:6px;">
|
||||||
|
<label for="nf-action-sound-upload" style="font-size:12px;">Envoyer un fichier :</label>
|
||||||
|
<input type="file" class="nodeFormUploadInput" id="nf-action-sound-upload" data-target="nf-action-sound-url" accept="audio/*" style="font-size:12px;">
|
||||||
|
<span class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="nf-action-variable-fields" style="display:none;">
|
<div id="nf-action-variable-fields" style="display:none;">
|
||||||
{% if not global_variables %}
|
{% if not global_variables %}
|
||||||
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
|
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""Son — Phase 6 du plan d'extension du moteur : musique de fond par écran
|
||||||
|
(en boucle, suit le cycle de vie de l'écran) et action ponctuelle "Jouer un
|
||||||
|
son" (effet sonore, non bouclé). Comme le reste du graphe de logique côté
|
||||||
|
client, la lecture audio RÉELLE n'est pas testable sans navigateur (voir
|
||||||
|
play_js_bundle() — on vérifie que la logique existe dans le JS livré et que
|
||||||
|
les données persistent, pas qu'elles jouent un son)."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
from conftest import play_js_bundle, screen_edit_js_bundle
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
|
||||||
|
def _create_screen(client, slug, name="Accueil"):
|
||||||
|
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
||||||
|
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
||||||
|
|
||||||
|
|
||||||
|
def _add_flow_node(client, slug, screen_id, **fields):
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{slug}/screens/{screen_id}/flow/nodes/add",
|
||||||
|
data=json.dumps(fields),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
return resp.get_json()["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_background_music_persists(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
resp = client.post(
|
||||||
|
f"/game/{game}/screens/{screen_id}/background-music",
|
||||||
|
data=json.dumps({"url": "https://example.com/song.mp3"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
screen = screens.get_screen(game, screen_id)
|
||||||
|
assert screen["background_music_url"] == "https://example.com/song.mp3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_background_music_empty_string_clears_it(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
client.post(
|
||||||
|
f"/game/{game}/screens/{screen_id}/background-music",
|
||||||
|
data=json.dumps({"url": "https://example.com/song.mp3"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
f"/game/{game}/screens/{screen_id}/background-music",
|
||||||
|
data=json.dumps({"url": ""}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
screen = screens.get_screen(game, screen_id)
|
||||||
|
assert screen["background_music_url"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_payload_exposes_background_music_per_screen(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
client.post(
|
||||||
|
f"/game/{game}/screens/{screen_id}/background-music",
|
||||||
|
data=json.dumps({"url": "https://example.com/song.mp3"}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
html = client.get(f"/game/{game}/runtime-payload").get_json()
|
||||||
|
screen_payload = next(s for s in html["screens"] if s["id"] == screen_id)
|
||||||
|
assert screen_payload["background_music_url"] == "https://example.com/song.mp3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sound_action_node_persists_url(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
node_id = _add_flow_node(
|
||||||
|
client, game, screen_id, node_type="action", action_type="jouer_son",
|
||||||
|
data_value="https://example.com/effect.wav",
|
||||||
|
)
|
||||||
|
node = screens.get_flow_node(game, node_id)
|
||||||
|
assert node["action_type"] == "jouer_son"
|
||||||
|
assert node["data_value"] == "https://example.com/effect.wav"
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_page_exposes_sound_runtime(client, game):
|
||||||
|
js = play_js_bundle()
|
||||||
|
assert "runScreenBackgroundMusic" in js
|
||||||
|
assert "'jouer_son'" in js
|
||||||
|
assert "new Audio" in js
|
||||||
|
|
||||||
|
|
||||||
|
def test_screen_edit_exposes_sound_editors(client, game):
|
||||||
|
screen_id = _create_screen(client, game)
|
||||||
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
||||||
|
assert "screen-bg-music-url" in html
|
||||||
|
assert "nf-action-sound-fields" in html
|
||||||
|
assert "nf-action-sound-url" in html
|
||||||
|
js = screen_edit_js_bundle()
|
||||||
|
assert "jouer_son" in js
|
||||||
|
assert "bindScreenBackgroundMusicField" in js
|
||||||
Reference in New Issue
Block a user