Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Le document de cadrage produit cible des formateurs non techniques créant des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de logique en nœuds, timeline d'animation, définitions d'objets/relations, templates réutilisables) est une complexité hors cible que l'effort d'ingénierie récent avait déjà abandonnée au profit du jeu_2d. - Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only) - Suppression en bloc des modules exclusifs au document : routes/elements, routes/element_types, routes/objects, routes/legacy_actions, screens/elements, screens/element_types, screens/widgets, screens/legacy_actions, le rendu render_element_html.py et son cluster, templates/screen_edit.html, templates/game_dashboard.html, flow-editor.js/tabs-and-blocks.js/animation-timeline.js - Dashboard toujours simplifié (un seul mode possible désormais) - Tests document-only supprimés, tests de logique partagée (flow, événements personnalisés, animations) retargetés sur des écrans jeu_2d - Aucune régression jeu_2d : 299 tests passent Carte d'onboarding retravaillée : argumentaire RH non technique (liste à coche, badge "Compatible LMS"), taille et interaction de retournement ajustées.
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
// de la quête au moment où le joueur parle au PNJ).
|
||||
|
||||
var QUEST_STATUS_LABELS = { nouvelle: 'Nouvelle', en_cours: 'En cours', terminee: 'Terminée' };
|
||||
var QUEST_RESULT_LABELS = { reussi: '✅ Réussi', echoue: '❌ Échoué' };
|
||||
var QUEST_RESULT_LABELS = { reussi: 'Réussi', echoue: 'Échoué' };
|
||||
var QUEST_STATUS_ORDER = ['nouvelle', 'en_cours', 'terminee'];
|
||||
|
||||
// Quêtes connues côté client — préremplies par QUESTS_JSON (voir
|
||||
@@ -52,7 +52,7 @@ function questListRowHtml(quest) {
|
||||
'<tr class="questListRow" onclick="openQuestFieldsModal(' + quest.id + ')">' +
|
||||
'<td><strong>' + (quest.title || '').replace(/</g, '<') + '</strong></td>' +
|
||||
'<td>' + (quest.objectif ? quest.objectif.replace(/</g, '<') : '<span class="hint">—</span>') + '</td>' +
|
||||
'<td><span class="questPill questPill-score">🏆 +' + (quest.recompense_score || 0) + '</span></td>' +
|
||||
'<td><span class="questPill questPill-score">+' + (quest.recompense_score || 0) + '</span></td>' +
|
||||
'<td><span class="questPill questPill-status questPill-status-' + quest.status + '">' + QUEST_STATUS_LABELS[quest.status] + '</span></td>' +
|
||||
'<td>' + resultatHtml + '</td>' +
|
||||
'<td class="fieldsTableActions">' +
|
||||
@@ -129,7 +129,27 @@ function ensureQuestFieldsModal() {
|
||||
// `onUse(quest)` optionnel : posé par collisionWizardChooseAction('quete', ...)
|
||||
// pour finaliser la règle de collision avec cette quête, affiche alors un
|
||||
// bouton "✓ Utiliser cette quête" dans les deux modales.
|
||||
//
|
||||
// Masque #collisionRuleModal s'il est ouvert, SANS effacer _collisionWizard
|
||||
// (contrairement à closeCollisionRuleModal(), static/js/scenes/
|
||||
// collision-rules-editor.js) : appelée depuis questPickerChoose()/
|
||||
// questPickerCreate() (assistant "+ Action" de l'onglet Collision, action
|
||||
// "quete" — voir renderQuestPickerStep()), CETTE modale et
|
||||
// #collisionRuleModal partagent le même z-index (.modal de base) — sans la
|
||||
// masquer, les deux restaient actives en même temps, empilées dans l'ordre
|
||||
// du DOM plutôt que l'une à la place de l'autre : le bouton "+ Action"
|
||||
// semblait ne plus rien faire ensuite (il rouvrait bien #collisionRuleModal,
|
||||
// mais caché derrière celle-ci restée active). _collisionWizard doit
|
||||
// SURVIVRE à ce masquage : "✓ Utiliser cette quête" (questUseThisQuest)
|
||||
// finalise la règle en cours via collisionWizardFinalizeAction(), qui en a
|
||||
// encore besoin (objectId, trigger, pendingInteragir pour "interagir").
|
||||
function _hideCollisionRuleModalKeepingWizardState() {
|
||||
var modal = document.getElementById('collisionRuleModal');
|
||||
if (modal) modal.classList.remove('is-active');
|
||||
}
|
||||
|
||||
function openQuestFieldsModal(questId, onUse) {
|
||||
_hideCollisionRuleModalKeepingWizardState();
|
||||
_questModalOnUse = onUse || null;
|
||||
_questModalCurrentId = questId;
|
||||
var modal = ensureQuestFieldsModal();
|
||||
@@ -179,16 +199,16 @@ function renderQuestFieldsModalBody(questId) {
|
||||
var body = document.getElementById('questFieldsModalBody');
|
||||
if (!quest || !body) return;
|
||||
var useBtn = _questModalOnUse
|
||||
? '<button type="button" class="collisionRuleAddBtn" onclick="questUseThisQuest(' + quest.id + ')">✓ Utiliser cette quête</button>'
|
||||
? '<button type="button" class="collisionRuleAddBtn" onclick="questUseThisQuest(' + quest.id + ')">Utiliser cette quête</button>'
|
||||
: '';
|
||||
body.innerHTML =
|
||||
'<div class="fieldRow questDialogueModalHeader" style="align-items:center; justify-content:space-between;">' +
|
||||
'<h2 class="title is-5" style="margin:0;">🗺️ ' + (quest.title || '').replace(/</g, '<') + '</h2>' +
|
||||
'<h2 class="title is-5" style="margin:0;">' + (quest.title || '').replace(/</g, '<') + '</h2>' +
|
||||
'<button type="button" class="button is-small" onclick="closeQuestFieldsModal()">✕</button>' +
|
||||
'</div>' +
|
||||
questFieldRowHtml(quest) +
|
||||
'<div class="fieldRow" style="justify-content:space-between; align-items:center; margin-top:6px;">' +
|
||||
'<button type="button" class="collisionRuleAddBtn" onclick="openQuestDialogueModal(' + quest.id + ')">💬 Ajouter les dialogues</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn" onclick="openQuestDialogueModal(' + quest.id + ')">Ajouter les dialogues</button>' +
|
||||
useBtn +
|
||||
'</div>';
|
||||
}
|
||||
@@ -212,7 +232,7 @@ function questSpeakerOptions(currentSpeaker) {
|
||||
function questDialogueBubbleHtml(line, status, index) {
|
||||
var kind = questBubbleKind(line.speaker);
|
||||
var speakerSelect = questSpeakerOptions(line.speaker).map(function (name) {
|
||||
return '<option value="' + name.replace(/"/g, '"') + '"' + (name === line.speaker ? ' selected' : '') + '>' + (name === 'Joueur' ? '🧑 Joueur' : '🗨️ ' + name) + '</option>';
|
||||
return '<option value="' + name.replace(/"/g, '"') + '"' + (name === line.speaker ? ' selected' : '') + '>' + (name === 'Joueur' ? 'Joueur' : name) + '</option>';
|
||||
}).join('');
|
||||
return (
|
||||
'<div class="questBubbleRow">' +
|
||||
@@ -262,9 +282,9 @@ function questQuestionBubbleHtml(line, status, index) {
|
||||
'<div class="questBubbleRow">' +
|
||||
'<div class="questBubble questBubble-question">' +
|
||||
'<div class="questBubbleHeader">' +
|
||||
'<label class="questQuestionHeaderField" title="Nombre de choix">🔢 <input type="number" min="2" max="4" value="' + line.choices.length + '" ' +
|
||||
'<label class="questQuestionHeaderField" title="Nombre de choix"><input type="number" min="2" max="4" value="' + line.choices.length + '" ' +
|
||||
'onchange="questSetQuestionChoiceCount(\'' + status + '\', ' + index + ', this.value)"></label>' +
|
||||
'<label class="questQuestionHeaderField" title="Récompense (points de score)">🏆 <input type="number" min="0" value="' + line.reward_amount + '" ' +
|
||||
'<label class="questQuestionHeaderField" title="Récompense (points de score)"><input type="number" min="0" value="' + line.reward_amount + '" ' +
|
||||
'onchange="questSetQuestionRewardAmount(\'' + status + '\', ' + index + ', this.value)"> pts</label>' +
|
||||
'<button type="button" class="questBubbleDelete" title="Supprimer" onclick="questDeleteBubble(\'' + status + '\', ' + index + ')">✕</button>' +
|
||||
'</div>' +
|
||||
@@ -288,8 +308,8 @@ function questDialogueColumnHtml(quest, status) {
|
||||
'<h4 class="questDialogueColumnTitle">' + QUEST_STATUS_LABELS[status] + '</h4>' +
|
||||
'<div class="questDialogueColumnBubbles" id="questDialogueColumn-' + status + '">' + bubblesHtml + '</div>' +
|
||||
'<div class="questDialogueColumnButtons">' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn" onclick="questAddBubble(\'' + status + '\')">✚ Réplique</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn questQuestionAddBtn" onclick="questAddQuestion(\'' + status + '\')">❓ Question</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn" onclick="questAddBubble(\'' + status + '\')">Réplique</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn questDialogueAddBtn questQuestionAddBtn" onclick="questAddQuestion(\'' + status + '\')">Question</button>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
@@ -338,13 +358,13 @@ function renderQuestDialogueModalBody(questId) {
|
||||
if (!quest || !body) return;
|
||||
_questModalCurrentId = questId;
|
||||
var useBtn = _questModalOnUse
|
||||
? '<button type="button" class="collisionRuleAddBtn" onclick="questUseThisQuest(' + quest.id + ')">✓ Utiliser cette quête</button>'
|
||||
? '<button type="button" class="collisionRuleAddBtn" onclick="questUseThisQuest(' + quest.id + ')">Utiliser cette quête</button>'
|
||||
: '';
|
||||
body.innerHTML =
|
||||
'<div class="fieldRow questDialogueModalHeader" style="align-items:center; justify-content:space-between;">' +
|
||||
'<div style="display:flex; align-items:center; gap:10px;">' +
|
||||
'<button type="button" class="button is-small" onclick="questBackToFieldsModal(' + quest.id + ')" title="Retour à la fiche">←</button>' +
|
||||
'<h2 class="title is-5" style="margin:0;">🗺️ ' + (quest.title || '').replace(/</g, '<') + ' — dialogues</h2>' +
|
||||
'<h2 class="title is-5" style="margin:0;">' + (quest.title || '').replace(/</g, '<') + ' — dialogues</h2>' +
|
||||
'</div>' +
|
||||
'<div style="display:flex; gap:8px; align-items:center;">' + useBtn +
|
||||
'<button type="button" class="button is-small" onclick="closeQuestDialogueModal()">✕</button>' +
|
||||
@@ -520,7 +540,7 @@ function questPickerHtml(quests) {
|
||||
}
|
||||
return '<div class="collisionWizardChoices">' + quests.map(function (q, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="questPickerChoose(' + q.id + ')">' +
|
||||
'<span class="emoji">🗺️</span><span>' + (q.title || '').replace(/</g, '<') + '</span>' +
|
||||
'<span>' + (q.title || '').replace(/</g, '<') + '</span>' +
|
||||
'</button>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
@@ -531,7 +551,7 @@ function renderQuestPickerStep(onUse) {
|
||||
_questPickerOnUse = onUse;
|
||||
var body = collisionWizardBodyEl();
|
||||
body.innerHTML = '<p class="hint mb-2">Quelle quête déclencher ?</p><div id="questPickerList">Chargement…</div>' +
|
||||
'<button type="button" class="collisionRuleAddBtn mt-2" onclick="questPickerCreate()">✚ Nouvelle quête</button>';
|
||||
'<button type="button" class="collisionRuleAddBtn mt-2" onclick="questPickerCreate()">Nouvelle quête</button>';
|
||||
fetch('/game/' + GAME_SLUG + '/quests/list.json')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
@@ -556,6 +576,13 @@ function questPickerCreate() {
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof QUESTS_JSON !== 'undefined') renderQuestListPage();
|
||||
});
|
||||
// Appelé directement (pas de DOMContentLoaded) : ce fichier est chargé par
|
||||
// un <script src> posé à la fin du contenu de la page — les éléments dont
|
||||
// on a besoin existent déjà à ce stade. Nécessaire aussi pour la
|
||||
// navigation pjax (voir static/pjax.js — runScriptsIn ré-exécute ce
|
||||
// script à chaque page swappée, mais DOMContentLoaded, lui, ne se
|
||||
// redéclenche jamais après le tout premier chargement réel) — sans ça,
|
||||
// l'onglet "🗺️ Quêtes" restait vide après un remplacement de <main> (ex.
|
||||
// suppression d'un objet de scène, voir scene_object_delete.py) qui ne
|
||||
// redéclenche jamais cet événement.
|
||||
if (typeof QUESTS_JSON !== 'undefined') renderQuestListPage();
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
// elle est validée une seconde fois avant d'être enregistrée).
|
||||
|
||||
var COLLISION_ACTION_LABELS = {
|
||||
quete: '🗺️ Déclencher une quête', attaque: '⚔️ Attaquer la cible',
|
||||
evenement: '📣 Déclencher un événement', interagir: '🔑 Interagir avec une touche',
|
||||
quete: 'Déclencher une quête', attaque: 'Attaquer la cible',
|
||||
evenement: 'Déclencher un événement', interagir: 'Interagir avec une touche',
|
||||
};
|
||||
// "attaque"/"evenement" retirés de l'assistant (pas besoin pour le
|
||||
// moment, demande explicite) — server-side (screens/rendering/
|
||||
// collision_rules.py::ACTION_TYPES) les accepte encore telles quelles,
|
||||
// pour ne jamais casser une règle déjà enregistrée avec l'un des deux.
|
||||
var COLLISION_WIZARD_ACTION_TYPES = ['quete', 'interagir'];
|
||||
var COLLISION_TRIGGER_LABELS = { collision: '💥 À la collision', perimetre: '📍 Dans un périmètre' };
|
||||
var COLLISION_TRIGGER_LABELS = { collision: 'À la collision', perimetre: 'Dans un périmètre' };
|
||||
|
||||
function collisionRuleObjectLabel(obj) {
|
||||
var kindLabel = obj.kind === 'personnage' ? (obj.role === 'ennemie' ? 'Ennemi' : 'PNJ') : 'Décor';
|
||||
@@ -52,15 +52,26 @@ function collisionRuleActionSummary(action) {
|
||||
function collisionRuleChainHtml(rule, ruleIndex, objectId) {
|
||||
var links = [
|
||||
{ kind: 'trigger', text: collisionRuleTriggerSummary(rule) },
|
||||
{ kind: rule.action.type, text: collisionRuleActionSummary(rule.action) },
|
||||
{ kind: rule.action.type, text: collisionRuleActionSummary(rule.action), questId: rule.action.quete_id },
|
||||
];
|
||||
var action = rule.action;
|
||||
while (action.type === 'interagir' && action.sub_action) {
|
||||
links.push({ kind: action.sub_action.type, text: collisionRuleActionSummary(action.sub_action) });
|
||||
links.push({ kind: action.sub_action.type, text: collisionRuleActionSummary(action.sub_action), questId: action.sub_action.quete_id });
|
||||
action = action.sub_action;
|
||||
}
|
||||
// Un maillon "quete" (kind==='quete') ouvre directement sa modale de
|
||||
// dialogue (voir openQuestDialogueModal(), static/js/quests/
|
||||
// quest-editor.js) au clic — demande explicite, plutôt qu'un simple
|
||||
// texte informatif comme les autres maillons. Reste un <span> comme les
|
||||
// autres (jamais un <button>, dont le style natif — apparence système,
|
||||
// marges par défaut — grignotait la pointe en clip-path malgré les
|
||||
// resets CSS) : un onclick sur un <span> suffit à le rendre cliquable,
|
||||
// ".collisionRuleLink[data-kind='quete']" ajoute juste le curseur/hover
|
||||
// qui le distingue visuellement (voir static/style.css).
|
||||
var linksHtml = links.map(function (link, i) {
|
||||
return '<span class="collisionRuleLink" data-kind="' + link.kind + '" style="animation-delay:' + (i * 60) + 'ms">' +
|
||||
var clickable = link.kind === 'quete' && link.questId != null;
|
||||
var onclick = clickable ? ' onclick="openQuestDialogueModal(' + link.questId + ')"' : '';
|
||||
return '<span class="collisionRuleLink" data-kind="' + link.kind + '" style="animation-delay:' + (i * 60) + 'ms"' + onclick + '>' +
|
||||
link.text.replace(/</g, '<') + '</span>';
|
||||
}).join('');
|
||||
return (
|
||||
@@ -72,6 +83,21 @@ function collisionRuleChainHtml(rule, ruleIndex, objectId) {
|
||||
);
|
||||
}
|
||||
|
||||
// obj.thumbnail_html (screens.render_scene_object) porte un style inline
|
||||
// PENSÉ POUR LE CANEVAS (position:absolute + left/top en dur, voir
|
||||
// screens/scenes/render_scene_object.py) — un personnage déplacé loin
|
||||
// dans la scène (left/top de plusieurs centaines de px) ressortait donc
|
||||
// hors du petit cadre 120×120 de cette miniature : la règle CSS
|
||||
// `.collisionRuleCardThumb img` (voir static/style.css) est censée le
|
||||
// neutraliser (position:static !important etc.), mais s'appuyer sur un
|
||||
// forçage CSS pour annuler un style inline entier reste fragile. Plus
|
||||
// robuste : reconstruire ici une balise <img> "propre", sans AUCUN style
|
||||
// hérité du canevas — seul le src (l'image elle-même) est repris.
|
||||
function collisionRuleThumbHtml(obj) {
|
||||
var match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
|
||||
return match ? '<img src="' + match[1] + '" alt="">' : '';
|
||||
}
|
||||
|
||||
function collisionRuleCardHtml(obj) {
|
||||
var rulesHtml = obj.rules.length
|
||||
? obj.rules.map(function (rule, i) { return collisionRuleChainHtml(rule, i, obj.id); }).join('')
|
||||
@@ -79,11 +105,11 @@ function collisionRuleCardHtml(obj) {
|
||||
return (
|
||||
'<div class="collisionRuleCard" id="collisionRuleCard-' + obj.id + '">' +
|
||||
'<div class="collisionRuleCardHeader">' +
|
||||
'<div class="collisionRuleCardThumb">' + obj.thumbnail_html + '</div>' +
|
||||
'<div class="collisionRuleCardThumb">' + collisionRuleThumbHtml(obj) + '</div>' +
|
||||
'<h3 class="collisionRuleCardTitle">' + collisionRuleObjectLabel(obj).replace(/</g, '<') + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="collisionRuleCardRules">' + rulesHtml + '</div>' +
|
||||
'<button type="button" class="collisionRuleAddBtn" onclick="openCollisionRuleModal(' + obj.id + ')">✚ Action</button>' +
|
||||
'<button type="button" class="collisionRuleAddBtn" onclick="openCollisionRuleModal(' + obj.id + ')">Action</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
@@ -154,24 +180,15 @@ function collisionWizardBodyEl() {
|
||||
|
||||
// Étape 1 — déclencheur : collision directe, ou périmètre (px réglable,
|
||||
// valeur par défaut COLLISION_DEFAULT_PERIMETRE_PX).
|
||||
// Sépare "emoji" et "libellé" d'un texte "🗺️ Déclencher une quête" pour
|
||||
// les afficher sur deux lignes dans une carte-choix (voir .collisionWizardChoice).
|
||||
function collisionSplitEmojiLabel(text) {
|
||||
var m = text.match(/^(\S+)\s+(.*)$/);
|
||||
return m ? { emoji: m[1], label: m[2] } : { emoji: '', label: text };
|
||||
}
|
||||
|
||||
function renderCollisionWizardTriggerStep() {
|
||||
var collision = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.collision);
|
||||
var perimetre = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.perimetre);
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
|
||||
'<div class="collisionWizardChoices">' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseTrigger(\'collision\')">' +
|
||||
'<span class="emoji">' + collision.emoji + '</span><span>' + collision.label + '</span>' +
|
||||
'<span>' + COLLISION_TRIGGER_LABELS.collision + '</span>' +
|
||||
'</button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:40ms" onclick="collisionWizardChooseTrigger(\'perimetre\')">' +
|
||||
'<span class="emoji">' + perimetre.emoji + '</span><span>' + perimetre.label + '</span>' +
|
||||
'<span>' + COLLISION_TRIGGER_LABELS.perimetre + '</span>' +
|
||||
'<span class="fieldRow" style="align-items:center; gap:6px;" onclick="event.stopPropagation()">' +
|
||||
'<input type="number" min="1" id="collisionWizardPerimetrePx" value="' + COLLISION_DEFAULT_PERIMETRE_PX + '" style="width:64px;" class="input is-small">' +
|
||||
'<span class="hint">px</span>' +
|
||||
@@ -196,9 +213,8 @@ function collisionWizardChooseTrigger(trigger) {
|
||||
function renderCollisionWizardActionStep(nested) {
|
||||
var types = nested ? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_WIZARD_ACTION_TYPES;
|
||||
var buttons = types.map(function (type, i) {
|
||||
var split = collisionSplitEmojiLabel(COLLISION_ACTION_LABELS[type]);
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
|
||||
'<span class="emoji">' + split.emoji + '</span><span>' + split.label + '</span>' +
|
||||
'<span>' + COLLISION_ACTION_LABELS[type] + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
@@ -253,18 +269,25 @@ function collisionWizardChooseAction(type, nested) {
|
||||
if (type === 'evenement') {
|
||||
var eventIds = Object.keys(CUSTOM_EVENTS_MAP);
|
||||
if (!eventIds.length) {
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint">Aucun événement personnalisé pour l\'instant — crée-en un dans l\'onglet "📣 Événements", puis reviens ici.</p>';
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint">Aucun événement personnalisé pour l\'instant — crée-en un dans l\'onglet "Événements", puis reviens ici.</p>';
|
||||
return;
|
||||
}
|
||||
var buttons = eventIds.map(function (id, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardFinalizeAction({type:\'evenement\', evenement_id:' + id + '}, ' + (nested ? 'true' : 'false') + ')">' +
|
||||
'<span class="emoji">📣</span><span>' + CUSTOM_EVENTS_MAP[id].name.replace(/</g, '<') + '</span>' +
|
||||
'<span>' + CUSTOM_EVENTS_MAP[id].name.replace(/</g, '<') + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel événement ?</p><div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof COLLISION_RULE_OBJECTS !== 'undefined') renderCollisionRuleCards();
|
||||
});
|
||||
// Appelé directement (pas de DOMContentLoaded) : ce fichier est chargé par
|
||||
// un <script src> posé à la fin du contenu de la page — les éléments dont
|
||||
// on a besoin existent déjà à ce stade. Nécessaire aussi pour la
|
||||
// navigation pjax (voir static/pjax.js — runScriptsIn ré-exécute ce
|
||||
// script à chaque page swappée, mais DOMContentLoaded, lui, ne se
|
||||
// redéclenche jamais après le tout premier chargement réel) — sans ça,
|
||||
// l'onglet "🧩 Collision" restait vide après un remplacement de <main>
|
||||
// (ex. suppression d'un objet de scène, voir scene_object_delete.py) qui
|
||||
// ne redéclenche jamais cet événement.
|
||||
if (typeof COLLISION_RULE_OBJECTS !== 'undefined') renderCollisionRuleCards();
|
||||
|
||||
@@ -8,16 +8,97 @@
|
||||
// animation-timeline.js, personnage-panel.js, personnage-preview.js) est
|
||||
// réutilisée TELLE QUELLE, chargée telle quelle par scene_edit.html.
|
||||
|
||||
// AJAX (voir routes/scenes/scene_object_add.py — répond en JSON dès que
|
||||
// "X-Requested-With: fetch" est posé) : pose l'objet SANS recharger la
|
||||
// page, pour ne plus faire "disparaître" un instant l'éditeur de collision/
|
||||
// quêtes (état JS entier réinitialisé par un rechargement complet) ni
|
||||
// perdre l'onglet/le scroll en cours. Le nouvel objet n'est PAS
|
||||
// automatiquement sélectionné/ouvert dans le panneau de propriétés (voir
|
||||
// onSceneObjectMouseDown, qui recharge encore la page pour ça) — juste
|
||||
// posé sur le canevas, cliquable ensuite comme n'importe quel autre.
|
||||
function addSceneObject(kind, forgeCharacter, backgroundSlug) {
|
||||
var form = document.createElement('form');
|
||||
form.method = 'post';
|
||||
form.action = "/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add";
|
||||
var html = '<input type="hidden" name="kind" value="' + kind + '">';
|
||||
if (forgeCharacter) html += '<input type="hidden" name="forge_character" value="' + forgeCharacter + '">';
|
||||
if (backgroundSlug) html += '<input type="hidden" name="background_slug" value="' + backgroundSlug + '">';
|
||||
form.innerHTML = html;
|
||||
document.body.appendChild(form);
|
||||
form.requestSubmit();
|
||||
var body = new FormData();
|
||||
body.append('kind', kind);
|
||||
if (forgeCharacter) body.append('forge_character', forgeCharacter);
|
||||
if (backgroundSlug) body.append('background_slug', backgroundSlug);
|
||||
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add", {
|
||||
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
|
||||
})
|
||||
.then(function (r) {
|
||||
// Secours si le serveur répond encore par une redirection HTML
|
||||
// classique (ex. déploiement pas encore à jour côté serveur alors
|
||||
// que ce script, lui, l'est déjà) : l'objet est de toute façon déjà
|
||||
// posé en base à ce stade (voir screens/scenes/add_scene_object.py),
|
||||
// seul un rechargement peut alors l'afficher.
|
||||
var contentType = r.headers.get('content-type') || '';
|
||||
if (!r.ok || contentType.indexOf('application/json') === -1) {
|
||||
window.location.reload();
|
||||
return null;
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
if (!json) return;
|
||||
var canvas = document.getElementById('canvas');
|
||||
var el = document.createElement('div');
|
||||
el.className = 'canvasElement';
|
||||
el.id = 'el-' + json.id;
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = json.x + 'px'; el.style.top = json.y + 'px';
|
||||
el.style.width = json.width + 'px'; el.style.height = json.height + 'px';
|
||||
el.setAttribute('onmousedown', 'onSceneObjectMouseDown(event, ' + json.id + ')');
|
||||
el.innerHTML =
|
||||
'<div class="canvasElementInner">' + json.rendered_html + '</div>' +
|
||||
'<div class="resizeHandle" onmousedown="onSceneObjectResizeMouseDown(event, ' + json.id + ')"></div>';
|
||||
canvas.appendChild(el);
|
||||
refreshCollisionRuleObjects();
|
||||
if (json.animations) {
|
||||
ELEMENT_ANIMATIONS_MAP[json.id] = json.animations;
|
||||
restartPersonnageIdlePreview(json.id, json.animations);
|
||||
}
|
||||
})
|
||||
// Filet de sécurité : mieux vaut un rechargement (l'objet est déjà en
|
||||
// base à ce stade) qu'un bouton "+ Ajouter" qui semble ne plus rien
|
||||
// faire du tout si jamais une erreur imprévue survient ici.
|
||||
.catch(function () { window.location.reload(); });
|
||||
}
|
||||
|
||||
// AJAX (voir routes/scenes/scene_object_delete.py) : bouton 🗑️ du panneau
|
||||
// de propriétés (toujours l'objet SÉLECTIONNÉ, seul cas où ce bouton est
|
||||
// affiché) — supprime l'objet SANS recharger la page ni même redemander
|
||||
// #builder3 au serveur (déjà retiré du DOM ici) : ne plus faire "vider"
|
||||
// l'éditeur de collision/quêtes le temps d'un remplacement de <main>,
|
||||
// comme le faisait la soumission de formulaire classique interceptée par
|
||||
// pjax.js (voir son commentaire d'en-tête).
|
||||
function deleteSceneObject(id) {
|
||||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + id + '/delete', {
|
||||
method: 'POST', headers: { 'X-Requested-With': 'fetch' },
|
||||
})
|
||||
.then(function (r) {
|
||||
var contentType = r.headers.get('content-type') || '';
|
||||
if (!r.ok || contentType.indexOf('application/json') === -1) {
|
||||
window.location.reload();
|
||||
return null;
|
||||
}
|
||||
return r.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
if (!json || !json.ok) return;
|
||||
var el = document.getElementById('el-' + id);
|
||||
if (el) el.remove();
|
||||
if (typeof _stopPersonnageIdlePreview === 'function') _stopPersonnageIdlePreview(id);
|
||||
refreshCollisionRuleObjects();
|
||||
// L'objet supprimé était forcément le sélectionné (seul cas où 🗑️
|
||||
// est affiché) — referme le panneau de propriétés et l'URL, sans
|
||||
// requête supplémentaire au serveur.
|
||||
var propsBody = document.querySelector('#propsPanelFloat .floatPanelBody');
|
||||
if (propsBody) {
|
||||
propsBody.innerHTML = '<div class="hint" style="padding:20px 0;">Sélectionne un objet (sur la scène ou dans la liste) pour voir et modifier ses propriétés ici.</div>';
|
||||
}
|
||||
CURRENT_SELECTED_ID = null;
|
||||
history.replaceState(null, '', SCREEN_EDIT_URL);
|
||||
})
|
||||
.catch(function () { window.location.reload(); });
|
||||
}
|
||||
|
||||
// Taille de la caméra (voir "🎮 Jeu 2D" en haut de templates/
|
||||
@@ -152,7 +233,17 @@ function onSceneObjectMouseDown(e, id) {
|
||||
updateScenePosFields(curLeft, curTop, width, height);
|
||||
saveSceneObjectGeometry(id, curLeft, curTop, width, height);
|
||||
} else {
|
||||
window.location.href = SCREEN_EDIT_URL + '?selected=' + id;
|
||||
// goToSelected() (static/js/screen_edit/tree-panels.js, déjà utilisée
|
||||
// par l'éditeur document) : va chercher #builder3 en arrière-plan et
|
||||
// le remplace sur place plutôt que de recharger toute la page —
|
||||
// demande explicite ("à chaque fois que je clique sur un élément la
|
||||
// page se recharge"). openPropsPanel() : si le panneau était fermé
|
||||
// (état mémorisé dans localStorage, voir restoreFloatPanelState()),
|
||||
// cliquer un objet doit le rouvrir pour montrer ses propriétés —
|
||||
// sinon initBuilderPanel() (rappelé par goToSelected) le laissait
|
||||
// fermé, panneau à jour mais invisible.
|
||||
openPropsPanel();
|
||||
goToSelected(id);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
@@ -208,7 +299,7 @@ function onSceneObjectResizeMouseDown(e, id) {
|
||||
// Champs numériques Position & taille du panneau de propriétés — mirror
|
||||
// du comportement équivalent pour l'éditeur document (voir l'IIFE dans
|
||||
// initBuilderPanel(), panel-init.js), mais ids/route distincts (pixels).
|
||||
(function () {
|
||||
function initScenePosFields() {
|
||||
var xEl = document.getElementById('scene-pos-x'), yEl = document.getElementById('scene-pos-y');
|
||||
var wEl = document.getElementById('scene-pos-w'), hEl = document.getElementById('scene-pos-h');
|
||||
if (!xEl || !wEl || !CURRENT_SELECTED_ID) return;
|
||||
@@ -223,7 +314,8 @@ function onSceneObjectResizeMouseDown(e, id) {
|
||||
saveSceneObjectGeometry(CURRENT_SELECTED_ID, x, y, w, h);
|
||||
}
|
||||
[xEl, yEl, wEl, hEl].forEach(function (el) { el.addEventListener('change', apply); });
|
||||
})();
|
||||
}
|
||||
initScenePosFields();
|
||||
|
||||
// Personnage d'un objet de scène : mirror de swapPersonnageCharacter()
|
||||
// (personnage-panel.js), mais POST vers /scene-objects/.../personnage
|
||||
@@ -263,7 +355,25 @@ function saveSceneObjectRole(role) {
|
||||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/role', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: role }),
|
||||
});
|
||||
}).then(refreshCollisionRuleObjects);
|
||||
}
|
||||
|
||||
// Réutilisé après ajout/suppression/changement de rôle ou de nom (voir
|
||||
// routes/scenes/scene_collision_rule_objects.py) : un changement de rôle
|
||||
// peut faire apparaître/disparaître une carte (passage à/depuis "joueur",
|
||||
// jamais dans "🧩 Collision") et un changement de nom change son libellé
|
||||
// affiché — recalculée entièrement côté serveur plutôt que rejouée en JS
|
||||
// (même logique déjà nécessaire pour le rôle implicite d'une scène posée
|
||||
// avant ce réglage, voir build_collision_rule_objects) SANS recharger la
|
||||
// page.
|
||||
function refreshCollisionRuleObjects() {
|
||||
if (typeof COLLISION_RULE_OBJECTS === 'undefined') return;
|
||||
fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/collision-rule-objects')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
COLLISION_RULE_OBJECTS = json;
|
||||
renderCollisionRuleCards();
|
||||
});
|
||||
}
|
||||
|
||||
// Boîte de collision (voir screens/rendering/collision_settings.py,
|
||||
@@ -475,9 +585,27 @@ function saveSceneObjectName(objectId, name) {
|
||||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/name', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: name }),
|
||||
}).then(function () {
|
||||
refreshCollisionRuleObjects();
|
||||
refreshSceneObjectNames();
|
||||
});
|
||||
}
|
||||
|
||||
// SCENE_OBJECT_NAMES (voir routes/scenes/scene_object_names_list.py) :
|
||||
// posée UNE SEULE FOIS au chargement complet de la page (<script> inline
|
||||
// en bas, HORS de #builder3, jamais rafraîchie par une simple sélection
|
||||
// sans rechargement, voir goToSelected()) — sans ce rafraîchissement,
|
||||
// renommer un personnage puis ouvrir la modale de dialogue dans la MÊME
|
||||
// session ne faisait jamais apparaître son nouveau nom dans "qui parle"
|
||||
// (voir questSpeakerOptions(), static/js/quests/quest-editor.js) tant
|
||||
// qu'aucun vrai rechargement de page n'avait eu lieu entre temps.
|
||||
function refreshSceneObjectNames() {
|
||||
if (typeof SCENE_OBJECT_NAMES === 'undefined') return;
|
||||
fetch('/game/' + GAME_SLUG + '/scene-object-names')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) { SCENE_OBJECT_NAMES = json; });
|
||||
}
|
||||
|
||||
// "🎨 Style" du widget "💬 Boîte de dialogue" (voir screens/rendering/
|
||||
// dialogue_box_style.py) — envoie TOUJOURS les 7 champs ensemble (même
|
||||
// convention "replace-all" que saveSceneObjectCollision), jamais un seul
|
||||
@@ -509,7 +637,7 @@ function saveDialogueBoxStyle(objectId) {
|
||||
});
|
||||
}
|
||||
|
||||
(function () {
|
||||
function initPersonnageCommandsPanel() {
|
||||
var body = document.getElementById('personnageCommandsBody');
|
||||
if (!body) return;
|
||||
var objectId = parseInt(body.dataset.elementId, 10);
|
||||
@@ -521,7 +649,47 @@ function saveDialogueBoxStyle(objectId) {
|
||||
if (el) el.addEventListener('change', saveSceneObjectCommands);
|
||||
});
|
||||
var container = document.getElementById('personnageCommandsExtra');
|
||||
(window.PERSONNAGE_COMMANDS_EXTRA || []).forEach(function (e) {
|
||||
// Lu depuis data-commands-extra (posé sur CET élément par le serveur,
|
||||
// voir templates/scene_edit.html) plutôt que la variable globale
|
||||
// PERSONNAGE_COMMANDS_EXTRA (déclarée dans le <script> inline en bas de
|
||||
// page, HORS de #builder3) : cette fonction est aussi rappelée après un
|
||||
// changement de sélection SANS rechargement (voir initSceneBuilderPanel
|
||||
// ci-dessous), qui ne remplace que #builder3 — cette variable globale
|
||||
// resterait alors bloquée sur les commandes du PRÉCÉDENT objet
|
||||
// sélectionné.
|
||||
var extra = [];
|
||||
try { extra = JSON.parse(body.dataset.commandsExtra || '[]'); } catch (e) { extra = []; }
|
||||
extra.forEach(function (e) {
|
||||
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
|
||||
});
|
||||
})();
|
||||
}
|
||||
initPersonnageCommandsPanel();
|
||||
|
||||
// Rappelée après chaque changement de sélection d'objet de scène SANS
|
||||
// rechargement de page (voir onSceneObjectMouseDown -> goToSelected(),
|
||||
// static/js/screen_edit/tree-panels.js::refreshBuilderPanel, qui l'appelle
|
||||
// juste après avoir remplacé #builder3 et initBuilderPanel()) — reproduit
|
||||
// ici ce que ce script fait déjà tout seul au chargement initial de la
|
||||
// page (voir initScenePosFields()/initPersonnageCommandsPanel() ci-dessus)
|
||||
// + relance l'aperçu animé de tous les personnages du canevas (leurs <img>
|
||||
// viennent d'être recréés par le même remplacement, voir
|
||||
// startAllPersonnagePreviews()/personnage-preview.js).
|
||||
//
|
||||
// .builderCanvasArea (les onglets "🖥️ Scène"/"🧩 Collision"/"🗺️ Quêtes"/
|
||||
// "📣 Événements") est ENTIÈREMENT à l'intérieur de #builder3 (voir
|
||||
// templates/scene_edit.html) — le remplacer réinjecte donc aussi
|
||||
// #collisionRulesCards/le conteneur des quêtes tels que le SERVEUR les
|
||||
// rend, c'est-à-dire VIDES (ces listes sont rendues entièrement en JS,
|
||||
// jamais en Jinja, voir collision-rules-editor.js/quest-editor.js) : sans
|
||||
// les redessiner ici, les onglets "🧩 Collision"/"🗺️ Quêtes" semblaient se
|
||||
// "vider" à chaque sélection d'un objet de scène. COLLISION_RULE_OBJECTS/
|
||||
// QUESTS_JSON eux-mêmes ne sont PAS affectés par ce remplacement (déclarés
|
||||
// dans le <script> inline en bas de page, HORS de #builder3) — il suffit
|
||||
// donc de rejouer leur rendu, pas de les recharger.
|
||||
function initSceneBuilderPanel() {
|
||||
initScenePosFields();
|
||||
initPersonnageCommandsPanel();
|
||||
if (typeof startAllPersonnagePreviews === 'function') startAllPersonnagePreviews();
|
||||
if (typeof renderCollisionRuleCards === 'function') renderCollisionRuleCards();
|
||||
if (typeof renderQuestListPage === 'function') renderQuestListPage();
|
||||
}
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
// ---------- Timeline d'animation : piste par élément, clips Animate.css ou
|
||||
// personnalisés (images-clés), glissés/redimensionnés à la souris sur un
|
||||
// axe du temps en secondes depuis l'affichage de l'écran. Même principe de
|
||||
// panneau rétractable/redimensionnable que "Logique de la scène"
|
||||
// ci-dessus, positionné juste en dessous. ----------
|
||||
|
||||
// ANIM_CLIPS/ANIMATE_CSS_LABELS/GAME_SLUG : voir le petit <script> inline
|
||||
// en tête de screen_edit.html (données Jinja).
|
||||
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; }
|
||||
|
||||
function animClipLabel(clip) {
|
||||
if (clip.kind === 'custom') return '🎨 Personnalisée';
|
||||
if (clip.kind === 'sprite') return '🏃 Sprite';
|
||||
return '🎞️ ' + (ANIMATE_CSS_LABELS[clip.animate_name] || clip.animate_name);
|
||||
}
|
||||
|
||||
function renderAnimTimeline() {
|
||||
var lanesWrap = document.getElementById('animTracks');
|
||||
var ticksWrap = document.getElementById('animRuler');
|
||||
if (!lanesWrap || !ticksWrap) return;
|
||||
lanesWrap.innerHTML = '';
|
||||
ticksWrap.innerHTML = '';
|
||||
|
||||
var maxTime = ANIM_CLIPS.reduce(function(m, c) { return Math.max(m, (c.start_time || 0) + (c.duration || 1)); }, 5);
|
||||
var totalSeconds = Math.ceil(maxTime) + 2;
|
||||
var totalWidth = totalSeconds * ANIM_PX_PER_SEC;
|
||||
|
||||
var spacer = document.createElement('div');
|
||||
spacer.className = 'animRulerSpacer';
|
||||
ticksWrap.appendChild(spacer);
|
||||
var ticksLane = document.createElement('div');
|
||||
ticksLane.className = 'animRulerTicksWrap';
|
||||
ticksLane.style.width = totalWidth + 'px';
|
||||
for (var s = 0; s <= totalSeconds; s++) {
|
||||
var tick = document.createElement('div');
|
||||
tick.className = 'animRulerTick';
|
||||
tick.style.left = (s * ANIM_PX_PER_SEC) + 'px';
|
||||
tick.textContent = s + 's';
|
||||
ticksLane.appendChild(tick);
|
||||
}
|
||||
ticksWrap.appendChild(ticksLane);
|
||||
|
||||
var elementIds = [];
|
||||
ANIM_CLIPS.forEach(function(c) { if (elementIds.indexOf(c.element_id) === -1) elementIds.push(c.element_id); });
|
||||
|
||||
if (!elementIds.length) {
|
||||
var empty = document.createElement('p');
|
||||
empty.className = 'hint';
|
||||
empty.style.padding = '10px 0';
|
||||
empty.textContent = "Aucun clip pour l'instant — choisis un élément ci-dessus et clique « Ajouter un clip ».";
|
||||
lanesWrap.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
elementIds.forEach(function(elId) {
|
||||
var track = document.createElement('div');
|
||||
track.className = 'animTrack';
|
||||
|
||||
var label = document.createElement('div');
|
||||
label.className = 'animTrackLabel';
|
||||
label.textContent = ELEMENTS_LABELS[elId] || ('elt-' + elId);
|
||||
track.appendChild(label);
|
||||
|
||||
var lane = document.createElement('div');
|
||||
lane.className = 'animTrackLane';
|
||||
lane.style.width = totalWidth + 'px';
|
||||
|
||||
ANIM_CLIPS.filter(function(c) { return c.element_id === elId; }).forEach(function(clip) {
|
||||
var block = document.createElement('div');
|
||||
block.className = 'animClip';
|
||||
block.style.left = ((clip.start_time || 0) * ANIM_PX_PER_SEC) + 'px';
|
||||
block.style.width = Math.max(24, (clip.duration || 1) * ANIM_PX_PER_SEC) + 'px';
|
||||
block.textContent = animClipLabel(clip);
|
||||
block.dataset.clipId = clip.id;
|
||||
block.addEventListener('mousedown', function(e) { onAnimClipMouseDown(e, clip.id); });
|
||||
var handle = document.createElement('div');
|
||||
handle.className = 'animClipResizeHandle';
|
||||
handle.addEventListener('mousedown', function(e) { onAnimClipResizeMouseDown(e, clip.id); });
|
||||
block.appendChild(handle);
|
||||
lane.appendChild(block);
|
||||
});
|
||||
|
||||
track.appendChild(lane);
|
||||
lanesWrap.appendChild(track);
|
||||
});
|
||||
}
|
||||
|
||||
function onAnimClipMouseDown(e, clipId) {
|
||||
if (e.target.classList.contains('animClipResizeHandle')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var clip = ANIM_CLIPS.find(function(c) { return c.id === clipId; });
|
||||
var block = document.querySelector('.animClip[data-clip-id="' + clipId + '"]');
|
||||
var startX = e.clientX;
|
||||
var startLeft = parseFloat(block.style.left);
|
||||
var moved = false;
|
||||
function onMove(ev) {
|
||||
var dx = ev.clientX - startX;
|
||||
if (Math.abs(dx) > 3) moved = true;
|
||||
block.style.left = Math.max(0, startLeft + dx) + 'px';
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
if (moved) {
|
||||
var newStart = Math.round((parseFloat(block.style.left) / ANIM_PX_PER_SEC) * 10) / 10;
|
||||
clip.start_time = newStart;
|
||||
updateAnimClip(clipId, { start_time: newStart });
|
||||
} else {
|
||||
openAnimClipForm(clip);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function onAnimClipResizeMouseDown(e, clipId) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var clip = ANIM_CLIPS.find(function(c) { return c.id === clipId; });
|
||||
var block = document.querySelector('.animClip[data-clip-id="' + clipId + '"]');
|
||||
var startX = e.clientX;
|
||||
var startWidth = parseFloat(block.style.width);
|
||||
function onMove(ev) {
|
||||
var dx = ev.clientX - startX;
|
||||
block.style.width = Math.max(10, startWidth + dx) + 'px';
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
var newDuration = Math.round((parseFloat(block.style.width) / ANIM_PX_PER_SEC) * 10) / 10;
|
||||
clip.duration = newDuration;
|
||||
updateAnimClip(clipId, { duration: newDuration });
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
function updateAnimClip(clipId, fields) {
|
||||
return fetch(animUrl('/animations/clips/' + clipId + '/update'), {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(fields)
|
||||
}).then(function(r) { return r.json(); }).then(function(updated) {
|
||||
var idx = ANIM_CLIPS.findIndex(function(c) { return c.id === updated.id; });
|
||||
if (idx > -1) ANIM_CLIPS[idx] = updated;
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
function addAnimTrack() {
|
||||
var elId = parseInt(document.getElementById('anim-new-track-element').value, 10);
|
||||
fetch(animUrl('/screens/' + ANIM_SCREEN_ID + '/animations/clips/add'), {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ element_id: elId, kind: 'animate_css', animate_name: 'fadeIn', start_time: 0, duration: 1 }),
|
||||
}).then(function(r) { return r.json(); }).then(function(clip) {
|
||||
ANIM_CLIPS.push(clip);
|
||||
renderAnimTimeline();
|
||||
openAnimClipForm(clip);
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
if (kind === 'sprite') populateAnimClipSpriteGrid();
|
||||
}
|
||||
|
||||
// Phase 8 — mirror de onSpriteTargetElementChange() (flow-editor.js), mais
|
||||
// scopé au personnage PROPRIÉTAIRE de ce clip (clip.element_id, fixé à la
|
||||
// création — voir addAnimTrack()) : pas de sélecteur d'élément séparé ici,
|
||||
// contrairement au nœud de flow (une action peut viser n'importe quel
|
||||
// personnage, un clip appartient à un seul élément dès sa création).
|
||||
function populateAnimClipSpriteGrid(preselectedName) {
|
||||
var grid = document.getElementById('anim-clip-sprite-animation-grid');
|
||||
var nameField = document.getElementById('anim-clip-sprite-animation-name');
|
||||
var noAnimHint = document.getElementById('anim-clip-sprite-no-animations');
|
||||
grid.innerHTML = '';
|
||||
var clip = ANIM_CLIPS.find(function(c) { return c.id === animCurrentClipId; });
|
||||
var animations = clip ? (ELEMENT_ANIMATIONS_MAP[clip.element_id] || {}) : {};
|
||||
var hasAnimations = Object.keys(animations).length > 0;
|
||||
noAnimHint.style.display = hasAnimations ? 'none' : 'block';
|
||||
Object.keys(animations).forEach(function(animName) {
|
||||
var frames = animations[animName];
|
||||
if (!frames.length) return;
|
||||
var tile = document.createElement('button');
|
||||
tile.type = 'button';
|
||||
tile.className = 'iconGalleryTile' + (animName === preselectedName ? ' selected' : '');
|
||||
tile.title = animName;
|
||||
tile.innerHTML = '<img src="' + frames[0] + '" alt="" class="personnageThumb">';
|
||||
tile.onclick = function() {
|
||||
grid.querySelectorAll('.iconGalleryTile').forEach(function(t) { t.classList.remove('selected'); });
|
||||
tile.classList.add('selected');
|
||||
nameField.value = animName;
|
||||
// Vitesse par défaut proportionnelle au nombre d'images — voir le
|
||||
// même correctif dans onSpriteTargetElementChange() (flow-editor.js).
|
||||
var fpsField = document.getElementById('anim-clip-sprite-fps');
|
||||
if (fpsField) fpsField.value = Math.max(8, Math.min(24, frames.length));
|
||||
};
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
if (preselectedName && animations[preselectedName]) nameField.value = preselectedName;
|
||||
}
|
||||
|
||||
function addAnimKeyframeRow(percent, cssText) {
|
||||
var wrap = document.getElementById('anim-clip-keyframes');
|
||||
var row = document.createElement('div');
|
||||
row.className = 'fieldRow animKeyframeRow';
|
||||
var percentInput = document.createElement('input');
|
||||
percentInput.type = 'number'; percentInput.min = '0'; percentInput.max = '100'; percentInput.step = '1';
|
||||
percentInput.className = 'anim-kf-percent'; percentInput.style.flex = '0 0 70px';
|
||||
percentInput.value = percent != null ? percent : 0;
|
||||
var cssInput = document.createElement('textarea');
|
||||
cssInput.className = 'anim-kf-css'; cssInput.style.flex = '1'; cssInput.rows = 2;
|
||||
cssInput.placeholder = 'ex. opacity: 0; transform: scale(0.5);';
|
||||
cssInput.value = cssText || '';
|
||||
var removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button'; removeBtn.className = 'smallBtn danger';
|
||||
removeBtn.textContent = '✕';
|
||||
removeBtn.onclick = function() { row.remove(); };
|
||||
row.appendChild(percentInput);
|
||||
row.appendChild(cssInput);
|
||||
row.appendChild(removeBtn);
|
||||
wrap.appendChild(row);
|
||||
}
|
||||
|
||||
function collectAnimKeyframes() {
|
||||
var rows = document.querySelectorAll('#anim-clip-keyframes .animKeyframeRow');
|
||||
var keyframes = [];
|
||||
rows.forEach(function(row) {
|
||||
var percent = parseFloat(row.querySelector('.anim-kf-percent').value) || 0;
|
||||
var cssText = row.querySelector('.anim-kf-css').value;
|
||||
var props = {};
|
||||
cssText.split(';').forEach(function(decl) {
|
||||
var parts = decl.split(':');
|
||||
if (parts.length === 2) {
|
||||
var prop = parts[0].trim(), val = parts[1].trim();
|
||||
if (prop && val) props[prop] = val;
|
||||
}
|
||||
});
|
||||
keyframes.push({ percent: percent, props: props });
|
||||
});
|
||||
return keyframes;
|
||||
}
|
||||
|
||||
function openAnimClipForm(clip) {
|
||||
animCurrentClipId = clip.id;
|
||||
document.getElementById('animClipForm').style.display = 'block';
|
||||
document.getElementById('animClipFormTitle').textContent = 'Clip — ' + (ELEMENTS_LABELS[clip.element_id] || ('elt-' + clip.element_id));
|
||||
document.getElementById('anim-clip-kind').value = clip.kind || 'animate_css';
|
||||
document.getElementById('anim-clip-animate-name').value = clip.animate_name || 'fadeIn';
|
||||
document.getElementById('anim-clip-start').value = clip.start_time || 0;
|
||||
document.getElementById('anim-clip-duration').value = clip.duration || 1;
|
||||
document.getElementById('anim-clip-delay').value = clip.delay || 0;
|
||||
document.getElementById('anim-clip-easing').value = clip.easing || 'ease';
|
||||
document.getElementById('anim-clip-iteration').value = clip.iteration_count || '1';
|
||||
document.getElementById('anim-clip-keyframes').innerHTML = '';
|
||||
var keyframes = [];
|
||||
if (clip.custom_keyframes) {
|
||||
try { keyframes = JSON.parse(clip.custom_keyframes); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (!keyframes.length) keyframes = [{ percent: 0, props: { opacity: '0' } }, { percent: 100, props: { opacity: '1' } }];
|
||||
keyframes.forEach(function(k) {
|
||||
var cssText = Object.keys(k.props || {}).map(function(p) { return p + ': ' + k.props[p] + ';'; }).join(' ');
|
||||
addAnimKeyframeRow(k.percent, cssText);
|
||||
});
|
||||
// Phase 8 — clip.custom_keyframes stocke {animation, fps} en JSON pour un
|
||||
// clip kind="sprite" (pas une liste d'images-clés CSS, voir ci-dessus) :
|
||||
// le NOM de l'animation, pas ses frames — résolues à l'exécution depuis
|
||||
// le personnage ACTUELLEMENT assigné (voir resolveSpriteFrames() dans
|
||||
// static/js/play/actions.js), jamais figées ici.
|
||||
var spriteData = {};
|
||||
if (clip.kind === 'sprite' && clip.custom_keyframes) {
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes); } catch (e) { /* ignore */ }
|
||||
}
|
||||
document.getElementById('anim-clip-sprite-fps').value = spriteData.fps || 8;
|
||||
toggleAnimClipKind();
|
||||
if (clip.kind === 'sprite') populateAnimClipSpriteGrid(spriteData.animation);
|
||||
document.querySelectorAll('.animClip').forEach(function(b) {
|
||||
b.classList.toggle('selected', parseInt(b.dataset.clipId, 10) === clip.id);
|
||||
});
|
||||
}
|
||||
|
||||
function closeAnimClipForm() {
|
||||
animCurrentClipId = null;
|
||||
document.getElementById('animClipForm').style.display = 'none';
|
||||
document.querySelectorAll('.animClip').forEach(function(b) { b.classList.remove('selected'); });
|
||||
}
|
||||
|
||||
function saveAnimClipForm() {
|
||||
if (!animCurrentClipId) return Promise.resolve();
|
||||
var kind = document.getElementById('anim-clip-kind').value;
|
||||
var fields = {
|
||||
kind: kind,
|
||||
animate_name: document.getElementById('anim-clip-animate-name').value,
|
||||
start_time: parseFloat(document.getElementById('anim-clip-start').value) || 0,
|
||||
duration: parseFloat(document.getElementById('anim-clip-duration').value) || 1,
|
||||
delay: parseFloat(document.getElementById('anim-clip-delay').value) || 0,
|
||||
easing: document.getElementById('anim-clip-easing').value,
|
||||
iteration_count: document.getElementById('anim-clip-iteration').value,
|
||||
};
|
||||
if (kind === 'custom') fields.custom_keyframes = collectAnimKeyframes();
|
||||
if (kind === 'sprite') {
|
||||
// Phase 8 — stocke le NOM de l'animation choisie (miniature cliquée,
|
||||
// voir populateAnimClipSpriteGrid()), PAS ses frames — résolues à
|
||||
// l'exécution depuis le personnage ACTUELLEMENT assigné (voir
|
||||
// resolveSpriteFrames() dans static/js/play/actions.js), pour que
|
||||
// changer de personnage Forge affecte aussi les clips déjà posés dans
|
||||
// la Timeline. La boucle reste portée par iteration_count
|
||||
// (1/2/3/infinite, ci-dessus).
|
||||
fields.custom_keyframes = {
|
||||
animation: document.getElementById('anim-clip-sprite-animation-name').value,
|
||||
fps: parseInt(document.getElementById('anim-clip-sprite-fps').value, 10) || 8,
|
||||
};
|
||||
}
|
||||
return updateAnimClip(animCurrentClipId, fields).then(function() { renderAnimTimeline(); });
|
||||
}
|
||||
|
||||
function deleteAnimClipForm() {
|
||||
if (!animCurrentClipId) return;
|
||||
if (!confirm('Supprimer ce clip ?')) return;
|
||||
var id = animCurrentClipId;
|
||||
fetch(animUrl('/animations/clips/' + id + '/delete'), { method: 'POST' }).then(function() {
|
||||
ANIM_CLIPS = ANIM_CLIPS.filter(function(c) { return c.id !== id; });
|
||||
closeAnimClipForm();
|
||||
renderAnimTimeline();
|
||||
});
|
||||
}
|
||||
|
||||
function previewAnimClip() {
|
||||
if (!animCurrentClipId) return;
|
||||
saveAnimClipForm().then(function() {
|
||||
var clip = ANIM_CLIPS.find(function(c) { return c.id === animCurrentClipId; });
|
||||
if (!clip) return;
|
||||
var target = document.querySelector('#canvas [data-anim-target="' + clip.element_id + '"]');
|
||||
if (!target) return;
|
||||
var duration = clip.duration || 1, delay = clip.delay || 0;
|
||||
var iterationCount = clip.iteration_count || '1';
|
||||
if (clip.kind === 'custom') {
|
||||
var keyframes = [];
|
||||
try { keyframes = JSON.parse(clip.custom_keyframes || '[]'); } catch (e) { /* ignore */ }
|
||||
var steps = keyframes.map(function(k) {
|
||||
var props = Object.keys(k.props || {}).map(function(p) { return p + ':' + k.props[p] + ';'; }).join(' ');
|
||||
return k.percent + '% { ' + props + ' }';
|
||||
}).join(' ');
|
||||
var styleTag = document.getElementById('animEditorPreviewStyle');
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'animEditorPreviewStyle';
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
styleTag.textContent = '@keyframes animEditorPreview { ' + steps + ' }';
|
||||
target.style.animation = 'animEditorPreview ' + duration + 's ' + (clip.easing || 'ease') + ' ' + delay + 's ' + iterationCount;
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.style.animation = '';
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
} else if (clip.kind === 'sprite') {
|
||||
var spriteData = {};
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* ignore */ }
|
||||
// Phase 8 — résout le NOM de l'animation depuis les animations
|
||||
// ACTUELLES du personnage (même logique que resolveSpriteFrames()
|
||||
// côté jeu, mais ELEMENT_ANIMATIONS_MAP n'existe que dans l'éditeur).
|
||||
var ownerAnimations = ELEMENT_ANIMATIONS_MAP[clip.element_id] || {};
|
||||
var frames = ownerAnimations[spriteData.animation] || [];
|
||||
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');
|
||||
target.style.setProperty('--animate-delay', delay + 's');
|
||||
target.style.animationIterationCount = iterationCount;
|
||||
target.classList.add('animate__animated', animClass);
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.classList.remove('animate__animated', animClass);
|
||||
target.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Panneau timeline absent sur un écran-modèle (élément de jeu), comme le
|
||||
// panneau de logique — voir le commentaire Jinja au-dessus de .builderTabs.
|
||||
var HAS_ANIM_PANEL = !!document.getElementById('animTabPanel');
|
||||
if (HAS_ANIM_PANEL) { renderAnimTimeline(); }
|
||||
@@ -1,875 +0,0 @@
|
||||
// ---------- Logique de la scène : éditeur de flow à nœuds ----------
|
||||
//
|
||||
// FLOW_NODES_INITIAL/FLOW_EDGES_INITIAL/FLOW_BLOCKS/ELEMENTS_LABELS/
|
||||
// SCREENS_MAP/*_LABELS_MAP/CUSTOM_EVENTS_MAP/GAME_SLUG/SCREEN_ID sont
|
||||
// posés par le petit <script> inline en tête de screen_edit.html (données
|
||||
// Jinja) — voir static/js/play/ pour le même patron déjà en place côté
|
||||
// moteur de jeu.
|
||||
var FLOW_SCREEN_ID = SCREEN_ID;
|
||||
// Fondations multi-éditeurs — ce même fichier (moteur de logique de flow
|
||||
// partagé, voir le plan "Fondations d'une plateforme multi-éditeurs")
|
||||
// sert aussi bien à l'éditeur document (cible trigger_element_id/
|
||||
// target_element_id, vers _screen_elements) qu'à l'éditeur de scène 2D
|
||||
// (cible trigger_object_id/target_object_id, vers _scene_objects) : posé
|
||||
// à `true` par templates/scene_edit.html, sinon absent (donc falsy) sur
|
||||
// templates/screen_edit.html — décide quelle paire de colonnes remplir à
|
||||
// l'enregistrement d'un nœud (voir submitNodeForm()).
|
||||
var FLOW_TARGETS_OBJECTS = typeof FLOW_TARGETS_OBJECTS !== 'undefined' ? FLOW_TARGETS_OBJECTS : false;
|
||||
var FLOW_TRIGGER_ID_FIELD = FLOW_TARGETS_OBJECTS ? 'trigger_object_id' : 'trigger_element_id';
|
||||
var FLOW_TARGET_ID_FIELD = FLOW_TARGETS_OBJECTS ? 'target_object_id' : 'target_element_id';
|
||||
// Blocs de logique (voir screens/flow/blocks/) : ALL_FLOW porte TOUS les
|
||||
// nœuds/arêtes de cet écran, tous blocs confondus (chargement inchangé) ;
|
||||
// un seul bloc est ouvert à la fois (modale unique, à la Unity) — pas
|
||||
// besoin d'une seconde copie filtrée à tenir synchronisée, currentBlockNodes()/
|
||||
// currentBlockEdges() recalculent la vue du bloc ouvert à chaque rendu.
|
||||
var ALL_FLOW = {
|
||||
nodes: FLOW_NODES_INITIAL,
|
||||
edges: FLOW_EDGES_INITIAL,
|
||||
};
|
||||
var CURRENT_BLOCK_ID = null;
|
||||
|
||||
function currentBlockNodes() {
|
||||
return ALL_FLOW.nodes.filter(function(n) { return n.block_id === CURRENT_BLOCK_ID; });
|
||||
}
|
||||
function currentBlockEdges() {
|
||||
var ids = {};
|
||||
currentBlockNodes().forEach(function(n) { ids[n.id] = true; });
|
||||
return ALL_FLOW.edges.filter(function(e) { return ids[e.from_node_id] && ids[e.to_node_id]; });
|
||||
}
|
||||
|
||||
var flowPendingPort = null; // {nodeId, port} en attente de connexion
|
||||
|
||||
function flowUrl(path) { return "/game/" + GAME_SLUG + path; }
|
||||
|
||||
function nodeLabel(node) {
|
||||
if (node.node_type === 'trigger') {
|
||||
if (node.trigger_event === 'affichage') return '🖥️ ' + TRIGGER_LABELS_MAP['affichage'];
|
||||
if (node.trigger_event === 'evenement') {
|
||||
var listenedEvent = CUSTOM_EVENTS_MAP[node.trigger_custom_event_id];
|
||||
return '📣 Sur « ' + (listenedEvent ? listenedEvent.name : '?') + ' »';
|
||||
}
|
||||
if (node.trigger_event === 'clavier') {
|
||||
return '⌨️ Touche « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
|
||||
}
|
||||
if (node.trigger_event === 'minuteur') {
|
||||
return '⏱️ Toutes les ' + (node.trigger_interval_ms || '?') + ' ms';
|
||||
}
|
||||
if (node.trigger_event === 'touche_maintenue') {
|
||||
return '⌨️⏳ Touche maintenue « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
|
||||
}
|
||||
if (node.trigger_event === 'touche_relachee') {
|
||||
return '⌨️⬆️ Relâchement de « ' + (node.trigger_key === ' ' ? 'Espace' : (node.trigger_key || '?')) + ' »';
|
||||
}
|
||||
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
|
||||
(ELEMENTS_LABELS[node.trigger_element_id || node.trigger_object_id] || '?');
|
||||
}
|
||||
if (node.node_type === 'condition') {
|
||||
var clauseSubject = function(c, field, variable) {
|
||||
return (c.source ?? c.cond_source) === 'variable' ? ('🔧 ' + (variable || c.cond_variable || '?')) : (field || c.cond_field || '?');
|
||||
};
|
||||
// Phase 5 — condition de collision : pas de champ/opérateur/valeur,
|
||||
// juste les deux éléments comparés (voir evaluateConditionClause()
|
||||
// dans static/js/play/conditions.js).
|
||||
var clauseFullLabel = function(c, field, variable) {
|
||||
if ((c.source ?? c.cond_source) === 'collision') {
|
||||
var elA = ELEMENTS_LABELS[c.element_a ?? c.cond_element_a] || '?';
|
||||
var elB = ELEMENTS_LABELS[c.element_b ?? c.cond_element_b] || '?';
|
||||
return '💥 ' + elA + ' touche ' + elB;
|
||||
}
|
||||
return clauseSubject(c, field, variable) + ' ' + (CONDITION_OPERATOR_LABELS_MAP[c.operator ?? c.cond_operator] || (c.operator ?? c.cond_operator)) + ' ' + ((c.value ?? c.cond_value) || '');
|
||||
};
|
||||
var mainClause = '❓ SI ' + clauseFullLabel(node, node.cond_field, node.cond_variable);
|
||||
if (node.cond_clauses) {
|
||||
// 2.4 — plusieurs clauses combinées : on affiche chaque champ/variable
|
||||
// testé, relié par le connecteur ET/OU choisi, plutôt qu'une seule clause.
|
||||
try {
|
||||
var clauses = JSON.parse(node.cond_clauses);
|
||||
var combLabel = node.cond_combinator === 'ou' ? ' OU ' : ' ET ';
|
||||
return '❓ SI ' + clauses.map(function(c) {
|
||||
return clauseFullLabel(c, c.field, c.variable);
|
||||
}).join(combLabel);
|
||||
} catch (e) { /* JSON invalide : on retombe sur la clause historique */ }
|
||||
}
|
||||
return mainClause;
|
||||
}
|
||||
// action
|
||||
var base = '⚡ ' + (ACTION_TYPE_LABELS_MAP[node.action_type] || node.action_type);
|
||||
if (node.action_type === 'aller_a') {
|
||||
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?');
|
||||
} else if (node.action_type === 'ouvrir_ligne') {
|
||||
base += ' → ' + (SCREENS_MAP[node.target_screen_id] || '?') + ' (ligne cliquée)';
|
||||
} else if (node.action_type === 'activer_onglet') {
|
||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)';
|
||||
} else if (node.action_type === 'modifier_objet_scene') {
|
||||
// Fondations multi-éditeurs — équivalent scène de "modifier_element"
|
||||
// ci-dessous, palette réduite (visibilité/position en pixels/
|
||||
// orientation seulement).
|
||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id || node.target_object_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
|
||||
if (node.element_property === 'visibilite') {
|
||||
base += ' = ' + (ELEMENT_VISIBILITY_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 {
|
||||
base += ' = ' + node.element_value;
|
||||
}
|
||||
} else if (node.action_type === 'modifier_element') {
|
||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
|
||||
if (node.element_property === 'visibilite') {
|
||||
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
|
||||
} else if (node.element_property === 'surbrillance') {
|
||||
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)';
|
||||
} else {
|
||||
base += ' = ' + node.element_value;
|
||||
}
|
||||
} else if (node.action_type === 'modifier_donnee') {
|
||||
base += ' → ' + (node.target_field || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
||||
if (node.data_value) base += ' ' + node.data_value;
|
||||
} else if (node.action_type === 'modifier_variable') {
|
||||
base += ' → ' + (node.target_variable || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
||||
if (node.data_value) base += ' ' + node.data_value;
|
||||
} else if (node.action_type === 'modifier_score') {
|
||||
base += ' → ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
||||
if (node.data_value) base += ' ' + node.data_value;
|
||||
} else if (node.action_type === 'definir_statut_partie') {
|
||||
base += ' → ' + (SCORE_STATUS_LABELS_MAP[node.data_value] || node.data_value || '?');
|
||||
} else if (node.action_type === 'attendre') {
|
||||
base += ' (' + (node.data_value || '0') + ' s)';
|
||||
} else if (node.action_type === 'declencher_evenement') {
|
||||
// Notification pure : rien d'autre à afficher que le nom de
|
||||
// l'événement (voir screens/custom_events/) — aucun élément/ligne
|
||||
// n'est jamais transmis au déclenchement.
|
||||
var dispatchedEvent = CUSTOM_EVENTS_MAP[node.target_custom_event_id];
|
||||
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 || '?') + ' »';
|
||||
} else if (node.action_type === 'jouer_animation_sprite') {
|
||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id || node.target_object_id] || '?');
|
||||
try {
|
||||
var spriteData = JSON.parse(node.data_value || '{}');
|
||||
base += ' « ' + (spriteData.animation || '?') + ' » (' + (spriteData.fps || '?') + ' i/s' + (spriteData.loop ? ', boucle' : '') + ')';
|
||||
} catch (e) { /* data_value invalide : pas de détail affiché */ }
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function renderFlow() {
|
||||
var canvas = document.getElementById('flowCanvas');
|
||||
var svg = document.getElementById('flowEdges');
|
||||
canvas.innerHTML = '';
|
||||
svg.innerHTML = '';
|
||||
|
||||
currentBlockNodes().forEach(function(node) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'flowNode flowNode-' + node.node_type;
|
||||
div.style.left = node.x + 'px';
|
||||
div.style.top = node.y + 'px';
|
||||
div.dataset.nodeId = node.id;
|
||||
|
||||
var del = document.createElement('button');
|
||||
del.className = 'flowNodeDelete';
|
||||
del.type = 'button';
|
||||
del.textContent = '✕';
|
||||
del.onclick = function(ev) { ev.stopPropagation(); deleteFlowNode(node.id); };
|
||||
div.appendChild(del);
|
||||
|
||||
var body = document.createElement('div');
|
||||
body.className = 'flowNodeBody';
|
||||
body.textContent = nodeLabel(node);
|
||||
div.appendChild(body);
|
||||
|
||||
if (node.node_type !== 'trigger') {
|
||||
var inPort = document.createElement('div');
|
||||
inPort.className = 'flowPort flowPortIn';
|
||||
inPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'in', null); };
|
||||
div.appendChild(inPort);
|
||||
}
|
||||
if (node.node_type === 'condition') {
|
||||
var outTrue = document.createElement('div');
|
||||
outTrue.className = 'flowPort flowPortOut flowPortTrue';
|
||||
outTrue.title = 'Vrai';
|
||||
outTrue.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'vrai'); };
|
||||
div.appendChild(outTrue);
|
||||
var outFalse = document.createElement('div');
|
||||
outFalse.className = 'flowPort flowPortOut flowPortFalse';
|
||||
outFalse.title = 'Faux';
|
||||
outFalse.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'faux'); };
|
||||
div.appendChild(outFalse);
|
||||
} else {
|
||||
var outPort = document.createElement('div');
|
||||
outPort.className = 'flowPort flowPortOut';
|
||||
outPort.onclick = function(ev) { ev.stopPropagation(); onPortClick(node.id, 'out', 'out'); };
|
||||
div.appendChild(outPort);
|
||||
}
|
||||
|
||||
div.addEventListener('mousedown', function(e) { onFlowNodeMouseDown(e, node.id); });
|
||||
canvas.appendChild(div);
|
||||
});
|
||||
|
||||
// Redessine les fils après que les nœuds soient dans le DOM (pour lire leur position réelle).
|
||||
requestAnimationFrame(drawFlowEdges);
|
||||
}
|
||||
|
||||
function portPosition(nodeId, side, sub) {
|
||||
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
|
||||
if (!div) return null;
|
||||
var wrap = document.querySelector('.flowCanvasWrap');
|
||||
var wrapRect = wrap.getBoundingClientRect();
|
||||
var selector = side === 'in' ? '.flowPortIn' : (sub === 'vrai' ? '.flowPortTrue' : (sub === 'faux' ? '.flowPortFalse' : '.flowPortOut'));
|
||||
var port = div.querySelector(selector);
|
||||
if (!port) return null;
|
||||
var r = port.getBoundingClientRect();
|
||||
return { x: r.left + r.width / 2 - wrapRect.left + wrap.scrollLeft, y: r.top + r.height / 2 - wrapRect.top + wrap.scrollTop };
|
||||
}
|
||||
|
||||
function drawFlowEdges() {
|
||||
var svg = document.getElementById('flowEdges');
|
||||
svg.innerHTML = '';
|
||||
currentBlockEdges().forEach(function(edge) {
|
||||
var from = portPosition(edge.from_node_id, 'out', edge.from_port);
|
||||
var to = portPosition(edge.to_node_id, 'in', null);
|
||||
if (!from || !to) return;
|
||||
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', from.x); line.setAttribute('y1', from.y);
|
||||
line.setAttribute('x2', to.x); line.setAttribute('y2', to.y);
|
||||
line.setAttribute('class', 'flowEdgeLine' + (edge.from_port === 'faux' ? ' flowEdgeFalse' : (edge.from_port === 'vrai' ? ' flowEdgeTrue' : '')));
|
||||
line.style.pointerEvents = 'stroke';
|
||||
line.addEventListener('click', function() { deleteFlowEdge(edge.id); });
|
||||
svg.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
function onPortClick(nodeId, side, port) {
|
||||
if (side === 'out') {
|
||||
flowPendingPort = { nodeId: nodeId, port: port };
|
||||
return;
|
||||
}
|
||||
// side === 'in'
|
||||
if (!flowPendingPort) return;
|
||||
if (flowPendingPort.nodeId === nodeId) { flowPendingPort = null; return; }
|
||||
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/edges/add'), {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId })
|
||||
}).then(function(r){ return r.json(); }).then(function(edge){
|
||||
ALL_FLOW.edges.push({ id: edge.id, screen_id: FLOW_SCREEN_ID, from_node_id: flowPendingPort.nodeId, from_port: flowPendingPort.port, to_node_id: nodeId });
|
||||
flowPendingPort = null;
|
||||
drawFlowEdges();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFlowEdge(edgeId) {
|
||||
fetch(flowUrl('/flow/edges/' + edgeId + '/delete'), { method: 'POST' }).then(function(){
|
||||
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.id !== edgeId; });
|
||||
drawFlowEdges();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFlowNode(nodeId) {
|
||||
fetch(flowUrl('/flow/nodes/' + nodeId + '/delete'), { method: 'POST' }).then(function(){
|
||||
ALL_FLOW.nodes = ALL_FLOW.nodes.filter(function(n){ return n.id !== nodeId; });
|
||||
ALL_FLOW.edges = ALL_FLOW.edges.filter(function(e){ return e.from_node_id !== nodeId && e.to_node_id !== nodeId; });
|
||||
renderFlow();
|
||||
});
|
||||
}
|
||||
|
||||
function onFlowNodeMouseDown(e, nodeId) {
|
||||
if (e.target.classList.contains('flowPort') || e.target.classList.contains('flowNodeDelete')) return;
|
||||
e.preventDefault();
|
||||
var node = ALL_FLOW.nodes.find(function(n){ return n.id === nodeId; });
|
||||
var div = document.querySelector('.flowNode[data-node-id="' + nodeId + '"]');
|
||||
var startX = e.clientX, startY = e.clientY;
|
||||
var startLeft = node.x, startTop = node.y;
|
||||
function onMove(ev) {
|
||||
var dx = ev.clientX - startX, dy = ev.clientY - startY;
|
||||
node.x = Math.max(0, startLeft + dx);
|
||||
node.y = Math.max(0, startTop + dy);
|
||||
div.style.left = node.x + 'px';
|
||||
div.style.top = node.y + 'px';
|
||||
drawFlowEdges();
|
||||
}
|
||||
function onUp() {
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
document.removeEventListener('mouseup', onUp);
|
||||
fetch(flowUrl('/flow/nodes/' + nodeId + '/move'), {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ x: node.x, y: node.y })
|
||||
});
|
||||
}
|
||||
document.addEventListener('mousemove', onMove);
|
||||
document.addEventListener('mouseup', onUp);
|
||||
}
|
||||
|
||||
// ---------- Formulaire d'ajout de nœud ----------
|
||||
|
||||
var currentNodeFormType = null;
|
||||
|
||||
function openNodeForm(type) {
|
||||
currentNodeFormType = type;
|
||||
document.getElementById('nodeForm').style.display = 'block';
|
||||
document.getElementById('nodeFormTitle').textContent =
|
||||
type === 'trigger' ? 'Nouveau déclencheur' : (type === 'condition' ? 'Nouvelle condition' : 'Nouvelle action');
|
||||
document.getElementById('nodeFormTrigger').style.display = type === 'trigger' ? 'block' : 'none';
|
||||
document.getElementById('nodeFormCondition').style.display = type === 'condition' ? 'block' : 'none';
|
||||
document.getElementById('nodeFormAction').style.display = type === 'action' ? 'block' : 'none';
|
||||
if (type === 'trigger') toggleFlowTriggerFields();
|
||||
if (type === 'condition') {
|
||||
document.getElementById('nf-cond-source').value = 'objet';
|
||||
onFlowConditionSourceChange();
|
||||
onFlowDefinitionChange('cond');
|
||||
flowConditionExtraClauses = [];
|
||||
renderFlowConditionClauses();
|
||||
}
|
||||
if (type === 'action') {
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
toggleFlowActionFields();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFlowTriggerFields() {
|
||||
var eventVal = document.getElementById('nf-trigger-event').value;
|
||||
var isAffichage = eventVal === 'affichage';
|
||||
// "Sur un événement personnalisé" ne concerne ni un élément précis ni
|
||||
// l'écran entier : c'est un scan global (voir dispatchGameEvent() dans
|
||||
// play.html), le seul réglage propre à ce nœud est QUEL événement.
|
||||
var isCustomEvent = eventVal === 'evenement';
|
||||
// Idem "clavier"/"minuteur" (Phase 3) : ni élément ni écran précis.
|
||||
// "touche_maintenue"/"touche_relachee" (mouvement continu) réutilisent
|
||||
// le même champ trigger_key que "clavier".
|
||||
var isKeyboard = eventVal === 'clavier' || eventVal === 'touche_maintenue' || eventVal === 'touche_relachee';
|
||||
var isTimer = eventVal === 'minuteur';
|
||||
var noElement = isAffichage || isCustomEvent || isKeyboard || isTimer;
|
||||
document.getElementById('nf-trigger-element-row').style.display = noElement ? 'none' : 'block';
|
||||
document.getElementById('nf-trigger-repeater-hint').style.display = noElement ? 'none' : 'block';
|
||||
document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none';
|
||||
document.getElementById('nf-trigger-event-row').style.display = isCustomEvent ? 'block' : 'none';
|
||||
document.getElementById('nf-trigger-key-row').style.display = isKeyboard ? 'block' : 'none';
|
||||
document.getElementById('nf-trigger-interval-row').style.display = isTimer ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Capture la touche pressée dans le champ "Touche à surveiller" (Phase 3)
|
||||
// — évite de faire deviner la syntaxe attendue (ev.key du navigateur,
|
||||
// ex. "ArrowUp", "a", " " pour la barre d'espace) : l'input lui-même
|
||||
// écoute keydown et se remplit tout seul, jamais tapé à la main.
|
||||
function captureFlowTriggerKey(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('nf-trigger-key').value = event.key;
|
||||
document.getElementById('nf-trigger-key-display').value = event.key === ' ' ? 'Espace' : event.key;
|
||||
}
|
||||
|
||||
function closeNodeForm() {
|
||||
document.getElementById('nodeForm').style.display = 'none';
|
||||
currentNodeFormType = null;
|
||||
}
|
||||
|
||||
// CLICKED_ROW_ID (-1, voir screens/flow/constants.py) : "la ligne de
|
||||
// Répéteur sur laquelle on vient de cliquer" — pour un déclencheur "Au
|
||||
// clic" posé sur un Répéteur, chaque ligne exécute le MÊME graphe et la
|
||||
// ligne cliquée n'est jamais connue à l'avance ; choisir une ligne fixe
|
||||
// dans l'éditeur n'a donc pas de sens pour ce cas. Résolue au moment de
|
||||
// l'exécution (window.lastClickedRowId, voir play.html), pas à la
|
||||
// création du nœud — proposée en tête de TOUTE liste "Ligne concernée",
|
||||
// avant les lignes réelles de l'objet.
|
||||
var FLOW_CLICKED_ROW_ID = -1;
|
||||
// LAST_INSERTED_ROW_ID (-3, voir screens/flow/constants.py) : "la ligne
|
||||
// qu'un nœud Action 'Ajouter une ligne' vient de créer" (Phase 4) — même
|
||||
// principe que CLICKED_ROW_ID ci-dessus, résolue au moment de l'exécution
|
||||
// (window.lastInsertedRowId, voir static/js/play/actions.js) puisqu'un
|
||||
// id de ligne n'existe qu'APRÈS l'insertion, jamais connu à la création
|
||||
// du nœud. Permet d'enchaîner "Ajouter une ligne" -> "Modifier une
|
||||
// donnée"/condition ciblant cette même ligne fraîchement créée.
|
||||
var FLOW_LAST_INSERTED_ROW_ID = -3;
|
||||
|
||||
function _flowRowOptionsHtml(info) {
|
||||
var options = '<option value="' + FLOW_CLICKED_ROW_ID + '">🖱️ Ligne cliquée (Répéteur)</option>';
|
||||
options += '<option value="' + FLOW_LAST_INSERTED_ROW_ID + '">➕ Dernière ligne ajoutée</option>';
|
||||
options += info.rows.length
|
||||
? info.rows.map(function(r){ return '<option value="' + r.id + '">' + r.label + '</option>'; }).join('')
|
||||
: '<option value="" disabled>Aucune donnée dans cet objet</option>';
|
||||
return options;
|
||||
}
|
||||
|
||||
function onFlowDefinitionChange(prefix) {
|
||||
var defSel = document.getElementById(prefix === 'cond' ? 'nf-cond-definition' : 'nf-action-data-definition');
|
||||
if (!defSel) return;
|
||||
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
||||
var rowSel = document.getElementById(prefix === 'cond' ? 'nf-cond-row' : 'nf-action-data-row');
|
||||
var fieldSel = document.getElementById(prefix === 'cond' ? 'nf-cond-field' : 'nf-action-data-field');
|
||||
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
||||
fieldSel.innerHTML = info.fields.length
|
||||
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
||||
: '<option value="">Aucun champ simple</option>';
|
||||
}
|
||||
|
||||
function onFlowFieldChange() {
|
||||
// Pas d'action nécessaire ici pour l'instant : le type du champ est relu
|
||||
// directement sur l'option sélectionnée au moment de la création du nœud.
|
||||
}
|
||||
|
||||
// Bascule la clause PRINCIPALE d'un nœud Condition entre "Champ d'un
|
||||
// objet" (historique) et "Variable globale" — voir ensure_flow_schema.py
|
||||
// (cond_source) et evaluateConditionClause() dans play.html. Même bascule
|
||||
// pour une clause SUPPLÉMENTAIRE : voir onFlowClauseSourceChange() plus
|
||||
// bas, sur le même principe mais scopée par index.
|
||||
function onFlowConditionSourceChange() {
|
||||
var source = document.getElementById('nf-cond-source').value;
|
||||
var isVariable = source === 'variable';
|
||||
var isCollision = source === 'collision';
|
||||
document.getElementById('nf-cond-objet-fields').style.display = (isVariable || isCollision) ? 'none' : 'block';
|
||||
document.getElementById('nf-cond-variable-fields').style.display = isVariable ? 'block' : 'none';
|
||||
var hint = document.getElementById('nf-cond-no-objet-hint');
|
||||
if (hint) hint.style.display = (isVariable || isCollision) ? 'none' : 'block';
|
||||
// Collision (Phase 5) : ni opérateur ni valeur de comparaison — le
|
||||
// chevauchement EST directement le booléen vrai/faux du nœud (voir
|
||||
// evaluateConditionClause() dans static/js/play/conditions.js).
|
||||
document.getElementById('nf-cond-collision-fields').style.display = isCollision ? 'block' : 'none';
|
||||
document.getElementById('nf-cond-operator-value-row').style.display = isCollision ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// ---------- 2.4 — clauses supplémentaires (ET/OU) d'un nœud Condition ----------
|
||||
// Chaque clause supplémentaire réutilise EXACTEMENT le même modèle que la
|
||||
// clause principale (nf-cond-definition/row/field/operator/value) mais dans
|
||||
// une ligne indépendante, numérotée par index dans flowConditionExtraClauses.
|
||||
var flowConditionExtraClauses = [];
|
||||
|
||||
function addFlowConditionClause() {
|
||||
var firstDefSel = document.getElementById('nf-cond-definition');
|
||||
var defaultDefId = firstDefSel && firstDefSel.value ? firstDefSel.value : '';
|
||||
flowConditionExtraClauses.push({
|
||||
source: 'objet', definition_id: defaultDefId, row_id: '', field: '', field_type: '',
|
||||
variable: '', variable_chemin: '', operator: 'egal', value: '',
|
||||
});
|
||||
renderFlowConditionClauses();
|
||||
}
|
||||
|
||||
function removeFlowConditionClause(idx) {
|
||||
flowConditionExtraClauses.splice(idx, 1);
|
||||
renderFlowConditionClauses();
|
||||
}
|
||||
|
||||
// Bascule une clause SUPPLÉMENTAIRE (indexée) entre "Champ d'un objet" et
|
||||
// "Variable globale" — même principe que onFlowConditionSourceChange() pour
|
||||
// la clause principale, scopé par index plutôt que par id fixe.
|
||||
function onFlowClauseSourceChange(idx) {
|
||||
var sel = document.getElementById('nf-cond-extra-source-' + idx);
|
||||
if (!sel) return;
|
||||
flowConditionExtraClauses[idx].source = sel.value;
|
||||
var isVariable = sel.value === 'variable';
|
||||
var objetFields = document.getElementById('nf-cond-extra-objet-fields-' + idx);
|
||||
var varFields = document.getElementById('nf-cond-extra-variable-fields-' + idx);
|
||||
if (objetFields) objetFields.style.display = isVariable ? 'none' : 'block';
|
||||
if (varFields) varFields.style.display = isVariable ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function renderFlowConditionClauses() {
|
||||
var wrap = document.getElementById('nf-cond-extra-clauses');
|
||||
var combinatorRow = document.getElementById('nf-cond-combinator-row');
|
||||
if (!wrap || !combinatorRow) return;
|
||||
combinatorRow.style.display = flowConditionExtraClauses.length ? 'block' : 'none';
|
||||
var defOptionsHtml = document.getElementById('nf-cond-definition') ? document.getElementById('nf-cond-definition').innerHTML : '';
|
||||
wrap.innerHTML = flowConditionExtraClauses.map(function(clause, idx) {
|
||||
var isVariable = clause.source === 'variable';
|
||||
return '' +
|
||||
'<div class="flowConditionClauseRow" style="border-top:1px dashed var(--border); margin-top:8px; padding-top:8px;">' +
|
||||
'<label>Source<select id="nf-cond-extra-source-' + idx + '" onchange="onFlowClauseSourceChange(' + idx + ')">' +
|
||||
'<option value="objet"' + (isVariable ? '' : ' selected') + '>Champ d\'un objet</option>' +
|
||||
'<option value="variable"' + (isVariable ? ' selected' : '') + '>Variable globale</option>' +
|
||||
'</select></label>' +
|
||||
'<div id="nf-cond-extra-objet-fields-' + idx + '" style="display:' + (isVariable ? 'none' : 'block') + ';">' +
|
||||
'<label>Objet<select id="nf-cond-extra-definition-' + idx + '" onchange="onFlowClauseDefinitionChange(' + idx + ')">' + defOptionsHtml + '</select></label>' +
|
||||
'<label>Ligne concernée<select id="nf-cond-extra-row-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].row_id = this.value"></select></label>' +
|
||||
'<label>Champ<select id="nf-cond-extra-field-' + idx + '" onchange="onFlowClauseFieldChange(' + idx + ')"></select></label>' +
|
||||
'</div>' +
|
||||
'<div id="nf-cond-extra-variable-fields-' + idx + '" style="display:' + (isVariable ? 'block' : 'none') + ';">' +
|
||||
'<label>Variable<select id="nf-cond-extra-variable-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].variable = this.value">' +
|
||||
'<option value="">(sélectionner une variable)</option>' + GLOBAL_VARIABLES_OPTIONS_HTML +
|
||||
'</select></label>' +
|
||||
'<label>Chemin (optionnel)<input type="text" id="nf-cond-extra-variable-chemin-' + idx + '" placeholder="ex. .nom ou [0].valeur" value="' + (clause.variable_chemin || '').replace(/"/g, '"') + '" onchange="flowConditionExtraClauses[' + idx + '].variable_chemin = this.value"></label>' +
|
||||
'</div>' +
|
||||
'<label>Condition<select id="nf-cond-extra-operator-' + idx + '" onchange="flowConditionExtraClauses[' + idx + '].operator = this.value">' +
|
||||
CONDITION_OPERATORS_HTML +
|
||||
'</select></label>' +
|
||||
'<label>Valeur de comparaison<input type="text" id="nf-cond-extra-value-' + idx + '" placeholder="ex. 1, vrai, 10..." value="' + (clause.value || '').replace(/"/g, '"') + '" onchange="flowConditionExtraClauses[' + idx + '].value = this.value"></label>' +
|
||||
'<button type="button" class="smallBtn button is-small" onclick="removeFlowConditionClause(' + idx + ')">Retirer cette condition</button>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
flowConditionExtraClauses.forEach(function(clause, idx) {
|
||||
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
|
||||
if (defSel && clause.definition_id) defSel.value = clause.definition_id;
|
||||
onFlowClauseDefinitionChange(idx);
|
||||
var opSel = document.getElementById('nf-cond-extra-operator-' + idx);
|
||||
if (opSel) opSel.value = clause.operator || 'egal';
|
||||
var varSel = document.getElementById('nf-cond-extra-variable-' + idx);
|
||||
if (varSel && clause.variable) varSel.value = clause.variable;
|
||||
});
|
||||
}
|
||||
|
||||
function onFlowClauseDefinitionChange(idx) {
|
||||
var defSel = document.getElementById('nf-cond-extra-definition-' + idx);
|
||||
var rowSel = document.getElementById('nf-cond-extra-row-' + idx);
|
||||
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
|
||||
if (!defSel || !rowSel || !fieldSel) return;
|
||||
flowConditionExtraClauses[idx].definition_id = defSel.value;
|
||||
var info = DEFINITIONS_DATA[defSel.value] || { rows: [], fields: [] };
|
||||
rowSel.innerHTML = _flowRowOptionsHtml(info);
|
||||
fieldSel.innerHTML = info.fields.length
|
||||
? info.fields.map(function(f){ return '<option value="' + f.name + '" data-type="' + f.type + '">' + f.name + ' (' + f.type + ')</option>'; }).join('')
|
||||
: '<option value="">Aucun champ simple</option>';
|
||||
flowConditionExtraClauses[idx].row_id = rowSel.value;
|
||||
onFlowClauseFieldChange(idx);
|
||||
}
|
||||
|
||||
function onFlowClauseFieldChange(idx) {
|
||||
var fieldSel = document.getElementById('nf-cond-extra-field-' + idx);
|
||||
if (!fieldSel) return;
|
||||
var opt = fieldSel.options[fieldSel.selectedIndex];
|
||||
flowConditionExtraClauses[idx].field = fieldSel.value;
|
||||
flowConditionExtraClauses[idx].field_type = opt ? opt.dataset.type : '';
|
||||
}
|
||||
|
||||
function toggleFlowActionFields() {
|
||||
var typeSel = document.getElementById('nf-action-type');
|
||||
var screenSel = document.getElementById('nf-action-target-screen');
|
||||
var elementFields = document.getElementById('nf-action-element-fields');
|
||||
var objectFields = document.getElementById('nf-action-object-fields');
|
||||
var dataFields = document.getElementById('nf-action-data-fields');
|
||||
var variableFields = document.getElementById('nf-action-variable-fields');
|
||||
var scoreFields = document.getElementById('nf-action-score-fields');
|
||||
var statusFields = document.getElementById('nf-action-status-fields');
|
||||
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 spriteFields = document.getElementById('nf-action-sprite-fields');
|
||||
var isOnglet = typeSel.value === 'activer_onglet';
|
||||
// Champs propres à l'éditeur document ci-dessous (nf-action-element-*/
|
||||
// -onglet-*) : absents du DOM de l'éditeur de scène (templates/
|
||||
// scene_edit.html), d'où les gardes "if (x)" partout — même
|
||||
// #nodeForm/flow-editor.js réutilisé pour les deux éditeurs (voir
|
||||
// FLOW_TARGETS_OBJECTS en tête de ce fichier).
|
||||
if (soundFields) soundFields.style.display = typeSel.value === 'jouer_son' ? 'block' : 'none';
|
||||
if (spriteFields) spriteFields.style.display = typeSel.value === 'jouer_animation_sprite' ? 'block' : 'none';
|
||||
if (typeSel.value === 'jouer_animation_sprite') onSpriteTargetElementChange();
|
||||
if (screenSel) screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
||||
if (elementFields) elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
||||
if (objectFields) objectFields.style.display = typeSel.value === 'modifier_objet_scene' ? 'block' : 'none';
|
||||
if (dataFields) dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
||||
if (variableFields) variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
|
||||
if (scoreFields) scoreFields.style.display = typeSel.value === 'modifier_score' ? 'block' : 'none';
|
||||
if (statusFields) statusFields.style.display = typeSel.value === 'definir_statut_partie' ? 'block' : 'none';
|
||||
if (waitFields) waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
|
||||
if (eventFields) eventFields.style.display = typeSel.value === 'declencher_evenement' ? 'block' : 'none';
|
||||
if (addRowFields) addRowFields.style.display = typeSel.value === 'ajouter_ligne' ? 'block' : 'none';
|
||||
var ongletHint = document.getElementById('nf-action-onglet-hint');
|
||||
if (ongletHint) ongletHint.style.display = isOnglet ? 'block' : 'none';
|
||||
var elementPropertyWrap = document.getElementById('nf-action-element-property-wrap');
|
||||
if (elementPropertyWrap) elementPropertyWrap.style.display = isOnglet ? 'none' : 'block';
|
||||
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
|
||||
if (typeSel.value === 'modifier_objet_scene') toggleFlowObjectPropertyValue();
|
||||
if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action');
|
||||
updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value');
|
||||
updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value');
|
||||
updateDataValuePlaceholder('nf-action-score-operation', 'nf-action-score-value');
|
||||
}
|
||||
|
||||
// Fondations multi-éditeurs — équivalent de toggleFlowElementPropertyValue()
|
||||
// pour l'action "Modifier un objet de scène" (palette réduite : pas de
|
||||
// contenu/couleur/largeur/hauteur, juste visibilité/position en pixels/
|
||||
// orientation — voir screens/scenes/flow_palette.py OBJECT_ACTION_PROPERTIES).
|
||||
function toggleFlowObjectPropertyValue() {
|
||||
var propSel = document.getElementById('nf-action-object-property');
|
||||
if (!propSel) return;
|
||||
var visSel = document.getElementById('nf-action-object-value-visibility');
|
||||
var orientSel = document.getElementById('nf-action-object-value-orientation');
|
||||
var numInp = document.getElementById('nf-action-object-value-number');
|
||||
[visSel, orientSel, numInp].forEach(function(el) { if (el) el.style.display = 'none'; });
|
||||
if (propSel.value === 'visibilite' && visSel) {
|
||||
visSel.style.display = 'block';
|
||||
} else if (propSel.value === 'orientation' && orientSel) {
|
||||
orientSel.style.display = 'block';
|
||||
} else if (numInp) {
|
||||
numInp.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// "alea" (Phase 2 — hasard) attend "min,max" dans data_value (voir
|
||||
// screens/data_actions/compute_operation.py) plutôt qu'un seul nombre
|
||||
// comme les autres opérations — indice visuel dans le champ plutôt que
|
||||
// d'ajouter un champ dédié (aucun changement de schéma nécessaire).
|
||||
function updateDataValuePlaceholder(operationSelectId, valueInputId) {
|
||||
var opSel = document.getElementById(operationSelectId);
|
||||
var valueInp = document.getElementById(valueInputId);
|
||||
if (!opSel || !valueInp) return;
|
||||
valueInp.placeholder = opSel.value === 'alea' ? 'min,max (ex. 1,6)' : 'Valeur / montant';
|
||||
}
|
||||
|
||||
// Phase 8 — sélecteur d'animation à miniatures RÉELLES, scopé au
|
||||
// personnage réellement ciblé (ELEMENT_ANIMATIONS_MAP, injecté par
|
||||
// screen_edit.html à partir des propriétés de CET élément précis — voir
|
||||
// resolve_personnage_animations() côté serveur) — jamais un menu déroulant
|
||||
// ni un catalogue global. Mirror de onFlowDefinitionChange() (précédent le
|
||||
// plus proche pour "choisir A repeuple B").
|
||||
function onSpriteTargetElementChange() {
|
||||
var elSel = document.getElementById('nf-action-sprite-target-element');
|
||||
var grid = document.getElementById('nf-action-sprite-animation-grid');
|
||||
var nameField = document.getElementById('nf-action-sprite-animation-name');
|
||||
grid.innerHTML = '';
|
||||
nameField.value = '';
|
||||
var elementId = elSel.value;
|
||||
var animations = elementId ? (ELEMENT_ANIMATIONS_MAP[elementId] || {}) : {};
|
||||
Object.keys(animations).forEach(function(animName) {
|
||||
var frames = animations[animName];
|
||||
if (!frames.length) return;
|
||||
var tile = document.createElement('button');
|
||||
tile.type = 'button';
|
||||
tile.className = 'iconGalleryTile';
|
||||
tile.title = animName;
|
||||
tile.innerHTML = '<img src="' + frames[0] + '" alt="" class="personnageThumb">';
|
||||
tile.onclick = function() {
|
||||
grid.querySelectorAll('.iconGalleryTile').forEach(function(t) { t.classList.remove('selected'); });
|
||||
tile.classList.add('selected');
|
||||
nameField.value = animName;
|
||||
// Vitesse par défaut proportionnelle au nombre d'images (8 pour un
|
||||
// cycle Kenney à 8 images = ~1 cycle/seconde, mais un cycle CraftPix
|
||||
// à 30 images (personnages animaux) au même 8 i/s prenait ~3,75 s —
|
||||
// visiblement "très lent", signalé par l'utilisateur. Reste modifiable
|
||||
// à la main ensuite (ce n'est qu'une valeur de départ raisonnable).
|
||||
var fpsField = document.getElementById('nf-action-sprite-fps');
|
||||
if (fpsField) fpsField.value = Math.max(8, Math.min(24, frames.length));
|
||||
};
|
||||
grid.appendChild(tile);
|
||||
});
|
||||
}
|
||||
|
||||
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, orientationSel, colorInp, numInp, textInp].forEach(function(el){ el.style.display = 'none'; });
|
||||
if (propSel.value === 'visibilite') {
|
||||
visSel.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
} else if (propSel.value === 'surbrillance') {
|
||||
surbrillanceSel.style.display = 'block';
|
||||
toggleRow.style.display = 'none';
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
} else if (propSel.value === 'desactive') {
|
||||
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';
|
||||
document.getElementById('nf-action-element-toggle').checked = false;
|
||||
} else if (propSel.value === 'bg_color' || propSel.value === 'border_color') {
|
||||
colorInp.style.display = 'block';
|
||||
toggleRow.style.display = 'flex';
|
||||
} else {
|
||||
numInp.style.display = 'block';
|
||||
toggleRow.style.display = 'flex';
|
||||
}
|
||||
toggleFlowElementToggleMode();
|
||||
}
|
||||
|
||||
function toggleFlowElementToggleMode() {
|
||||
var propSel = document.getElementById('nf-action-element-property');
|
||||
var checked = document.getElementById('nf-action-element-toggle').checked;
|
||||
var wrap = document.getElementById('nf-action-element-value2-wrap');
|
||||
var colorInp2 = document.getElementById('nf-action-element-value2-color');
|
||||
var numInp2 = document.getElementById('nf-action-element-value2-number');
|
||||
var isColor = propSel.value === 'bg_color' || propSel.value === 'border_color';
|
||||
wrap.style.display = checked ? 'block' : 'none';
|
||||
colorInp2.style.display = (checked && isColor) ? 'block' : 'none';
|
||||
numInp2.style.display = (checked && !isColor) ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function submitNodeForm() {
|
||||
var payload = { node_type: currentNodeFormType, x: 20, y: 20 + currentBlockNodes().length * 10, block_id: CURRENT_BLOCK_ID };
|
||||
|
||||
if (currentNodeFormType === 'trigger') {
|
||||
payload.trigger_event = document.getElementById('nf-trigger-event').value;
|
||||
if (payload.trigger_event === 'evenement') {
|
||||
payload.trigger_custom_event_id = parseInt(document.getElementById('nf-trigger-custom-event').value, 10);
|
||||
} else if (payload.trigger_event === 'clavier' || payload.trigger_event === 'touche_maintenue' || payload.trigger_event === 'touche_relachee') {
|
||||
payload.trigger_key = document.getElementById('nf-trigger-key').value;
|
||||
} else if (payload.trigger_event === 'minuteur') {
|
||||
payload.trigger_interval_ms = parseInt(document.getElementById('nf-trigger-interval').value, 10);
|
||||
} else if (payload.trigger_event !== 'affichage') {
|
||||
payload[FLOW_TRIGGER_ID_FIELD] = parseInt(document.getElementById('nf-trigger-element').value, 10);
|
||||
}
|
||||
} else if (currentNodeFormType === 'condition') {
|
||||
var condSource = document.getElementById('nf-cond-source').value;
|
||||
payload.cond_source = condSource;
|
||||
if (condSource === 'collision') {
|
||||
// Phase 5 — ni opérateur ni valeur de comparaison pour une
|
||||
// collision (voir onFlowConditionSourceChange()) : juste les deux
|
||||
// éléments dont on compare les rectangles. Pas de clauses combinées
|
||||
// (ET/OU) pour une collision — même limitation assumée que pour les
|
||||
// autres sources tant qu'aucune clause supplémentaire n'a été ajoutée.
|
||||
payload.cond_element_a = parseInt(document.getElementById('nf-cond-element-a').value, 10);
|
||||
payload.cond_element_b = parseInt(document.getElementById('nf-cond-element-b').value, 10);
|
||||
} else {
|
||||
payload.cond_operator = document.getElementById('nf-cond-operator').value;
|
||||
payload.cond_value = document.getElementById('nf-cond-value').value;
|
||||
if (condSource === 'variable') {
|
||||
payload.cond_variable = document.getElementById('nf-cond-variable').value;
|
||||
payload.cond_variable_chemin = document.getElementById('nf-cond-variable-chemin').value;
|
||||
} else {
|
||||
var fieldSel = document.getElementById('nf-cond-field');
|
||||
var fieldOpt = fieldSel.options[fieldSel.selectedIndex];
|
||||
payload.cond_definition_id = parseInt(document.getElementById('nf-cond-definition').value, 10);
|
||||
payload.cond_row_id = parseInt(document.getElementById('nf-cond-row').value, 10) || null;
|
||||
payload.cond_field = fieldSel.value;
|
||||
payload.cond_field_type = fieldOpt ? fieldOpt.dataset.type : '';
|
||||
}
|
||||
}
|
||||
if (condSource !== 'collision' && flowConditionExtraClauses.length) {
|
||||
// 2.4 — clauses combinées : la clause "historique" ci-dessus devient
|
||||
// la première clause de la liste, suivie des clauses supplémentaires.
|
||||
var mainClause = condSource === 'variable'
|
||||
? { source: 'variable', variable: payload.cond_variable, variable_chemin: payload.cond_variable_chemin,
|
||||
operator: payload.cond_operator, value: payload.cond_value }
|
||||
: { source: 'objet', definition_id: payload.cond_definition_id, row_id: payload.cond_row_id,
|
||||
field: payload.cond_field, field_type: payload.cond_field_type,
|
||||
operator: payload.cond_operator, value: payload.cond_value };
|
||||
var allClauses = [mainClause].concat(flowConditionExtraClauses.map(function(c) {
|
||||
return c.source === 'variable'
|
||||
? { source: 'variable', variable: c.variable, variable_chemin: c.variable_chemin, operator: c.operator, value: c.value }
|
||||
: { source: 'objet', definition_id: parseInt(c.definition_id, 10), row_id: parseInt(c.row_id, 10) || null,
|
||||
field: c.field, field_type: c.field_type, operator: c.operator, value: c.value };
|
||||
}));
|
||||
payload.cond_clauses = JSON.stringify(allClauses);
|
||||
payload.cond_combinator = document.getElementById('nf-cond-combinator').value;
|
||||
}
|
||||
} else if (currentNodeFormType === 'action') {
|
||||
var actionType = document.getElementById('nf-action-type').value;
|
||||
payload.action_type = actionType;
|
||||
if (actionType === 'aller_a' || actionType === 'ouvrir_ligne') {
|
||||
payload.target_screen_id = parseInt(document.getElementById('nf-action-target-screen').value, 10);
|
||||
} else if (actionType === 'activer_onglet') {
|
||||
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
|
||||
} else if (actionType === 'modifier_element') {
|
||||
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
|
||||
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' && 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 {
|
||||
var v1 = isColorProp
|
||||
? document.getElementById('nf-action-element-value-color').value
|
||||
: document.getElementById('nf-action-element-value-number').value;
|
||||
if (toggleOn) {
|
||||
var v2 = isColorProp
|
||||
? document.getElementById('nf-action-element-value2-color').value
|
||||
: document.getElementById('nf-action-element-value2-number').value;
|
||||
payload.element_value = v1 + '||' + v2;
|
||||
} else {
|
||||
payload.element_value = v1;
|
||||
}
|
||||
}
|
||||
} else if (actionType === 'modifier_objet_scene') {
|
||||
// Fondations multi-éditeurs — équivalent scène de "modifier_element"
|
||||
// ci-dessus, palette réduite (voir OBJECT_ACTION_PROPERTIES) : pas
|
||||
// de va-et-vient/couleur, juste visibilité/position en pixels/
|
||||
// orientation. FLOW_TARGET_ID_FIELD vaut "target_object_id" ici
|
||||
// (voir tête de fichier).
|
||||
payload[FLOW_TARGET_ID_FIELD] = parseInt(document.getElementById('nf-action-target-object').value, 10);
|
||||
var objProp = document.getElementById('nf-action-object-property').value;
|
||||
payload.element_property = objProp;
|
||||
if (objProp === 'visibilite') {
|
||||
payload.element_value = document.getElementById('nf-action-object-value-visibility').value;
|
||||
} else if (objProp === 'orientation') {
|
||||
payload.element_value = document.getElementById('nf-action-object-value-orientation').value;
|
||||
} else {
|
||||
payload.element_value = document.getElementById('nf-action-object-value-number').value;
|
||||
}
|
||||
} else if (actionType === 'modifier_donnee') {
|
||||
payload.target_definition_id = parseInt(document.getElementById('nf-action-data-definition').value, 10);
|
||||
payload.target_row_id = parseInt(document.getElementById('nf-action-data-row').value, 10) || null;
|
||||
payload.target_field = document.getElementById('nf-action-data-field').value;
|
||||
payload.data_operation = document.getElementById('nf-action-data-operation').value;
|
||||
payload.data_value = document.getElementById('nf-action-data-value').value;
|
||||
} else if (actionType === 'modifier_variable') {
|
||||
payload.target_variable = document.getElementById('nf-action-variable-name').value;
|
||||
payload.data_operation = document.getElementById('nf-action-variable-operation').value;
|
||||
payload.data_value = document.getElementById('nf-action-variable-value').value;
|
||||
} else if (actionType === 'modifier_score') {
|
||||
// Score/progression (voir db/scoring/) : un seul score par joueur,
|
||||
// pas de nom à transmettre (contrairement à "modifier_variable").
|
||||
payload.data_operation = document.getElementById('nf-action-score-operation').value;
|
||||
payload.data_value = document.getElementById('nf-action-score-value').value;
|
||||
} else if (actionType === 'definir_statut_partie') {
|
||||
payload.data_value = document.getElementById('nf-action-status-value').value;
|
||||
} else if (actionType === 'attendre') {
|
||||
payload.data_value = document.getElementById('nf-action-wait-seconds').value;
|
||||
} else if (actionType === 'declencher_evenement') {
|
||||
// Notification pure : aucun élément/ligne à transmettre (voir
|
||||
// screens/custom_events/) — c'est à l'écouteur de décider quoi
|
||||
// faire, avec ses propres réglages habituels.
|
||||
payload.target_custom_event_id = parseInt(document.getElementById('nf-action-custom-event').value, 10);
|
||||
} else if (actionType === 'ajouter_ligne') {
|
||||
// Phase 4 — crée une ligne VIDE ; ses champs se renseignent via un ou
|
||||
// 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;
|
||||
} else if (actionType === 'jouer_animation_sprite') {
|
||||
// Phase 8 — stocke le NOM de l'animation choisie (miniature cliquée,
|
||||
// voir onSpriteTargetElementChange()), PAS ses frames : résolues à
|
||||
// l'exécution depuis le personnage ACTUELLEMENT assigné à la cible
|
||||
// (voir resolveSpriteFrames() dans static/js/play/actions.js) —
|
||||
// sinon changer le personnage Forge de l'élément dans l'éditeur
|
||||
// n'avait aucun effet sur les animations déjà posées dans la
|
||||
// logique, qui continuaient à jouer les frames de l'ancien
|
||||
// personnage indéfiniment (bug signalé par l'utilisateur).
|
||||
var elementId = document.getElementById('nf-action-sprite-target-element').value;
|
||||
payload[FLOW_TARGET_ID_FIELD] = parseInt(elementId, 10);
|
||||
payload.data_value = JSON.stringify({
|
||||
animation: document.getElementById('nf-action-sprite-animation-name').value,
|
||||
fps: parseInt(document.getElementById('nf-action-sprite-fps').value, 10) || 8,
|
||||
loop: document.getElementById('nf-action-sprite-loop').checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetch(flowUrl('/screens/' + FLOW_SCREEN_ID + '/flow/nodes/add'), {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)
|
||||
}).then(function(r){ return r.json(); }).then(function(node){
|
||||
ALL_FLOW.nodes.push(node);
|
||||
closeNodeForm();
|
||||
renderFlow();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,10 +36,10 @@ function initBuilderPanel() {
|
||||
.then(function (json) {
|
||||
if (json.url) {
|
||||
targetField.value = json.url;
|
||||
status.textContent = 'Fichier envoyé ✓ Enregistrement...';
|
||||
status.textContent = 'Fichier envoyé — Enregistrement...';
|
||||
// Enregistre tout de suite, comme n'importe quel autre réglage —
|
||||
// un fichier envoyé ne doit jamais rester "en attente".
|
||||
submitPropsForm().then(function () { status.textContent = 'Fichier envoyé ✓ Enregistré.'; });
|
||||
submitPropsForm().then(function () { status.textContent = 'Fichier envoyé — Enregistré.'; });
|
||||
}
|
||||
else { status.textContent = "Échec de l'envoi."; }
|
||||
})
|
||||
@@ -63,7 +63,7 @@ function initBuilderPanel() {
|
||||
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é ✓'; }
|
||||
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."; });
|
||||
@@ -442,8 +442,8 @@ function bindScreenBackgroundMusicField() {
|
||||
.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é.'; });
|
||||
status.textContent = 'Fichier envoyé — Enregistrement...';
|
||||
saveScreenBackgroundMusic(json.url).then(function () { status.textContent = 'Fichier envoyé — Enregistré.'; });
|
||||
}
|
||||
else { status.textContent = "Échec de l'envoi."; }
|
||||
})
|
||||
@@ -483,7 +483,7 @@ document.querySelectorAll('.uploadInput').forEach(function(input){
|
||||
.then(function(json){
|
||||
if (json.url) {
|
||||
targetField.value = json.url;
|
||||
status.textContent = 'Fichier envoyé ✓ Enregistrement...';
|
||||
status.textContent = 'Fichier envoyé — Enregistrement...';
|
||||
// Enregistre tout de suite (comme un clic sur "Enregistrer") pour
|
||||
// qu'un fichier envoyé ne reste jamais "en attente" si on oublie
|
||||
// de cliquer sur le bouton ensuite — l'image doit apparaître dès
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
// switchBuilderTab()/toggleDashCreate() ont déménagé dans builder-tabs.js
|
||||
// (générique, chargé par les DEUX éditeurs — voir ce fichier) : tout ce
|
||||
// qui reste ici est propre aux blocs de logique/panneaux flottants,
|
||||
// jamais chargé par l'éditeur de scène 2D (scene_edit.html).
|
||||
|
||||
function toggleBlockEditRow(blockId) {
|
||||
document.getElementById('blockEditRow' + blockId).classList.toggle('hidden');
|
||||
}
|
||||
|
||||
// ---------- Panneaux flottants : glisser-déposer + redimensionnement +
|
||||
// plein écran ---------- Générique, porté tel quel depuis game_dashboard.html
|
||||
// (mêmes gestes partout dans l'outil plutôt qu'une variante par panneau).
|
||||
function makeFloatPanelDraggable(panel, header) {
|
||||
var dragging = false, offsetX = 0, offsetY = 0;
|
||||
header.addEventListener('mousedown', function (e) {
|
||||
if (e.target.closest('button')) return;
|
||||
dragging = true;
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
|
||||
var rect = panel.getBoundingClientRect();
|
||||
panel.style.left = rect.left + 'px';
|
||||
panel.style.top = rect.top + 'px';
|
||||
panel.style.right = 'auto';
|
||||
panel.style.bottom = 'auto';
|
||||
panel.style.transform = 'none';
|
||||
offsetX = e.clientX - rect.left;
|
||||
offsetY = e.clientY - rect.top;
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!dragging) return;
|
||||
panel.style.left = (e.clientX - offsetX) + 'px';
|
||||
panel.style.top = (e.clientY - offsetY) + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function () {
|
||||
dragging = false;
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
}
|
||||
function makeFloatPanelResizable(panel, handle) {
|
||||
var resizing = false, startX = 0, startY = 0, startW = 0, startH = 0;
|
||||
handle.addEventListener('mousedown', function (e) {
|
||||
resizing = true;
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) panel._fsToggle.exit();
|
||||
var rect = panel.getBoundingClientRect();
|
||||
startX = e.clientX; startY = e.clientY;
|
||||
startW = rect.width; startH = rect.height;
|
||||
panel.style.width = startW + 'px';
|
||||
panel.style.maxWidth = 'none';
|
||||
document.body.style.userSelect = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
document.addEventListener('mousemove', function (e) {
|
||||
if (!resizing) return;
|
||||
panel.style.width = Math.max(320, startW + (e.clientX - startX)) + 'px';
|
||||
panel.style.height = Math.max(200, startH + (e.clientY - startY)) + 'px';
|
||||
});
|
||||
document.addEventListener('mouseup', function () {
|
||||
resizing = false;
|
||||
document.body.style.userSelect = '';
|
||||
});
|
||||
}
|
||||
function makeFloatPanelFullscreenable(panel, toggleBtn) {
|
||||
var saved = null;
|
||||
function enter() {
|
||||
saved = {
|
||||
left: panel.style.left, top: panel.style.top, right: panel.style.right,
|
||||
width: panel.style.width, height: panel.style.height,
|
||||
maxWidth: panel.style.maxWidth, transform: panel.style.transform,
|
||||
};
|
||||
panel.classList.add('floatPanel--fullscreen');
|
||||
panel.style.left = '12px'; panel.style.top = '12px'; panel.style.right = '12px'; panel.style.bottom = '12px';
|
||||
panel.style.width = 'auto'; panel.style.height = 'auto'; panel.style.maxWidth = 'none'; panel.style.transform = 'none';
|
||||
toggleBtn.title = 'Quitter le plein écran';
|
||||
}
|
||||
function exit() {
|
||||
panel.classList.remove('floatPanel--fullscreen');
|
||||
panel.style.bottom = '';
|
||||
if (saved) {
|
||||
panel.style.left = saved.left; panel.style.top = saved.top; panel.style.right = saved.right;
|
||||
panel.style.width = saved.width; panel.style.height = saved.height;
|
||||
panel.style.maxWidth = saved.maxWidth; panel.style.transform = saved.transform;
|
||||
}
|
||||
toggleBtn.title = 'Plein écran';
|
||||
}
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
if (panel.classList.contains('floatPanel--fullscreen')) exit(); else enter();
|
||||
});
|
||||
panel._fsToggle = { enter: enter, exit: exit };
|
||||
return panel._fsToggle;
|
||||
}
|
||||
|
||||
// ---------- Modale d'un bloc de logique (voir screens/flow/blocks/) : un
|
||||
// seul bloc ouvert à la fois, réutilise le même #nodeForm/#flowCanvas pour
|
||||
// n'importe quel bloc — voir currentBlockNodes()/currentBlockEdges(). ----------
|
||||
(function () {
|
||||
var panel = document.getElementById('logicBlockPanel');
|
||||
makeFloatPanelDraggable(panel, document.getElementById('logicBlockPanelHeader'));
|
||||
makeFloatPanelResizable(panel, document.getElementById('logicBlockPanelResizeHandle'));
|
||||
makeFloatPanelFullscreenable(panel, panel.querySelector('.floatPanelFullscreenBtn'));
|
||||
window.openLogicBlockPanel = function (blockId) {
|
||||
CURRENT_BLOCK_ID = blockId;
|
||||
closeNodeForm();
|
||||
var block = FLOW_BLOCKS.find(function (b) { return b.id === blockId; });
|
||||
document.getElementById('logicBlockPanelTitle').textContent = (block && block.name) || '?';
|
||||
panel.classList.remove('hidden');
|
||||
panel._fsToggle.enter();
|
||||
renderFlow();
|
||||
requestAnimationFrame(drawFlowEdges);
|
||||
};
|
||||
window.closeLogicBlockPanel = function () {
|
||||
panel.classList.add('hidden');
|
||||
CURRENT_BLOCK_ID = null;
|
||||
};
|
||||
})();
|
||||
|
||||
// L'ouverture initiale ?tab=/?block=... (données Jinja : request.args)
|
||||
// reste dans le petit <script> inline en fin de screen_edit.html, après
|
||||
// ce fichier — voir le commentaire là-bas.
|
||||
|
||||
@@ -249,7 +249,7 @@ function handleDeleteElementSubmit(event, form) {
|
||||
if (!data.flow_node_count) return;
|
||||
var n = data.flow_node_count;
|
||||
var plural = n > 1 ? 's' : '';
|
||||
warnEl.textContent = '⚠️ ' + n + ' connexion' + plural + ' de la Logique de la scène (déclencheur/action) utilise' +
|
||||
warnEl.textContent = n + ' connexion' + plural + ' de la Logique de la scène (déclencheur/action) utilise' +
|
||||
(n > 1 ? 'nt' : '') + ' cet élément et sera' + (n > 1 ? 'ont' : '') + ' aussi supprimée' + plural + '.';
|
||||
warnEl.style.display = 'block';
|
||||
})
|
||||
@@ -533,7 +533,11 @@ function refreshAfterReorder() {
|
||||
// le bouton "précédent" du navigateur continue à fonctionner normalement.
|
||||
|
||||
function refreshBuilderPanel(selectedId) {
|
||||
var url = SCREEN_EDIT_URL + "?selected=" + selectedId;
|
||||
// selectedId falsy (0/null/undefined) : referme le panneau de propriétés
|
||||
// plutôt que de garder une sélection (ex. après suppression de l'objet
|
||||
// sélectionné, voir deleteSceneObject(), static/js/scenes/scene-editor.js)
|
||||
// — jamais "?selected=null/undefined" dans l'URL.
|
||||
var url = SCREEN_EDIT_URL + (selectedId ? ("?selected=" + selectedId) : "");
|
||||
return fetch(url, { headers: { "X-Requested-With": "pjax" } })
|
||||
.then(function (r) { return r.text().then(function (html) { return { html: html, url: r.url || url }; }); })
|
||||
.then(function (result) {
|
||||
@@ -543,9 +547,15 @@ function refreshBuilderPanel(selectedId) {
|
||||
if (newBuilder && curBuilder) {
|
||||
curBuilder.innerHTML = newBuilder.innerHTML;
|
||||
}
|
||||
CURRENT_SELECTED_ID = selectedId;
|
||||
CURRENT_SELECTED_ID = selectedId || null;
|
||||
history.pushState({ pjax: true }, "", result.url);
|
||||
initBuilderPanel();
|
||||
// Éditeur de scène 2D (voir static/js/scenes/scene-editor.js) : ce
|
||||
// fichier générique (partagé avec l'éditeur document) ne connaît pas
|
||||
// ses réglages propres (position en pixels, panneau "Commandes",
|
||||
// aperçu animé des personnages) — rien à faire ici pour l'éditeur
|
||||
// document, où cette fonction n'existe simplement pas.
|
||||
if (typeof initSceneBuilderPanel === "function") initSceneBuilderPanel();
|
||||
})
|
||||
.catch(function () {
|
||||
// Échec réseau : on retombe sur une vraie navigation plutôt que de
|
||||
@@ -683,7 +693,7 @@ function flashAutosave(state) {
|
||||
autosaveIndicator.textContent = 'Enregistrement...';
|
||||
autosaveIndicator.classList.add('saving');
|
||||
} else {
|
||||
autosaveIndicator.textContent = 'Enregistré automatiquement ✓';
|
||||
autosaveIndicator.textContent = 'Enregistré automatiquement';
|
||||
autosaveIndicator.classList.add('saved');
|
||||
}
|
||||
}
|
||||
|
||||
+119
-13
@@ -219,10 +219,21 @@ input[type=checkbox]{ width:auto; margin-top:6px; }
|
||||
les différents éditeurs d'un même jeu, en ligne (contrairement à
|
||||
.navList/.navRow ci-dessous, empilés verticalement dans le panneau
|
||||
"Créer" du tableau de bord). */
|
||||
.gameNavBar{ display:flex; flex-wrap:wrap; justify-content:center; gap:6px; padding:8px 20px; background:transparent; border-bottom:1px solid var(--border); }
|
||||
/* space-between (jamais center, voir le commentaire d'en-tête plus haut) :
|
||||
"Mes écrans" (page de gestion) reste à gauche, "Jouer"/"Exporter"
|
||||
(.gameNavRight, des ACTIONS) groupés à droite — demande explicite. */
|
||||
.gameNavBar{ display:flex; flex-wrap:wrap; justify-content:space-between; align-items:center; gap:6px; padding:8px 20px; background:transparent; border-bottom:1px solid var(--border); }
|
||||
.gameNavRight{ display:flex; flex-wrap:wrap; gap:6px; margin-left:auto; }
|
||||
.gameNavLink{ display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border:1px solid var(--border); border-radius:6px; background:var(--panel); text-decoration:none; color:var(--text); font-size:12.5px; }
|
||||
.gameNavLink:hover{ background:var(--panel2); border-color:var(--accent); }
|
||||
.gameNavLink.active{ background:var(--panel2); border-color:var(--accent); color:var(--accent); }
|
||||
/* "Jouer" (orange, --accent) / "Exporter" (vert, --status-success) —
|
||||
couleur pleine plutôt qu'un simple contour, pour ressortir comme des
|
||||
actions parmi les liens de gestion neutres. */
|
||||
.gameNavLink--play{ background:var(--accent); border-color:var(--accent); color:#fff; font-weight:600; }
|
||||
.gameNavLink--play:hover{ background:var(--accent); filter:brightness(1.1); border-color:var(--accent); }
|
||||
.gameNavLink--export{ background:var(--status-success); border-color:var(--status-success); color:#fff; font-weight:600; }
|
||||
.gameNavLink--export:hover{ background:var(--status-success); filter:brightness(1.1); border-color:var(--status-success); }
|
||||
.content{ max-width:760px; margin:0 auto; padding:28px 20px; }
|
||||
|
||||
.card{ border:1px solid var(--border); border-radius:10px; padding:16px; background:var(--panel); margin:12px 0; }
|
||||
@@ -246,14 +257,26 @@ input[type=checkbox]{ width:auto; margin-top:6px; }
|
||||
.onboardingStep{ flex:0 0 50%; width:50%; box-sizing:border-box; padding:12px 24px; }
|
||||
|
||||
/* ---------- Étape 1 : cartes de type, grandes, retournables ---------- */
|
||||
.onboardingGrid{ display:flex; gap:18px; height:66vh; min-height:420px; max-height:640px; }
|
||||
/* Un seul choix aujourd'hui (RPG, voir db/games/game_type_catalog.py) :
|
||||
pleine hauteur (comme avant), mais LARGEUR d'1/4 (comme s'il y avait
|
||||
toujours 4 cartes côte à côte) et centrée — demande explicite, plutôt
|
||||
que de repenser toute la mise en page pour un seul choix. */
|
||||
.onboardingGrid{ display:flex; gap:18px; height:66vh; min-height:420px; max-height:640px; max-width:25%; margin:0 auto; transition:max-width .35s ease; }
|
||||
/* Élargit la carte (jamais un scale() : demande explicite, "s'élargir sur
|
||||
sa width", pas grossir dans les deux sens) au survol/à la sélection —
|
||||
pour lire confortablement le dos retourné (titre + liste + badge, voir
|
||||
.onboardingPitch*) sans reprendre toute la mise en page à 4 cartes. Le
|
||||
sélecteur :has() cible le PARENT depuis l'état d'un enfant (support
|
||||
navigateurs modernes déjà requis ailleurs, voir container queries
|
||||
"cqw" plus bas). */
|
||||
.onboardingGrid:has(.onboardingCard:hover), .onboardingGrid:has(.onboardingCard.is-selected){ max-width:42%; }
|
||||
.onboardingCard{
|
||||
flex:1; position:relative; cursor:pointer; padding:0; margin:0; border:none; background:none;
|
||||
border-radius:20px; perspective:1400px;
|
||||
opacity:0; animation:forgeOnboardCardIn .5s ease both;
|
||||
transition:flex-grow .3s ease;
|
||||
}
|
||||
.onboardingCard:hover{ flex-grow:1.25; }
|
||||
.onboardingCard:hover{ flex-grow:1.25; z-index:1; }
|
||||
.onboardingCardInner{
|
||||
position:relative; display:block; width:100%; height:100%;
|
||||
transform-style:preserve-3d; transition:transform .6s cubic-bezier(.4,.2,.2,1);
|
||||
@@ -269,9 +292,30 @@ input[type=checkbox]{ width:auto; margin-top:6px; }
|
||||
.onboardingCardIcon{ font-size:3.4em; line-height:1; filter:drop-shadow(0 4px 10px rgba(0,0,0,.35)); }
|
||||
.onboardingCardLabel{ font-weight:800; font-size:1.3em; letter-spacing:.01em; }
|
||||
.onboardingCardBack{
|
||||
transform:rotateY(180deg); background:var(--panel); border:2px solid var(--accent);
|
||||
transform:rotateY(180deg); background:linear-gradient(165deg, var(--panel), var(--panel2));
|
||||
border:2px solid var(--accent); justify-content:flex-start; padding:26px 22px;
|
||||
}
|
||||
/* Argumentaire de vente (voir db/games/game_type_catalog.py::ONBOARDING_TYPES,
|
||||
"description" contient du HTML de confiance — jamais de saisie
|
||||
utilisateur — d'où |safe dans le template) : un titre accrocheur, une
|
||||
liste à coche plutôt qu'un bloc de texte brut, un badge LMS mis en
|
||||
avant en bas — pensé pour convaincre un public RH non technique en un
|
||||
coup d'œil, pas juste informer. */
|
||||
.onboardingCardDesc{ display:flex; flex-direction:column; gap:16px; width:100%; text-align:left; }
|
||||
.onboardingPitchTitle{ font-size:1.15em; font-weight:800; line-height:1.3; color:var(--text); }
|
||||
.onboardingPitchList{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px; }
|
||||
.onboardingPitchList li{ display:flex; align-items:flex-start; gap:10px; font-size:.93em; line-height:1.45; color:var(--text-dim); }
|
||||
.onboardingPitchCheck{
|
||||
flex:0 0 auto; width:20px; height:20px; border-radius:50%; background:var(--gradient);
|
||||
color:#fff; display:flex; align-items:center; justify-content:center; font-size:.7em; font-weight:900;
|
||||
box-shadow:0 2px 8px var(--glow); margin-top:1px;
|
||||
}
|
||||
.onboardingPitchList strong{ color:var(--text); }
|
||||
.onboardingPitchBadge{
|
||||
display:flex; align-items:center; gap:8px; font-size:.85em; font-weight:700;
|
||||
color:var(--accent); background:rgba(255,95,46,.12); border:1px solid rgba(255,95,46,.35);
|
||||
border-radius:999px; padding:8px 14px; align-self:flex-start;
|
||||
}
|
||||
.onboardingCardDesc{ color:var(--text); font-size:1em; line-height:1.5; }
|
||||
|
||||
/* Une couleur de dégradé par type — voir data-type dans le template,
|
||||
généré depuis db.ONBOARDING_TYPES (mêmes clés que ci-dessous). */
|
||||
@@ -513,10 +557,14 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
}
|
||||
.floatPanelHeader .floatPanelTitle{ font-size:11px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.05em; }
|
||||
.floatPanelBody{ padding:12px; overflow-y:auto; min-height:0; flex:1 1 auto; }
|
||||
.floatPanelReopenBtn{ position:fixed; top:90px; z-index:60; display:none; }
|
||||
/* Posés dans la barre d'outils de la scène (templates/scene_edit.html),
|
||||
plus en position:fixed flottant au-dessus de toute la page — ils
|
||||
chevauchaient "Mes écrans"/"Jouer"/"Exporter" (voir base.html) une fois
|
||||
les panneaux fermés. display:none/.visible (JS : openFloatPanel/
|
||||
closeFloatPanel, static/js/screen_edit/tree-panels.js) reste la même
|
||||
logique : visible seulement quand le panneau correspondant est fermé. */
|
||||
.floatPanelReopenBtn{ display:none; }
|
||||
.floatPanelReopenBtn.visible{ display:inline-flex; }
|
||||
#panelLeftReopenBtn{ left:24px; }
|
||||
#propsPanelReopenBtn{ right:24px; }
|
||||
|
||||
.widgetGrid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
|
||||
|
||||
@@ -615,6 +663,29 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
.canvasElement:hover{ border-color:rgba(91,140,255,.5); }
|
||||
.canvasElement.selected{ border:2px solid var(--accent); }
|
||||
.canvasElementInner{ width:100%; height:100%; pointer-events:none; }
|
||||
/* render_scene_object.py positionne son <img> en absolu (left/top/width/
|
||||
height en px) pour s'afficher correctement en enfant DIRECT de
|
||||
.playScreen.playScene (voir templates/play.html) — dans le canevas de
|
||||
l'éditeur de scène (templates/scene_edit.html), cette même balise est
|
||||
nichée dans .canvasElementInner, lui-même déjà positionné/dimensionné
|
||||
par .canvasElement ci-dessus : sans ce correctif, l'image se
|
||||
positionnait EN PLUS depuis #canvas (son ancêtre positionné le plus
|
||||
proche), en double du décalage déjà appliqué par .canvasElement.
|
||||
Posé ici (feuille de style globale, chargée par TOUTES les pages) et
|
||||
non plus dans un <style> inline propre à scene_edit.html : la
|
||||
navigation "Mes écrans" -> scène se fait en pjax (voir static/pjax.js),
|
||||
qui ne fusionne dans <head> que les <link rel="stylesheet"> manquants,
|
||||
JAMAIS un <style> inline — arriver sur une scène par un clic (plutôt
|
||||
qu'un vrai chargement de page) laissait donc cette règle absente, et
|
||||
les personnages/décors se retrouvaient mal positionnés (invisibles)
|
||||
tant qu'aucun rechargement complet n'avait eu lieu. */
|
||||
.canvasElementInner > img{ position:static !important; left:auto !important; top:auto !important; width:100% !important; height:100% !important; z-index:auto !important; }
|
||||
/* Même correctif que ci-dessus, pour les widgets d'interface ("💬 Boîte
|
||||
de dialogue"/"❓ Boîte à quiz"/"🏆 Score", screens/scenes/
|
||||
render_scene_object.py) — des <div>, pas des <img>. */
|
||||
.canvasElementInner > .dialogueBoxWidget, .canvasElementInner > .quizBoxWidget, .canvasElementInner > .scoreWidget{
|
||||
position:static !important; left:auto !important; top:auto !important; width:100% !important; height:100% !important; z-index:auto !important;
|
||||
}
|
||||
/* Objet de scène (screens/scenes/render_scene_object.py, éditeur ET jeu
|
||||
jouable) : object-fit:contain — les frames CraftPix (screens/labels/
|
||||
animal_sprite_library.py) sont de grands canevas très paddés, très
|
||||
@@ -808,19 +879,31 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
border:1px solid var(--border); border-radius:12px; padding:8px 10px;
|
||||
}
|
||||
.collisionRuleChainLinks{ display:flex; align-items:center; flex-wrap:wrap; row-gap:6px; }
|
||||
/* Pointe/encoche en PIXELS FIXES (calc(100% - 14px), jamais un pourcentage
|
||||
de la largeur du maillon) — un maillon plus court ("PNJ") ou plus long
|
||||
("Déclencher une quête « ... »") a alors TOUJOURS la même profondeur de
|
||||
pointe, qui s'emboîte exactement dans l'encoche du suivant (même 14px
|
||||
que la marge négative ci-dessous) : avec un pourcentage, la profondeur
|
||||
de la pointe variait avec la largeur du texte, sans jamais coïncider
|
||||
avec l'encoche du voisin — les pièces flottaient les unes par rapport
|
||||
aux autres au lieu de s'emboîter. */
|
||||
.collisionRuleLink{
|
||||
--link-color:var(--accent);
|
||||
position:relative; display:inline-flex; align-items:center; gap:4px;
|
||||
background:linear-gradient(135deg, var(--link-color), color-mix(in srgb, var(--link-color) 70%, #000 30%));
|
||||
color:#fff; padding:7px 16px 7px 20px; font-size:12.5px; font-weight:600; white-space:nowrap;
|
||||
clip-path:polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%, 10% 50%);
|
||||
margin-left:-12px; z-index:1; animation:collisionLinkIn .3s ease backwards;
|
||||
clip-path:polygon(0 0, calc(100% - 14px) 0, 100% 50%, calc(100% - 14px) 100%, 0 100%, 14px 50%);
|
||||
margin-left:-14px; z-index:1; animation:collisionLinkIn .3s ease backwards;
|
||||
transition:transform .15s ease, filter .15s ease;
|
||||
}
|
||||
.collisionRuleLink:first-child{ margin-left:0; clip-path:polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%); padding-left:14px; border-radius:8px 0 0 8px; }
|
||||
.collisionRuleLink:first-child{ margin-left:0; clip-path:polygon(0 0, calc(100% - 14px) 0, 100% 50%, calc(100% - 14px) 100%, 0 100%); padding-left:14px; border-radius:8px 0 0 8px; }
|
||||
.collisionRuleLink:hover{ transform:translateY(-2px) scale(1.04); filter:brightness(1.12); z-index:2; }
|
||||
.collisionRuleLink[data-kind="trigger"]{ --link-color:#f2994a; }
|
||||
.collisionRuleLink[data-kind="quete"]{ --link-color:#9b59b6; }
|
||||
/* "quete" est le seul type de maillon cliquable (voir collision-rules-
|
||||
editor.js — un onclick posé directement sur le <span>, jamais un
|
||||
<button> : son style natif grignotait la pointe en clip-path même
|
||||
après reset), d'où le curseur en plus ici. */
|
||||
.collisionRuleLink[data-kind="quete"]{ --link-color:#9b59b6; cursor:pointer; }
|
||||
.collisionRuleLink[data-kind="attaque"]{ --link-color:#e0524d; }
|
||||
.collisionRuleLink[data-kind="evenement"]{ --link-color:#17a2b8; }
|
||||
.collisionRuleLink[data-kind="interagir"]{ --link-color:#d4a72c; }
|
||||
@@ -884,6 +967,13 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
(distincte de la modale de fiche ci-dessus) : couvre tout le viewport
|
||||
plutôt qu'une boîte centrée, pour laisser toute la place aux 3
|
||||
colonnes de répliques. */
|
||||
/* z-index:61 (au-dessus de .floatPanel, à 60, voir plus haut) : sans ça,
|
||||
les panneaux flottants "Objets"/"Propriétés" restaient peints par-
|
||||
dessus cette modale PLEIN ÉCRAN. Les boutons "Objets"/"Propriétés"
|
||||
eux-mêmes (.floatPanelReopenBtn) vivent maintenant dans la barre
|
||||
d'outils de la scène, plus en position:fixed — ce n'est donc plus qu'un
|
||||
souci entre CETTE modale et les panneaux flottants encore ouverts. */
|
||||
.questDialogueModal{ z-index:61; }
|
||||
.questDialogueModal .modal-content{ width:100vw; height:100vh; max-width:100vw; max-height:100vh; margin:0; }
|
||||
/* La modale entière est une colonne flex (jamais un scroll global, voir
|
||||
overflow:hidden ici) : le header/la fiche restent fixes en haut, les 3
|
||||
@@ -1097,7 +1187,12 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
|
||||
.builderTabBtn:hover{ color:var(--text); }
|
||||
.builderTabBtn.active{ color:var(--accent); border-bottom-color:var(--accent); font-weight:600; }
|
||||
|
||||
.builderTabPanel{ flex:1 1 auto; min-height:0; display:none; flex-direction:column; }
|
||||
/* overflow-y:auto : contrairement à "screen" (voir .canvasFrame plus haut,
|
||||
qui défile LUI-MÊME), "collision-rules"/"quests"/"events" n'ont pas de
|
||||
sous-conteneur dédié au défilement — sans ça, une liste plus haute que
|
||||
l'espace disponible restait simplement coupée par .builderCanvasArea
|
||||
(overflow:hidden), au lieu de défiler. */
|
||||
.builderTabPanel{ flex:1 1 auto; min-height:0; display:none; flex-direction:column; overflow-y:auto; }
|
||||
.builderTabPanel.active{ display:flex; }
|
||||
|
||||
/* Sous-onglets Champs/Données À L'INTÉRIEUR d'un panneau "Modifier un
|
||||
@@ -1223,6 +1318,17 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
|
||||
position:absolute; top:0; left:0; transform-origin:top left;
|
||||
transform:scale(calc(100cqw / var(--scene-w)));
|
||||
}
|
||||
/* obj.rendered_html (screens/scenes/render_scene_object.py) porte un
|
||||
z-index PENSÉ POUR L'ÉDITEUR DE SCÈNE (souvent très négatif — un "fond"
|
||||
à -1000, un personnage juste au-dessus — pour rester sous les cadres/
|
||||
poignées de sélection de CET éditeur, voir templates/scene_edit.html) :
|
||||
réutilisé tel quel ici, ce z-index très négatif faisait passer l'objet
|
||||
SOUS le fond de la carte (.screenPreviewBox, background:#0b0d12),
|
||||
invisible. L'ordre d'empilement voulu ici (fond, puis personnages/décor
|
||||
par-dessus) est déjà garanti par l'ORDRE dans lequel screen_preview()
|
||||
(game_dashboard_simple.html) les liste — z-index:auto (l'ordre du DOM)
|
||||
suffit donc, sans dépendre de leurs valeurs numériques d'origine. */
|
||||
.screenPreviewScene > *{ z-index:auto !important; }
|
||||
.screenPreviewEmpty{
|
||||
position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
|
||||
color:var(--forge-text-muted, #8b95a7); font-size:.85em;
|
||||
|
||||
Reference in New Issue
Block a user