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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
76331f9285
commit
d2c2f20b65
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user