Phase 7 : personnage animé par sprites (poses/images successives)
Ajoute une nouvelle action de flow "jouer_animation_sprite" qui joue une
séquence de PNG sur un élément image (cycle en boucle type marche, ou
une fois type saut) — réutilise target_element_id (déjà whitelisté) et
data_value en JSON {frames, fps, loop}, exactement le patron déjà établi
par "jouer_son" (Phase 6) et "alea" (Phase 2) : aucune nouvelle colonne,
aucune migration de schéma.
Ajoute une propriété d'élément "orientation" (Modifier un élément) pour
retourner un personnage en miroir (gauche/droite/bascule) sans nécessiter
une 2e feuille de sprites "vue de dos" — même patron que "surbrillance"/
"désactivé".
Étend aussi la Timeline d'animation existante (kind="sprite", aux côtés
de "animate_css"/"custom") pour qu'un personnage puisse animer tout seul
dès l'affichage de l'écran (ex. idle en boucle perpétuelle), pas
seulement en réaction à un événement — réutilise la colonne générique
custom_keyframes (JSON) et le réglage iteration_count déjà là pour la
boucle, aucune migration non plus. Les deux entrées (action de flow et
clip de timeline) partagent le même moteur côté client
(runSpriteAnimation()/activeSpriteAnimations dans static/js/play/
actions.js, indexé par nœud DOM plutôt que par id d'élément pour
supporter plusieurs instances d'un même écran-modèle animées
indépendamment).
Une bibliothèque de sprites Forge (2 personnages Kenney CC0, sous-
ensemble curé idle/marche/saut copié dans static/characters/) est
proposée dans l'éditeur, mais un créateur peut tout aussi bien utiliser
ses propres sprites uploadés (même route d'upload générique que le son
de la Phase 6).
Périmètre volontairement limité à ce qui a été demandé : pas d'avatar
modulable en couches (assemblage cheveux/haut/bas par le joueur),
écarté du plan initial à la demande de l'utilisateur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -0,0 +1,23 @@
|
||||
|
||||
|
||||
Toon Characters
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
Creation date: 26-09-2019
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
This content is free to use in personal, educational and commercial projects.
|
||||
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
|
||||
|
||||
------------------------------
|
||||
|
||||
Donate: http://support.kenney.nl
|
||||
Request: http://request.kenney.nl
|
||||
Patreon: http://patreon.com/kenney/
|
||||
|
||||
Follow on Twitter for updates:
|
||||
http://twitter.com/KenneyNL
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
@@ -76,6 +76,19 @@ function applyElementProperty(targetEl, property, value) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (property === 'orientation') {
|
||||
// Phase 7 — retourne le personnage en miroir (voir .forgeFlipped dans
|
||||
// play.html) plutôt que de nécessiter une 2e feuille de sprites "vue
|
||||
// de dos" pour chaque sens de déplacement.
|
||||
if (value === 'toggle') {
|
||||
targetEl.classList.toggle('forgeFlipped');
|
||||
} else if (value === 'gauche') {
|
||||
targetEl.classList.add('forgeFlipped');
|
||||
} else {
|
||||
targetEl.classList.remove('forgeFlipped');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (property === 'pos_x_relatif' || property === 'pos_y_relatif') {
|
||||
// Phase 5 — AJOUTE la valeur à la position actuelle plutôt que de
|
||||
// l'écraser (contrairement à pos_x/pos_y, "absolus") : combiné à un
|
||||
@@ -227,7 +240,62 @@ function runActionNode(node) {
|
||||
// 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();
|
||||
} else if (node.action_type === 'jouer_animation_sprite' && node.target_element_id) {
|
||||
const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]');
|
||||
if (targetEl) {
|
||||
let spriteData = {};
|
||||
try { spriteData = JSON.parse(node.data_value || '{}'); } catch (e) { /* data_value invalide : rien à jouer */ }
|
||||
runSpriteAnimation(targetEl, spriteData);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
// 'rien' : aucune action volontairement.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// ---------- Phase 7 — animation de sprites (poses/images successives) ----------
|
||||
// Premier état actif PAR ÉLÉMENT du moteur (contrairement à
|
||||
// activeTimerTriggerIntervals dans triggers.js et currentBackgroundAudio
|
||||
// dans screens.js, qui sont par-écran/singleton) : plusieurs personnages
|
||||
// peuvent animer indépendamment en même temps. Clé = le NŒUD DOM lui-même
|
||||
// (pas element_id) : un clip posé sur un écran-modèle peut viser plusieurs
|
||||
// instances à la fois (ex. chaque ligne d'un Répéteur utilisant ce modèle),
|
||||
// toutes partageant le même element_id — les indexer par élément DOM réel
|
||||
// évite qu'une nouvelle instance n'arrête l'animation d'une autre. Alimenté
|
||||
// par deux entrées équivalentes — l'action de flow "jouer_animation_sprite"
|
||||
// ci-dessus (événementiel) ET un clip de la Timeline d'animation de kind
|
||||
// "sprite" (déclaratif, voir applyAnimationClip() dans screens.js) — les
|
||||
// deux partagent ce même moteur, aucune logique dupliquée.
|
||||
var activeSpriteAnimations = new Map();
|
||||
|
||||
function runSpriteAnimation(targetEl, spriteData) {
|
||||
var frames = spriteData.frames || [];
|
||||
var fps = spriteData.fps > 0 ? spriteData.fps : 8;
|
||||
var loop = !!spriteData.loop;
|
||||
if (activeSpriteAnimations.has(targetEl)) {
|
||||
clearInterval(activeSpriteAnimations.get(targetEl));
|
||||
activeSpriteAnimations.delete(targetEl);
|
||||
}
|
||||
if (!frames.length) return;
|
||||
var frameIndex = 0;
|
||||
targetEl.src = frames[0];
|
||||
if (frames.length === 1) return;
|
||||
var intervalId = setInterval(function() {
|
||||
frameIndex++;
|
||||
if (frameIndex >= frames.length) {
|
||||
if (!loop) { clearInterval(intervalId); activeSpriteAnimations.delete(targetEl); return; }
|
||||
frameIndex = 0;
|
||||
}
|
||||
targetEl.src = frames[frameIndex];
|
||||
}, 1000 / fps);
|
||||
activeSpriteAnimations.set(targetEl, intervalId);
|
||||
}
|
||||
|
||||
// Appelée depuis showScreen() (screens.js) à chaque changement d'écran —
|
||||
// encapsule activeSpriteAnimations ici plutôt que de le manipuler
|
||||
// directement depuis screens.js, même patron que runScreenTimerTriggers()
|
||||
// dans triggers.js (l'appelant ne connaît jamais la structure interne).
|
||||
function stopAllSpriteAnimations() {
|
||||
activeSpriteAnimations.forEach(function(intervalId) { clearInterval(intervalId); });
|
||||
activeSpriteAnimations.clear();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ function showScreen(id) {
|
||||
window.currentScreenId = id;
|
||||
applyScreenAspect(el.dataset.aspect || '9/16');
|
||||
}
|
||||
// Phase 7 — nettoie les animations de sprites de l'écran quitté (sinon
|
||||
// une boucle setInterval reste active sur un élément désormais caché,
|
||||
// mirroring le nettoyage déjà fait pour activeTimerTriggerIntervals
|
||||
// dans triggers.js).
|
||||
stopAllSpriteAnimations();
|
||||
runScreenShowTriggers(id);
|
||||
runScreenTimerTriggers(id);
|
||||
runAnimationTimeline(id);
|
||||
@@ -141,6 +146,15 @@ function applyAnimationClip(clip) {
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
} else if (clip.kind === 'sprite') {
|
||||
// Phase 7 — même moteur que l'action de flow "jouer_animation_sprite"
|
||||
// (voir runSpriteAnimation() dans actions.js) : custom_keyframes
|
||||
// stocke ici {frames, fps} en JSON plutôt que des images-clés CSS
|
||||
// (réutilise la colonne générique existante, pas de nouveau champ).
|
||||
var spriteData = {};
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* invalide : rien à jouer */ }
|
||||
spriteData.loop = infinite;
|
||||
setTimeout(function() { runSpriteAnimation(target, spriteData); }, delay * 1000);
|
||||
} else {
|
||||
var animClass = 'animate__' + clip.animate_name;
|
||||
target.style.setProperty('--animate-duration', duration + 's');
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
var ANIM_SCREEN_ID = SCREEN_ID;
|
||||
var ANIM_PX_PER_SEC = 60;
|
||||
var animCurrentClipId = null;
|
||||
// Phase 7 — aperçu d'un clip "sprite" dans l'éditeur (voir previewAnimClip
|
||||
// ci-dessous) : un intervalle à la fois suffit ici (contrairement au
|
||||
// moteur de jeu, static/js/play/actions.js, PAS chargé sur cette page —
|
||||
// runSpriteAnimation()/activeSpriteAnimations n'existent que côté jeu).
|
||||
var animPreviewSpriteInterval = null;
|
||||
|
||||
function animUrl(path) { return "/game/" + GAME_SLUG + path; }
|
||||
|
||||
@@ -165,6 +170,32 @@ function toggleAnimClipKind() {
|
||||
var kind = document.getElementById('anim-clip-kind').value;
|
||||
document.getElementById('anim-clip-animate-css-fields').style.display = kind === 'animate_css' ? 'block' : 'none';
|
||||
document.getElementById('anim-clip-custom-fields').style.display = kind === 'custom' ? 'block' : 'none';
|
||||
document.getElementById('anim-clip-sprite-fields').style.display = kind === 'sprite' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Phase 7 — même mécanisme que onSpriteLibraryCharacterChange()/
|
||||
// applySpriteLibraryAnimation() dans flow-editor.js, pour le formulaire de
|
||||
// clip de Timeline (DOM distinct, pas d'état partagé entre les deux formulaires).
|
||||
function onAnimClipSpriteLibraryCharacterChange() {
|
||||
var charSel = document.getElementById('anim-clip-sprite-library-character');
|
||||
var animSel = document.getElementById('anim-clip-sprite-library-animation');
|
||||
var slug = charSel.value;
|
||||
animSel.innerHTML = '';
|
||||
if (!slug || !SPRITE_LIBRARY[slug]) { animSel.style.display = 'none'; return; }
|
||||
Object.keys(SPRITE_LIBRARY[slug].animations).forEach(function(name) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = name; opt.textContent = name;
|
||||
animSel.appendChild(opt);
|
||||
});
|
||||
animSel.style.display = 'block';
|
||||
applyAnimClipSpriteLibraryAnimation();
|
||||
}
|
||||
|
||||
function applyAnimClipSpriteLibraryAnimation() {
|
||||
var slug = document.getElementById('anim-clip-sprite-library-character').value;
|
||||
var name = document.getElementById('anim-clip-sprite-library-animation').value;
|
||||
if (!slug || !name || !SPRITE_LIBRARY[slug]) return;
|
||||
document.getElementById('anim-clip-sprite-frames').value = SPRITE_LIBRARY[slug].animations[name].join('\n');
|
||||
}
|
||||
|
||||
function addAnimKeyframeRow(percent, cssText) {
|
||||
@@ -229,6 +260,14 @@ function openAnimClipForm(clip) {
|
||||
var cssText = Object.keys(k.props || {}).map(function(p) { return p + ': ' + k.props[p] + ';'; }).join(' ');
|
||||
addAnimKeyframeRow(k.percent, cssText);
|
||||
});
|
||||
// Phase 7 — clip.custom_keyframes stocke {frames, fps} en JSON pour un
|
||||
// clip kind="sprite" (pas une liste d'images-clés CSS, voir ci-dessus).
|
||||
var spriteData = {};
|
||||
if (clip.kind === 'sprite' && clip.custom_keyframes) {
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes); } catch (e) { /* ignore */ }
|
||||
}
|
||||
document.getElementById('anim-clip-sprite-frames').value = (spriteData.frames || []).join('\n');
|
||||
document.getElementById('anim-clip-sprite-fps').value = spriteData.fps || 8;
|
||||
toggleAnimClipKind();
|
||||
document.querySelectorAll('.animClip').forEach(function(b) {
|
||||
b.classList.toggle('selected', parseInt(b.dataset.clipId, 10) === clip.id);
|
||||
@@ -254,6 +293,18 @@ function saveAnimClipForm() {
|
||||
iteration_count: document.getElementById('anim-clip-iteration').value,
|
||||
};
|
||||
if (kind === 'custom') fields.custom_keyframes = collectAnimKeyframes();
|
||||
if (kind === 'sprite') {
|
||||
// Phase 7 — {frames, fps} en JSON dans custom_keyframes (colonne
|
||||
// générique déjà réutilisée par le kind "custom" ci-dessus pour ses
|
||||
// images-clés) ; la boucle est déjà portée par iteration_count
|
||||
// (1/2/3/infinite, ci-dessus) — aucun champ dédié nécessaire.
|
||||
var frames = document.getElementById('anim-clip-sprite-frames').value
|
||||
.split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
|
||||
fields.custom_keyframes = {
|
||||
frames: frames,
|
||||
fps: parseInt(document.getElementById('anim-clip-sprite-fps').value, 10) || 8,
|
||||
};
|
||||
}
|
||||
return updateAnimClip(animCurrentClipId, fields).then(function() { renderAnimTimeline(); });
|
||||
}
|
||||
|
||||
@@ -296,6 +347,22 @@ function previewAnimClip() {
|
||||
target.style.animation = '';
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
} else if (clip.kind === 'sprite') {
|
||||
var spriteData = {};
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* ignore */ }
|
||||
var frames = spriteData.frames || [];
|
||||
var fps = spriteData.fps > 0 ? spriteData.fps : 8;
|
||||
if (animPreviewSpriteInterval) { clearInterval(animPreviewSpriteInterval); animPreviewSpriteInterval = null; }
|
||||
if (frames.length) {
|
||||
var frameIndex = 0;
|
||||
target.src = frames[0];
|
||||
if (frames.length > 1) {
|
||||
animPreviewSpriteInterval = setInterval(function() {
|
||||
frameIndex = (frameIndex + 1) % frames.length;
|
||||
target.src = frames[frameIndex];
|
||||
}, 1000 / fps);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var animClass = 'animate__' + clip.animate_name;
|
||||
target.style.setProperty('--animate-duration', duration + 's');
|
||||
|
||||
@@ -91,6 +91,8 @@ function nodeLabel(node) {
|
||||
base += ' = ' + (ELEMENT_SURBRILLANCE_LABELS_MAP[node.element_value] || node.element_value);
|
||||
} else if (node.element_property === 'desactive') {
|
||||
base += ' = ' + (ELEMENT_DESACTIVE_LABELS_MAP[node.element_value] || node.element_value);
|
||||
} else if (node.element_property === 'orientation') {
|
||||
base += ' = ' + (ELEMENT_ORIENTATION_LABELS_MAP[node.element_value] || node.element_value);
|
||||
} else if (node.element_value && node.element_value.indexOf('||') !== -1) {
|
||||
var vparts = node.element_value.split('||');
|
||||
base += ' = ' + vparts[0] + ' ↔ ' + vparts[1] + ' (va-et-vient)';
|
||||
@@ -115,6 +117,12 @@ function nodeLabel(node) {
|
||||
base += ' → ' + (DEFINITIONS_NAMES[node.target_definition_id] || '?');
|
||||
} else if (node.action_type === 'jouer_son') {
|
||||
base += ' « ' + (node.data_value || '?') + ' »';
|
||||
} else if (node.action_type === 'jouer_animation_sprite') {
|
||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?');
|
||||
try {
|
||||
var spriteData = JSON.parse(node.data_value || '{}');
|
||||
base += ' (' + (spriteData.frames ? spriteData.frames.length : 0) + ' images, ' + (spriteData.fps || '?') + ' i/s' + (spriteData.loop ? ', boucle' : '') + ')';
|
||||
} catch (e) { /* data_value invalide : pas de détail affiché */ }
|
||||
}
|
||||
return base;
|
||||
}
|
||||
@@ -500,8 +508,10 @@ function toggleFlowActionFields() {
|
||||
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 spriteFields = document.getElementById('nf-action-sprite-fields');
|
||||
var isOnglet = typeSel.value === 'activer_onglet';
|
||||
soundFields.style.display = typeSel.value === 'jouer_son' ? 'block' : 'none';
|
||||
spriteFields.style.display = typeSel.value === 'jouer_animation_sprite' ? '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';
|
||||
@@ -528,16 +538,44 @@ function updateDataValuePlaceholder(operationSelectId, valueInputId) {
|
||||
valueInp.placeholder = opSel.value === 'alea' ? 'min,max (ex. 1,6)' : 'Valeur / montant';
|
||||
}
|
||||
|
||||
// Phase 7 — bibliothèque de sprites Forge (SPRITE_LIBRARY, injecté par
|
||||
// screen_edit.html) : choisir un personnage puis une de ses animations
|
||||
// pré-remplit la zone de frames — un créateur qui préfère ses propres
|
||||
// sprites laisse simplement ce sélecteur sur "— Choisir —" et remplit/
|
||||
// uploade directement dans la zone de frames.
|
||||
function onSpriteLibraryCharacterChange() {
|
||||
var charSel = document.getElementById('nf-action-sprite-library-character');
|
||||
var animSel = document.getElementById('nf-action-sprite-library-animation');
|
||||
var slug = charSel.value;
|
||||
animSel.innerHTML = '';
|
||||
if (!slug || !SPRITE_LIBRARY[slug]) { animSel.style.display = 'none'; return; }
|
||||
Object.keys(SPRITE_LIBRARY[slug].animations).forEach(function(name) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = name; opt.textContent = name;
|
||||
animSel.appendChild(opt);
|
||||
});
|
||||
animSel.style.display = 'block';
|
||||
applySpriteLibraryAnimation();
|
||||
}
|
||||
|
||||
function applySpriteLibraryAnimation() {
|
||||
var slug = document.getElementById('nf-action-sprite-library-character').value;
|
||||
var name = document.getElementById('nf-action-sprite-library-animation').value;
|
||||
if (!slug || !name || !SPRITE_LIBRARY[slug]) return;
|
||||
document.getElementById('nf-action-sprite-frames').value = SPRITE_LIBRARY[slug].animations[name].join('\n');
|
||||
}
|
||||
|
||||
function toggleFlowElementPropertyValue() {
|
||||
var propSel = document.getElementById('nf-action-element-property');
|
||||
var visSel = document.getElementById('nf-action-element-value-visibility');
|
||||
var surbrillanceSel = document.getElementById('nf-action-element-value-surbrillance');
|
||||
var desactiveSel = document.getElementById('nf-action-element-value-desactive');
|
||||
var orientationSel = document.getElementById('nf-action-element-value-orientation');
|
||||
var colorInp = document.getElementById('nf-action-element-value-color');
|
||||
var numInp = document.getElementById('nf-action-element-value-number');
|
||||
var textInp = document.getElementById('nf-action-element-value-text');
|
||||
var toggleRow = document.getElementById('nf-action-element-toggle-row');
|
||||
[visSel, surbrillanceSel, desactiveSel, colorInp, numInp, textInp].forEach(function(el){ el.style.display = 'none'; });
|
||||
[visSel, surbrillanceSel, desactiveSel, orientationSel, colorInp, numInp, textInp].forEach(function(el){ el.style.display = 'none'; });
|
||||
if (propSel.value === 'visibilite') {
|
||||
visSel.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
@@ -550,6 +588,10 @@ function toggleFlowElementPropertyValue() {
|
||||
desactiveSel.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
} else if (propSel.value === 'orientation') {
|
||||
orientationSel.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
} else if (propSel.value === 'contenu') {
|
||||
textInp.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
@@ -646,13 +688,15 @@ function submitNodeForm() {
|
||||
var prop = document.getElementById('nf-action-element-property').value;
|
||||
payload.element_property = prop;
|
||||
var isColorProp = prop === 'bg_color' || prop === 'border_color';
|
||||
var toggleOn = prop !== 'visibilite' && prop !== 'surbrillance' && prop !== 'desactive' && document.getElementById('nf-action-element-toggle').checked;
|
||||
var toggleOn = prop !== 'visibilite' && prop !== 'surbrillance' && prop !== 'desactive' && prop !== 'orientation' && document.getElementById('nf-action-element-toggle').checked;
|
||||
if (prop === 'visibilite') {
|
||||
payload.element_value = document.getElementById('nf-action-element-value-visibility').value;
|
||||
} else if (prop === 'surbrillance') {
|
||||
payload.element_value = document.getElementById('nf-action-element-value-surbrillance').value;
|
||||
} else if (prop === 'desactive') {
|
||||
payload.element_value = document.getElementById('nf-action-element-value-desactive').value;
|
||||
} else if (prop === 'orientation') {
|
||||
payload.element_value = document.getElementById('nf-action-element-value-orientation').value;
|
||||
} else if (prop === 'contenu') {
|
||||
payload.element_value = document.getElementById('nf-action-element-value-text').value;
|
||||
} else {
|
||||
@@ -695,6 +739,17 @@ function submitNodeForm() {
|
||||
// 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;
|
||||
} else if (actionType === 'jouer_animation_sprite') {
|
||||
// Phase 7 — réutilise target_element_id (déjà whitelisté) +
|
||||
// data_value en JSON {frames, fps, loop}, même patron que jouer_son.
|
||||
payload.target_element_id = parseInt(document.getElementById('nf-action-sprite-target-element').value, 10);
|
||||
var frames = document.getElementById('nf-action-sprite-frames').value
|
||||
.split('\n').map(function(s) { return s.trim(); }).filter(Boolean);
|
||||
payload.data_value = JSON.stringify({
|
||||
frames: frames,
|
||||
fps: parseInt(document.getElementById('nf-action-sprite-fps').value, 10) || 8,
|
||||
loop: document.getElementById('nf-action-sprite-loop').checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,55 @@ function initBuilderPanel() {
|
||||
});
|
||||
});
|
||||
|
||||
// Phase 7 — sprites propres au créateur : contrairement aux autres
|
||||
// champs d'upload (qui REMPLACENT la valeur d'un champ texte unique),
|
||||
// chaque fichier envoyé ici AJOUTE une ligne à la zone de frames — un
|
||||
// personnage animé a besoin de plusieurs images, pas d'une seule.
|
||||
var spriteUploadInput = document.getElementById('nf-action-sprite-upload');
|
||||
if (spriteUploadInput) {
|
||||
spriteUploadInput.addEventListener('change', function () {
|
||||
if (!spriteUploadInput.files.length) return;
|
||||
var status = document.getElementById('nf-action-sprite-upload-status');
|
||||
var framesField = document.getElementById('nf-action-sprite-frames');
|
||||
status.textContent = 'Envoi en cours...';
|
||||
var data = new FormData();
|
||||
data.append('file', spriteUploadInput.files[0]);
|
||||
fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
if (json.url) {
|
||||
framesField.value = framesField.value ? (framesField.value + '\n' + json.url) : json.url;
|
||||
status.textContent = 'Fichier envoyé ✓';
|
||||
} else { status.textContent = "Échec de l'envoi."; }
|
||||
})
|
||||
.catch(function () { status.textContent = "Échec de l'envoi."; });
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 7 — même mécanisme d'upload que nf-action-sprite-upload
|
||||
// ci-dessus (ajoute une ligne plutôt que de remplacer), pour le
|
||||
// formulaire de clip de la Timeline d'animation.
|
||||
var animClipSpriteUploadInput = document.getElementById('anim-clip-sprite-upload');
|
||||
if (animClipSpriteUploadInput) {
|
||||
animClipSpriteUploadInput.addEventListener('change', function () {
|
||||
if (!animClipSpriteUploadInput.files.length) return;
|
||||
var status = document.getElementById('anim-clip-sprite-upload-status');
|
||||
var framesField = document.getElementById('anim-clip-sprite-frames');
|
||||
status.textContent = 'Envoi en cours...';
|
||||
var data = new FormData();
|
||||
data.append('file', animClipSpriteUploadInput.files[0]);
|
||||
fetch("/game/" + GAME_SLUG + "/upload", { method: 'POST', body: data })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
if (json.url) {
|
||||
framesField.value = framesField.value ? (framesField.value + '\n' + json.url) : 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']);
|
||||
|
||||