Refonte : vrai widget "Personnage" visuel (remplace l'UI de la Phase 7)
L'utilisateur a testé la Phase 7 (sprites pilotés via un menu déroulant + zone de texte dans la logique de flow) et l'a rejetée à raison : ce n'est pas comme ça qu'un moteur de jeu (Phaser, Unity) gère un personnage. Cette refonte remplace tout le flux d'AUTEURING par un vrai widget visuel — le moteur d'exécution de la Phase 7 (runSpriteAnimation, activeSpriteAnimations, orientation, kind="sprite" de la Timeline) reste inchangé. Nouveau widget "personnage" (screens/widgets/registry.py) : - Rendu serveur dédié (special_render, screens/rendering/render_personnage.py) qui affiche la pose "idle" dès le HTML généré — jamais une image cassée à configurer après coup. - Toutes ses données (source Forge ou sprites propres au créateur, quel personnage/quelles animations) vivent dans une seule clé JSON _personnage_data (screens/rendering/personnage_data.py), sans aucune migration de schéma (même patron que c_clause_list). - Nouvelle échappatoire "custom_panel", symétrique à "special_render" mais pour le panneau de propriétés : ce widget affiche une galerie/un import de sprites sur-mesure plutôt que les contrôles génériques. Galerie visuelle de personnages Forge (VRAIES miniatures, pas un emoji) : - Panneau gauche "🎭 Personnages" : pose un personnage déjà configuré, animé immédiatement. - Panneau droit (propriétés) : change le personnage Forge de l'élément sélectionné, ou importe les animations d'un sprite personnalisé. - static/js/screen_edit/personnage-preview.js : lance l'aperçu animé de CHAQUE personnage du canevas dès le chargement de la page et après toute sauvegarde — le cœur de la demande ("je dois voir ça bouger"). Sélecteur d'animation à miniatures (nœud de flow "Jouer une animation" et clip de Timeline "sprite") : remplace le menu déroulant/la zone de texte par une grille de vraies miniatures, scopée aux SEULES animations du personnage réellement ciblé (ELEMENT_ANIMATIONS_MAP, résolu côté serveur à partir des propriétés de cet élément précis) — jamais un catalogue global. Le format stocké sur le nœud/le clip ({frames, fps, loop}) est inchangé, seule l'UI d'édition change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d2c2f20b65
commit
9e076f8cf9
+99
-46
@@ -40,6 +40,27 @@
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Phase 8 — galerie visuelle de personnages Forge (VRAIES miniatures,
|
||||
pas un emoji/icône), réutilisée deux fois : "add" pour poser un
|
||||
nouveau personnage (panneau gauche), "swap" pour changer le
|
||||
personnage Forge d'un élément déjà posé (panneau droit, propriétés). -->
|
||||
{% macro render_personnage_gallery(mode, gallery_id, selected_forge_character=None) %}
|
||||
<div class="iconGallery" id="{{ gallery_id }}">
|
||||
{% for char_slug, character in sprite_library.items() %}
|
||||
<button type="button" class="iconGalleryTile {{ 'selected' if char_slug == selected_forge_character else '' }}"
|
||||
title="{{ character.label }}"
|
||||
onclick="{{ 'addPersonnageElement' if mode == 'add' else 'swapPersonnageCharacter' }}('{{ char_slug }}')">
|
||||
<img src="/static/characters/{{ char_slug }}/idle.png" alt="{{ character.label }}" class="personnageThumb">
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% if mode == 'add' %}
|
||||
<button type="button" class="iconGalleryTile" title="Sprite personnalisé" onclick="addPersonnageElement('__custom__')">
|
||||
<span style="font-size:28px;">➕</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="builder3" id="builder3">
|
||||
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
|
||||
<!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) —
|
||||
@@ -102,7 +123,7 @@
|
||||
<summary>Ajouter un élément sur l'écran</summary>
|
||||
<div class="propGroupBody">
|
||||
<div class="widgetGrid">
|
||||
{% for wkey, w in widgets.items() if wkey != 'icone' %}
|
||||
{% for wkey, w in widgets.items() if wkey != 'icone' and wkey != 'personnage' %}
|
||||
<form method="post" action="{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}" class="widgetBtnForm">
|
||||
<input type="hidden" name="widget" value="{{ wkey }}">
|
||||
<button type="submit" class="widgetBtn" title="{{ w.label }}">
|
||||
@@ -136,6 +157,21 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Phase 8 — galerie de personnages Forge (widget "personnage" dédié,
|
||||
voir screens/widgets/registry.py) : cliquer une tuile pose un
|
||||
personnage DÉJÀ configuré (Kenney CC0), animé immédiatement dans
|
||||
le canevas (voir personnage-preview.js) — pas un élément générique
|
||||
à configurer ensuite dans la logique. "➕ Sprite personnalisé" pose
|
||||
un personnage vide dont les animations s'importent dans ses
|
||||
propriétés (panneau droit). -->
|
||||
<details class="propGroup">
|
||||
<summary>🎭 Personnages</summary>
|
||||
<div class="propGroupBody">
|
||||
{{ render_personnage_gallery('add', 'personnageGalleryAdd') }}
|
||||
<p class="help">Personnage prêt à l'emploi (Kenney, licence libre CC0) ou "➕" pour un sprite personnalisé — importe ensuite tes propres images d'animation dans ses propriétés.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Menu rétractable dédié aux éléments de jeu personnalisés (Personnage,
|
||||
Outil, IA...) : chacun a désormais SA PROPRE tuile, avec son nom,
|
||||
plutôt qu'une unique tuile générique "Élément de jeu" qui ajoutait
|
||||
@@ -505,36 +541,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 7 — personnage animé par sprites : joue une séquence de
|
||||
PNG (cycle en boucle type marche, ou une fois type saut) sur
|
||||
l'élément visé, depuis la bibliothèque Forge ou des sprites
|
||||
propres au créateur (même bouton d'upload que le son ci-dessus,
|
||||
mais ajoute une ligne plutôt que de remplacer le champ). -->
|
||||
<!-- Phase 8 — personnage animé par sprites : joue une des
|
||||
animations du PERSONNAGE CIBLÉ (miniatures réelles, jamais un
|
||||
menu déroulant ni une zone d'URLs en texte) — seuls les
|
||||
éléments widget "personnage" sont proposés ici, leurs
|
||||
animations viennent de leurs propres propriétés (galerie
|
||||
Forge ou sprites importés, voir personnage-panel.js). -->
|
||||
<div id="nf-action-sprite-fields" style="display:none;">
|
||||
<label>Élément à animer
|
||||
<select id="nf-action-sprite-target-element">
|
||||
{% for e2 in elements %}
|
||||
<label>Personnage à animer
|
||||
<select id="nf-action-sprite-target-element" onchange="onSpriteTargetElementChange()">
|
||||
<option value="">— Choisir —</option>
|
||||
{% for e2 in elements if e2.widget == 'personnage' %}
|
||||
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label style="margin-top:6px; display:block;">Personnage Forge (optionnel)
|
||||
<select id="nf-action-sprite-library-character" onchange="onSpriteLibraryCharacterChange()">
|
||||
<option value="">— Choisir —</option>
|
||||
{% for slug, character in sprite_library.items() %}
|
||||
<option value="{{ slug }}">{{ character.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<select id="nf-action-sprite-library-animation" style="margin-top:6px; display:none;" onchange="applySpriteLibraryAnimation()"></select>
|
||||
<label style="margin-top:10px; display:block;">Images (une URL par ligne)
|
||||
<textarea id="nf-action-sprite-frames" rows="4" style="width:100%;" placeholder="https://.../frame1.png"></textarea>
|
||||
</label>
|
||||
<div style="margin-top:6px;">
|
||||
<label for="nf-action-sprite-upload" style="font-size:12px;">Ajouter un fichier :</label>
|
||||
<input type="file" id="nf-action-sprite-upload" accept="image/*" style="font-size:12px;">
|
||||
<span id="nf-action-sprite-upload-status" class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
|
||||
</div>
|
||||
{% if not elements|selectattr('widget', 'equalto', 'personnage')|list %}
|
||||
<p class="hint">Aucun personnage sur cet écran — ajoute-en un depuis la galerie "🎭 Personnages" du panneau de gauche.</p>
|
||||
{% endif %}
|
||||
<div id="nf-action-sprite-animation-grid" class="iconGallery" style="margin-top:6px;"></div>
|
||||
<input type="hidden" id="nf-action-sprite-animation-name" value="">
|
||||
<div class="fieldRow" style="margin-top:6px;">
|
||||
<label style="flex:1;">Images/seconde
|
||||
<input type="number" id="nf-action-sprite-fps" min="1" step="1" value="8">
|
||||
@@ -638,27 +664,15 @@
|
||||
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
|
||||
</div>
|
||||
|
||||
<!-- Phase 7 — même champs que le nœud de flow "Jouer une animation
|
||||
de sprites" (voir plus haut #nf-action-sprite-fields), formulaire
|
||||
indépendant : bibliothèque Forge ou sprites propres au créateur. -->
|
||||
<!-- Phase 8 — même principe que le nœud de flow "Jouer une
|
||||
animation de sprites" (voir plus haut #nf-action-sprite-fields) :
|
||||
miniatures réelles des SEULES animations du personnage propriétaire
|
||||
de ce clip (clip.element_id, fixé à la création du clip — voir
|
||||
addAnimTrack()), jamais un menu déroulant ni une zone d'URLs. -->
|
||||
<div id="anim-clip-sprite-fields" style="display:none;">
|
||||
<label>Personnage Forge (optionnel)
|
||||
<select id="anim-clip-sprite-library-character" onchange="onAnimClipSpriteLibraryCharacterChange()">
|
||||
<option value="">— Choisir —</option>
|
||||
{% for slug, character in sprite_library.items() %}
|
||||
<option value="{{ slug }}">{{ character.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<select id="anim-clip-sprite-library-animation" style="margin-top:6px; display:none;" onchange="applyAnimClipSpriteLibraryAnimation()"></select>
|
||||
<label style="margin-top:10px; display:block;">Images (une URL par ligne)
|
||||
<textarea id="anim-clip-sprite-frames" rows="4" style="width:100%;" placeholder="https://.../frame1.png"></textarea>
|
||||
</label>
|
||||
<div style="margin-top:6px;">
|
||||
<label for="anim-clip-sprite-upload" style="font-size:12px;">Ajouter un fichier :</label>
|
||||
<input type="file" id="anim-clip-sprite-upload" accept="image/*" style="font-size:12px;">
|
||||
<span id="anim-clip-sprite-upload-status" class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
|
||||
</div>
|
||||
<p class="hint" id="anim-clip-sprite-no-animations" style="display:none;">Cet élément n'est pas un personnage (widget "Personnage"), ou n'a encore aucune animation.</p>
|
||||
<div id="anim-clip-sprite-animation-grid" class="iconGallery"></div>
|
||||
<input type="hidden" id="anim-clip-sprite-animation-name" value="">
|
||||
<label style="margin-top:6px; display:block;">Images/seconde
|
||||
<input type="number" id="anim-clip-sprite-fps" min="1" step="1" value="8">
|
||||
</label>
|
||||
@@ -883,6 +897,42 @@
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{% if el.widget == 'personnage' %}
|
||||
<!-- Phase 8 — panneau sur-mesure (custom_panel, voir registry.py) :
|
||||
pas de controls génériques, juste la source (Forge/personnalisée)
|
||||
et son contenu. Sauvegarde en AJAX (personnage-panel.js), jamais
|
||||
via #propsForm (générique, utilisé par tous les autres widgets). -->
|
||||
<details class="propGroup" open>
|
||||
<summary>🎭 Personnage</summary>
|
||||
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ el.id }}" data-source="{{ personnage_data.source }}">
|
||||
{% if personnage_data.source == 'custom' %}
|
||||
<p class="help">Sprite personnalisé — importe les images de chaque animation ("idle" recommandée, affichée par défaut).</p>
|
||||
<div id="personnageAnimationsList">
|
||||
{% for anim_name, frames in (personnage_data.animations or {}).items() %}
|
||||
<div class="personnageAnimRow box" data-anim-name="{{ anim_name }}" style="margin-bottom:8px; padding:8px;">
|
||||
<div class="fieldRow" style="align-items:center; gap:6px;">
|
||||
<input type="text" class="input is-small personnageAnimName" value="{{ anim_name }}" placeholder="ex. idle, marche, saut" style="flex:1;">
|
||||
<button type="button" class="button is-small is-danger" onclick="removePersonnageAnimation(this)">🗑</button>
|
||||
</div>
|
||||
<div class="personnageAnimFrames" style="display:flex; flex-wrap:wrap; gap:4px; margin-top:6px;">
|
||||
{% for url in frames %}
|
||||
<img src="{{ url }}" alt="" class="personnageFrameThumb" style="width:32px; height:32px; object-fit:contain; background:var(--panel2); border-radius:4px;">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<input type="file" class="personnageFrameUpload input is-small" accept="image/*" multiple style="margin-top:6px;">
|
||||
<span class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="button is-small is-fullwidth" onclick="addPersonnageAnimation()">+ Ajouter une animation</button>
|
||||
{% else %}
|
||||
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
|
||||
{{ render_personnage_gallery('swap', 'personnageGallerySwap', personnage_data.forge_character) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
|
||||
{% set size_ctrls = {} %}
|
||||
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
|
||||
@@ -1218,6 +1268,7 @@
|
||||
var ELEMENT_DESACTIVE_LABELS_MAP = {{ element_desactive_labels_json|safe }};
|
||||
var ELEMENT_ORIENTATION_LABELS_MAP = {{ element_orientation_labels_json|safe }};
|
||||
var SPRITE_LIBRARY = {{ sprite_library_json|safe }};
|
||||
var ELEMENT_ANIMATIONS_MAP = {{ element_animations_map_json|safe }};
|
||||
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
|
||||
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
|
||||
var TRIGGER_LABELS_MAP = {{ trigger_labels_json|safe }};
|
||||
@@ -1227,9 +1278,11 @@
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/tree-panels.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/panel-init.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/personnage-panel.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/flow-editor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/tabs-and-blocks.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/animation-timeline.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/screen_edit/personnage-preview.js') }}"></script>
|
||||
<script>
|
||||
// Onglet initial : ?tab=... (utilisé par les routes de CRUD des
|
||||
// événements/blocs, pour revenir directement sur le bon onglet après
|
||||
|
||||
Reference in New Issue
Block a user