Phase 6 : son (musique de fond par écran + action "Jouer un son")
Build and deploy / test-python (push) Successful in 1m27s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

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:
william
2026-08-30 17:56:49 +02:00
co-authored by Claude Sonnet 5
parent 9e263435e6
commit 76331f9285
12 changed files with 269 additions and 1 deletions
+7
View File
@@ -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();
+21
View File
@@ -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 <style> de play.html : la
+9
View File
@@ -113,6 +113,8 @@ function nodeLabel(node) {
base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »';
} else if (node.action_type === 'ajouter_ligne') {
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
} else if (node.action_type === 'jouer_son') {
base += ' « ' + (node.data_value || '?') + ' »';
}
return base;
}
@@ -497,7 +499,9 @@ function toggleFlowActionFields() {
var waitFields = document.getElementById('nf-action-wait-fields');
var eventFields = document.getElementById('nf-action-event-fields');
var addRowFields = document.getElementById('nf-action-add-row-fields');
var soundFields = document.getElementById('nf-action-sound-fields');
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';
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? '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
// FLOW_LAST_INSERTED_ROW_ID ("➕ Dernière ligne ajoutée").
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;
}
}
+63
View File
@@ -21,6 +21,7 @@ function initBuilderPanel() {
})();
bindAspectButtons();
bindScreenBackgroundMusicField();
document.querySelectorAll('.uploadInput').forEach(function (input) {
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();
bindJaugeDefinitionSelect();
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à
// fait. Appelé depuis initBuilderPanel() (voir plus haut), une seule fois
// 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() {
document.querySelectorAll('.aspectBtn').forEach(function (btn) {
btn.addEventListener('click', function () {