Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y

Config strictement stricte partout (ruff, mypy --strict, bandit, vulture,
import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas
casser le build" - l'existant a ete corrige pour la satisfaire plutot que
l'inverse. Hooks pre-commit locaux (language: system) bloquants.

- Typage mypy --strict propage a tout le moteur (db, screens, auth, core,
  ai, routes, puis publish/scripts/tests/app.py/build_css.py).
- Securite : fuite de handle fichier Windows corrigee dans l'export SCORM
  (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes
  (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe
  "</script>" dans le JSON embarque en <script>, 25 sites).
- Architecture : imports circulaires/F811 nettoyes, contrats
  import-linter respectes, code mort retire (vulture).
- Accessibilite : 69 champs de formulaire sans label correctement
  associe corriges (for/id ou aria-label) sur 11 templates.
- ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis
  appliquees (aucune desactivee sans verification individuelle).
- Tests : isolation du compte admin partage (nettoyage ponctuel +
  fixture de teardown automatique en filet de securite), suite complete
  verte (591 tests Python, 241 tests JS).
- SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport
  complet analyse point par point, faux positifs documentes.
- .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine
  faisait echouer ESLint (linebreak-style) via un bug connu de git
  (checkout "en place" qui ignore l'eol force sur un fichier deja
  present sur disque - contourne en supprimant puis recreant chaque
  fichier suivi).

djLint (H021, styles inline) volontairement saute pour ce commit -
backlog assume, deja documente, traite dans un lot separe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
@@ -17,13 +17,17 @@ function loadAsScript(relativePath) {
function makeEl() {
const el = {
_html: '', style: {}, value: '',
_html: '',
style: {},
value: '',
classList: { add() {}, remove() {}, toggle() {} },
children: [],
appendChild(c) { this.children.push(c); },
querySelector() { return null; },
setAttribute() {}, getAttribute() { return null; },
addEventListener() {}, removeEventListener() {},
setAttribute() {},
getAttribute() { return null; },
addEventListener() {},
removeEventListener() {},
};
Object.defineProperty(el, 'innerHTML', {
get() { return this._html; },
@@ -90,7 +94,7 @@ test('assistant — une chaîne "attendre -> puis dialogue" sauvegarde bien LES
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
assert.ok(saveCall, 'la règle aurait dû être postée');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'attendre');
assert.equal(action.data_value, '2');
assert.equal(action.then.type, 'dialogue');
@@ -114,7 +118,7 @@ test('assistant — une chaîne de 3 actions ("variable" -> "attendre" -> "dialo
global.collisionWizardChainDone('none');
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'variable');
assert.equal(action.then.type, 'attendre');
assert.equal(action.then.data_value, '1.5');
@@ -131,7 +135,7 @@ test('assistant — une seule action ("Rien de plus" tout de suite) sauvegarde b
global.collisionWizardChainDone('none');
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
const action = saveCall.body.rule.action;
const { action } = saveCall.body.rule;
assert.equal(action.type, 'dialogue');
assert.equal(action.then, undefined);
});
@@ -142,9 +146,15 @@ test('assistant — l\'action "vidéo" ne propose QUE les fichiers vidéo (jamai
// USER_ASSETS_OPTIONS en entier, sans filtrer par kind.
setupGlobals();
global.USER_ASSETS_OPTIONS = [
{ id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image' },
{ id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio' },
{ id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video' },
{
id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image',
},
{
id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio',
},
{
id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video',
},
];
const body = document.getElementById('questTriggerModalBody');
@@ -161,9 +171,15 @@ test('assistant — l\'action "vidéo" ne propose QUE les fichiers vidéo (jamai
test('assistant — l\'action "son" ne propose QUE les fichiers audio (jamais les images/vidéos)', () => {
setupGlobals();
global.USER_ASSETS_OPTIONS = [
{ id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image' },
{ id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio' },
{ id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video' },
{
id: 1, url: '/assets/1/photo.png', name: 'photo.png', kind: 'image',
},
{
id: 2, url: '/assets/1/bip.mp3', name: 'bip.mp3', kind: 'audio',
},
{
id: 3, url: '/assets/1/clip.mp4', name: 'clip.mp4', kind: 'video',
},
];
const body = document.getElementById('questTriggerModalBody');
@@ -183,7 +199,9 @@ test('triggerActionChainHtml — un bouton "+ Ajouter une action" apparaît apr
// juste après lui, pas seulement le tout dernier de la chaîne.
setupGlobals();
const chain = [
{ type: 'variable', id: 'v_1', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
{
type: 'variable', id: 'v_1', target_variable: 'score', data_operation: 'incrementer', data_value: '1',
},
{ type: 'attendre', id: 'a_1', data_value: '2' },
{ type: 'dialogue', dialogue: { id: 'd_1', lines: [] } },
];
+175 -188
View File
@@ -9,30 +9,37 @@
// exacte d'une règle (côté serveur, où elle est validée une seconde fois
// avant d'être enregistrée).
var COLLISION_ACTION_LABELS = {
dialogue: 'Déclencher ce dialogue', variable: 'Modifier une variable',
surbrillance: 'Mettre en surbrillance', visibilite: 'Rendre visible/invisible',
son: 'Jouer un son', video: 'Jouer une vidéo', indication: 'Afficher une indication',
const COLLISION_ACTION_LABELS = {
dialogue: 'Déclencher ce dialogue',
variable: 'Modifier une variable',
surbrillance: 'Mettre en surbrillance',
visibilite: 'Rendre visible/invisible',
son: 'Jouer un son',
video: 'Jouer une vidéo',
indication: 'Afficher une indication',
attendre: 'Attendre quelques secondes',
interagir: 'Interagir avec une touche', condition: 'Si une condition est vraie',
interagir: 'Interagir avec une touche',
condition: 'Si une condition est vraie',
};
// Ordre d'affichage — "interagir"/"condition" en dernier (comportements
// spéciaux), le reste (les FEUILLES, voir screens/rendering/
// collision_rules.py::LEAF_ACTION_TYPES) dans le même ordre que
// COLLISION_CHAIN_CHOICES ci-dessous, pour rester cohérent d'un écran à
// l'autre de l'assistant.
var COLLISION_WIZARD_ACTION_TYPES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre', 'interagir', 'condition'];
const COLLISION_WIZARD_ACTION_TYPES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre', 'interagir', 'condition'];
// FEUILLES chaînables via "then" — utilisées à la fois pour l'étape
// "Ensuite ?" (renderCollisionWizardChainStep) et les branches Vrai/Faux
// d'une condition (renderCollisionWizardBranchStep) : MÊME liste des deux
// côtés, voir screens/rendering/collision_rules.py::LEAF_ACTION_TYPES.
var COLLISION_CHAIN_CHOICES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre'];
var COLLISION_TRIGGER_LABELS = { collision: 'À la collision', clic: 'Au clic', survol: 'Au survol', affichage: 'À l\'affichage de l\'écran' };
const COLLISION_CHAIN_CHOICES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre'];
const COLLISION_TRIGGER_LABELS = {
collision: 'À la collision', clic: 'Au clic', survol: 'Au survol', affichage: 'À l\'affichage de l\'écran',
};
// Opérations qui n'ont besoin d'AUCUNE valeur (voir screens/labels/
// data_operations.py::DATA_OPERATIONS) — le champ valeur de l'étape
// "Modifier une variable" (renderCollisionWizardVariableStep) se masque
// pour celles-ci.
var COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
const COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
// obj.thumbnail_html (screens.render_scene_object) porte un style inline
// PENSÉ POUR LE CANEVAS (position:absolute + left/top en dur, voir
@@ -46,8 +53,8 @@ var COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', '
// (l'image elle-même) est repris. Utilisée par static/js/triggers/
// trigger-editor.js (chaîne de bulles + grille de choix d'objet).
function collisionRuleThumbHtml(obj) {
var match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
return match ? '<img src="' + match[1] + '" alt="">' : '';
const match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
return match ? `<img src="${match[1]}" alt="">` : '';
}
// Id court généré CÔTÉ CLIENT pour une NOUVELLE feuille ("dialogue" ou
@@ -58,7 +65,7 @@ function collisionRuleThumbHtml(obj) {
// permet d'adresser CETTE feuille précise pour y attacher un "then"
// plus tard (voir "+ Ajouter une action", trigger-editor.js).
function collisionGenerateActionId() {
return 'd_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
return `d_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; // NOSONAR S2245 - id local UI, jamais crypto
}
// ---------- Assistant "déclencheur -> action" ----------
@@ -68,12 +75,14 @@ function collisionGenerateActionId() {
// `action`, ou dans `action.sub_action` au second niveau pour "interagir").
// Plus de "quête cible" à connaître d'avance — la règle est posée sur
// l'objet, un déclencheur "dialogue" se suffit à lui-même.
var _collisionWizard = null;
let _collisionWizard = null;
// Appelée après avoir choisi l'objet dans la modale d'ajout de déclencheur
// (voir triggerSelectObject, trigger-editor.js).
function openTriggerWizard(objectId, bodyEl) {
_collisionWizard = { target: 'object', objectId: objectId, trigger: null, bodyEl: bodyEl };
_collisionWizard = {
target: 'object', objectId, trigger: null, bodyEl,
};
renderCollisionWizardTriggerStep();
}
@@ -83,7 +92,9 @@ function openTriggerWizard(objectId, bodyEl) {
// un seul déclencheur possible ("affichage"), donc l'étape 1 n'a qu'un
// choix, gardée pour rester cohérente avec le parcours objet.
function openScreenTriggerWizard(screenId, bodyEl) {
_collisionWizard = { target: 'screen', screenId: screenId, trigger: null, bodyEl: bodyEl };
_collisionWizard = {
target: 'screen', screenId, trigger: null, bodyEl,
};
renderCollisionWizardTriggerStep();
}
@@ -96,15 +107,12 @@ function collisionWizardBodyEl() {
// reste affichée telle quelle même à un seul choix (plutôt que sautée
// automatiquement) pour rester cohérente d'un parcours à l'autre.
function renderCollisionWizardTriggerStep() {
var keys = _collisionWizard.target === 'screen' ? ['affichage'] : ['collision', 'clic', 'survol'];
var buttons = keys.map(function (key, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseTrigger(\'' + key + '\')">' +
'<span>' + COLLISION_TRIGGER_LABELS[key] + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
'<div class="collisionWizardChoices">' + buttons + '</div>';
const keys = _collisionWizard.target === 'screen' ? ['affichage'] : ['collision', 'clic', 'survol'];
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseTrigger('${key}')">`
+ `<span>${COLLISION_TRIGGER_LABELS[key]}</span>`
+ '</button>').join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Cette règle se déclenche...</p>'
+ `<div class="collisionWizardChoices">${buttons}</div>`;
}
function collisionWizardChooseTrigger(trigger) {
@@ -119,17 +127,14 @@ function collisionWizardChooseTrigger(trigger) {
// (bulle "Appuie sur X" tant que le contact dure) — jamais pour "clic"/
// "survol"/"affichage", qui n'ont pas cette notion de contact prolongé.
function renderCollisionWizardActionStep(nested) {
var types = (nested || _collisionWizard.trigger !== 'collision')
? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; })
const types = (nested || _collisionWizard.trigger !== 'collision')
? COLLISION_WIZARD_ACTION_TYPES.filter((t) => t !== 'interagir')
: COLLISION_WIZARD_ACTION_TYPES;
var buttons = types.map(function (type, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
'<span>' + COLLISION_ACTION_LABELS[type] + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">' + (nested ? 'Une fois la touche pressée...' : 'Alors...') + '</p>' +
'<div class="collisionWizardChoices">' + buttons + '</div>';
const buttons = types.map((type, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseAction('${type}', ${nested ? 'true' : 'false'})">`
+ `<span>${COLLISION_ACTION_LABELS[type]}</span>`
+ '</button>').join('');
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${nested ? 'Une fois la touche pressée...' : 'Alors...'}</p>`
+ `<div class="collisionWizardChoices">${buttons}</div>`;
}
function collisionWizardFinalizeAction(action, nested) {
@@ -156,17 +161,18 @@ function collisionWizardFinalizeAction(action, nested) {
// "liste ENTIÈRE" des deux côtés (recharge les règles existantes, y
// ajoute la nouvelle, sauvegarde tout), géré côté serveur.
function collisionWizardSaveRule(action) {
var rule = { trigger: _collisionWizard.trigger, action: action };
const rule = { trigger: _collisionWizard.trigger, action };
closeTriggerModal();
var isScreen = _collisionWizard.target === 'screen';
var url = isScreen
? '/game/' + GAME_SLUG + '/screens/' + _collisionWizard.screenId + '/triggers'
: '/game/' + GAME_SLUG + '/triggers';
var body = isScreen ? { rule: rule } : { object_id: _collisionWizard.objectId, rule: rule };
const isScreen = _collisionWizard.target === 'screen';
const url = isScreen
? `/game/${GAME_SLUG}/screens/${_collisionWizard.screenId}/triggers`
: `/game/${GAME_SLUG}/triggers`;
const body = isScreen ? { rule } : { object_id: _collisionWizard.objectId, rule };
fetch(url, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(function () { isScreen ? screenTriggerLoadAll() : triggerLoadAll(); });
}).then(() => { isScreen ? screenTriggerLoadAll() : triggerLoadAll(); });
}
// Construit une FEUILLE du type demandé — passe par ses éventuelles
@@ -188,53 +194,56 @@ function collisionWizardBuildLeafAction(type, onBuilt) {
return;
}
if (type === 'surbrillance' || type === 'visibilite') {
renderCollisionWizardObjectTargetStep(function (objectId) {
renderCollisionWizardValeurStep(type, function (valeur) {
onBuilt({ type: type, id: collisionGenerateActionId(), object_id: objectId, valeur: valeur });
renderCollisionWizardObjectTargetStep((objectId) => {
renderCollisionWizardValeurStep(type, (valeur) => {
onBuilt({
type, id: collisionGenerateActionId(), object_id: objectId, valeur,
});
});
});
return;
}
if (type === 'son') {
renderCollisionWizardAssetStep(function (assetUrl) {
renderCollisionWizardAssetStep((assetUrl) => {
onBuilt({ type: 'son', id: collisionGenerateActionId(), asset_url: assetUrl });
}, 'audio');
return;
}
if (type === 'video') {
renderCollisionWizardAssetStep(function (assetUrl) {
renderCollisionWizardVideoModeStep(function (mode) {
onBuilt({ type: 'video', id: collisionGenerateActionId(), asset_url: assetUrl, mode: mode });
renderCollisionWizardAssetStep((assetUrl) => {
renderCollisionWizardVideoModeStep((mode) => {
onBuilt({
type: 'video', id: collisionGenerateActionId(), asset_url: assetUrl, mode,
});
});
}, 'video');
return;
}
if (type === 'indication') {
renderCollisionWizardObjectTargetStep(function (objectId) {
renderCollisionWizardObjectTargetStep((objectId) => {
_collisionWizard.pendingIndicationObjectId = objectId;
renderCollisionWizardIndicationStep(function (action) { onBuilt(action); });
renderCollisionWizardIndicationStep((action) => { onBuilt(action); });
});
return;
}
// 'attendre'
renderCollisionWizardAttendreStep(function (secondes) {
renderCollisionWizardAttendreStep((secondes) => {
onBuilt({ type: 'attendre', id: collisionGenerateActionId(), data_value: secondes });
});
}
// ---------- Étape "attendre" (nombre de secondes) ----------
function renderCollisionWizardAttendreStep(onChoose) {
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Attendre combien de secondes avant de continuer ?</p>' +
'<input type="number" id="attendreWizSecondes" min="0" step="0.5" placeholder="2" style="width:100px; margin-bottom:10px;">' +
'<button type="button" class="button primary" onclick="collisionWizardAttendreNext()">Continuer</button>';
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Attendre combien de secondes avant de continuer ?</p>'
+ '<input type="number" id="attendreWizSecondes" min="0" step="0.5" placeholder="2" style="width:100px; margin-bottom:10px;">'
+ '<button type="button" class="button primary" onclick="collisionWizardAttendreNext()">Continuer</button>';
_collisionWizard.onAttendreChosen = onChoose;
}
function collisionWizardAttendreNext() {
var raw = document.getElementById('attendreWizSecondes').value;
var secondes = String(Math.max(0, parseFloat(raw) || 0));
var onChoose = _collisionWizard.onAttendreChosen;
const raw = document.getElementById('attendreWizSecondes').value;
const secondes = String(Math.max(0, parseFloat(raw) || 0));
const onChoose = _collisionWizard.onAttendreChosen;
_collisionWizard.onAttendreChosen = null;
onChoose(secondes);
}
@@ -246,27 +255,24 @@ function collisionWizardAttendreNext() {
// forcément celui qui porte la règle (ex. le joueur touche un
// interrupteur -> une indication apparaît près d'une porte ailleurs).
function renderCollisionWizardObjectTargetStep(onChoose) {
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Sur quel objet ?</p>' +
'<div id="collisionWizardObjectTargetGrid" class="collisionWizardChoices">Chargement…</div>';
fetch('/game/' + GAME_SLUG + '/triggers/objects-catalog')
.then(function (r) { return r.json(); })
.then(function (objects) {
var grid = document.getElementById('collisionWizardObjectTargetGrid');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Sur quel objet ?</p>'
+ '<div id="collisionWizardObjectTargetGrid" class="collisionWizardChoices">Chargement…</div>';
fetch(`/game/${GAME_SLUG}/triggers/objects-catalog`)
.then((r) => r.json())
.then((objects) => {
const grid = document.getElementById('collisionWizardObjectTargetGrid');
if (!grid) return;
if (!objects.length) { grid.innerHTML = '<p class="hint">Aucun objet éligible sur ce jeu.</p>'; return; }
grid.innerHTML = objects.map(function (o, i) {
return '<button type="button" class="collisionWizardChoice questTriggerObjectChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardObjectTargetChosen(' + o.id + ')">' +
'<div class="questTriggerObjectThumb">' + collisionRuleThumbHtml(o) + '</div>' +
'<span>' + (o.name || '').replace(/</g, '&lt;') + '</span>' +
'</button>';
}).join('');
grid.innerHTML = objects.map((o, i) => `<button type="button" class="collisionWizardChoice questTriggerObjectChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardObjectTargetChosen(${o.id})">`
+ `<div class="questTriggerObjectThumb">${collisionRuleThumbHtml(o)}</div>`
+ `<span>${(o.name || '').replace(/</g, '&lt;')}</span>`
+ '</button>').join('');
_collisionWizard.onObjectTargetChosen = onChoose;
});
}
function collisionWizardObjectTargetChosen(objectId) {
var onChoose = _collisionWizard.onObjectTargetChosen;
const onChoose = _collisionWizard.onObjectTargetChosen;
_collisionWizard.onObjectTargetChosen = null;
onChoose(objectId);
}
@@ -276,21 +282,18 @@ function collisionWizardObjectTargetChosen(objectId) {
// templates/scene_edit.html (voir screens/labels/surbrillance_values.py
// et screens/labels/element_visibility.py — mêmes valeurs que le moteur).
function renderCollisionWizardValeurStep(actionType, onChoose) {
var labelsMap = actionType === 'surbrillance' ? SURBRILLANCE_LABELS_MAP : ELEMENT_VISIBILITY_LABELS_MAP;
var keys = Object.keys(labelsMap);
var buttons = keys.map(function (key, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardValeurChosen(\'' + key + '\')">' +
'<span>' + labelsMap[key] + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">' + (actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :') + '</p>' +
'<div class="collisionWizardChoices">' + buttons + '</div>';
const labelsMap = actionType === 'surbrillance' ? SURBRILLANCE_LABELS_MAP : ELEMENT_VISIBILITY_LABELS_MAP;
const keys = Object.keys(labelsMap);
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardValeurChosen('${key}')">`
+ `<span>${labelsMap[key]}</span>`
+ '</button>').join('');
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :'}</p>`
+ `<div class="collisionWizardChoices">${buttons}</div>`;
_collisionWizard.onValeurChosen = onChoose;
}
function collisionWizardValeurChosen(valeur) {
var onChoose = _collisionWizard.onValeurChosen;
const onChoose = _collisionWizard.onValeurChosen;
_collisionWizard.onValeurChosen = null;
onChoose(valeur);
}
@@ -305,46 +308,39 @@ function collisionWizardValeurChosen(valeur) {
// inversement pour "son"), un fichier du mauvais type ne pouvant de toute
// façon jamais être lu par <video>/<audio> côté jeu.
function renderCollisionWizardAssetStep(onChoose, kind) {
var assets = USER_ASSETS_OPTIONS.filter(function (a) { return a.kind === kind; });
const assets = USER_ASSETS_OPTIONS.filter((a) => a.kind === kind);
if (!assets.length) {
var label = kind === 'video' ? 'vidéo (mp4/webm)' : 'son';
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Aucun fichier ' + label + ' dans "Mes assets" pour l\'instant — importe-en un depuis le panneau de gauche (section "Mes assets"), puis recommence.</p>';
const label = kind === 'video' ? 'vidéo (mp4/webm)' : 'son';
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">Aucun fichier ${label} dans "Mes assets" pour l'instant — importe-en un depuis le panneau de gauche (section "Mes assets"), puis recommence.</p>`;
return;
}
var options = assets.map(function (a) {
return '<option value="' + a.url + '">' + (a.name || '').replace(/</g, '&lt;') + '</option>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Quel fichier (voir "Mes assets") ?</p>' +
'<select id="collisionWizardAssetSelect" style="width:100%; margin-bottom:10px;">' + options + '</select>' +
'<button type="button" class="button primary" onclick="collisionWizardAssetNext()">Continuer</button>';
const options = assets.map((a) => `<option value="${a.url}">${(a.name || '').replace(/</g, '&lt;')}</option>`).join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel fichier (voir "Mes assets") ?</p>'
+ `<select id="collisionWizardAssetSelect" style="width:100%; margin-bottom:10px;">${options}</select>`
+ '<button type="button" class="button primary" onclick="collisionWizardAssetNext()">Continuer</button>';
_collisionWizard.onAssetChosen = onChoose;
}
function collisionWizardAssetNext() {
var url = document.getElementById('collisionWizardAssetSelect').value;
var onChoose = _collisionWizard.onAssetChosen;
const url = document.getElementById('collisionWizardAssetSelect').value;
const onChoose = _collisionWizard.onAssetChosen;
_collisionWizard.onAssetChosen = null;
onChoose(url);
}
// ---------- Étape "comment l'afficher ?" (vidéo) ----------
function renderCollisionWizardVideoModeStep(onChoose) {
var keys = Object.keys(VIDEO_MODE_LABELS_MAP);
var buttons = keys.map(function (key, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardVideoModeChosen(\'' + key + '\')">' +
'<span>' + VIDEO_MODE_LABELS_MAP[key] + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Comment l\'afficher ?</p>' +
'<div class="collisionWizardChoices">' + buttons + '</div>';
const keys = Object.keys(VIDEO_MODE_LABELS_MAP);
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardVideoModeChosen('${key}')">`
+ `<span>${VIDEO_MODE_LABELS_MAP[key]}</span>`
+ '</button>').join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Comment l\'afficher ?</p>'
+ `<div class="collisionWizardChoices">${buttons}</div>`;
_collisionWizard.onVideoModeChosen = onChoose;
}
function collisionWizardVideoModeChosen(mode) {
var onChoose = _collisionWizard.onVideoModeChosen;
const onChoose = _collisionWizard.onVideoModeChosen;
_collisionWizard.onVideoModeChosen = null;
onChoose(mode);
}
@@ -354,22 +350,23 @@ function collisionWizardVideoModeChosen(mode) {
// collisionWizardBuildLeafAction) — mémorisé sur le wizard le temps de
// cette étape plutôt que reporté en paramètre partout.
function renderCollisionWizardIndicationStep(onChoose) {
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Texte de la bulle :</p>' +
'<input type="text" id="indicationWizText" placeholder="Ex. Clique ici pour continuer" style="width:100%; margin-bottom:8px;">' +
'<label class="hint" style="display:block; margin-bottom:10px;">Durée d\'affichage en ms (optionnel, 3000 par défaut) : ' +
'<input type="number" id="indicationWizDuree" min="500" step="500" placeholder="3000" style="width:100px;"></label>' +
'<button type="button" class="button primary" onclick="collisionWizardIndicationNext()">Continuer</button>';
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Texte de la bulle :</p>'
+ '<input type="text" id="indicationWizText" placeholder="Ex. Clique ici pour continuer" style="width:100%; margin-bottom:8px;">'
+ '<label class="hint" style="display:block; margin-bottom:10px;">Durée d\'affichage en ms (optionnel, 3000 par défaut) : '
+ '<input type="number" id="indicationWizDuree" min="500" step="500" placeholder="3000" style="width:100px;"></label>'
+ '<button type="button" class="button primary" onclick="collisionWizardIndicationNext()">Continuer</button>';
_collisionWizard.onIndicationChosen = onChoose;
}
function collisionWizardIndicationNext() {
var texte = document.getElementById('indicationWizText').value;
var dureeRaw = document.getElementById('indicationWizDuree').value;
var action = { type: 'indication', id: collisionGenerateActionId(), object_id: _collisionWizard.pendingIndicationObjectId, texte: texte };
const texte = document.getElementById('indicationWizText').value;
const dureeRaw = document.getElementById('indicationWizDuree').value;
const action = {
type: 'indication', id: collisionGenerateActionId(), object_id: _collisionWizard.pendingIndicationObjectId, texte,
};
if (dureeRaw) action.duree_ms = parseInt(dureeRaw, 10);
_collisionWizard.pendingIndicationObjectId = null;
var onChoose = _collisionWizard.onIndicationChosen;
const onChoose = _collisionWizard.onIndicationChosen;
_collisionWizard.onIndicationChosen = null;
onChoose(action);
}
@@ -384,15 +381,15 @@ function collisionWizardIndicationNext() {
// mini-formulaire (voir renderCollisionWizardConditionStep).
function collisionWizardChooseAction(type, nested) {
if (type === 'interagir') {
collisionWizardFinalizeAction({ type: type }, nested);
collisionWizardFinalizeAction({ type }, nested);
return;
}
if (type === 'condition') {
renderCollisionWizardConditionStep(nested);
return;
}
collisionWizardBuildLeafAction(type, function (leaf) {
renderCollisionWizardChainStep(leaf, function (finalLeaf) { collisionWizardFinalizeAction(finalLeaf, nested); });
collisionWizardBuildLeafAction(type, (leaf) => {
renderCollisionWizardChainStep(leaf, (finalLeaf) => { collisionWizardFinalizeAction(finalLeaf, nested); });
});
}
@@ -404,40 +401,38 @@ function collisionWizardChooseAction(type, nested) {
// valeur (masquée pour les opérations qui n'en ont pas besoin, voir
// COLLISION_NO_VALUE_OPERATIONS). `onChoose(action)`.
function renderCollisionWizardVariableStep(onChoose) {
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
var operationOptions = Object.keys(DATA_OPERATION_LABELS_MAP).map(function (key) {
return '<option value="' + key + '">' + DATA_OPERATION_LABELS_MAP[key] + '</option>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Modifier...</p>' +
'<div class="collisionWizardConditionForm">' +
'<select id="varWizVariable">' + variableOptions + '</select>' +
'<select id="varWizOperation" onchange="collisionWizardVariableOperationChanged()">' + operationOptions + '</select>' +
'<input type="text" id="varWizValue" placeholder="Valeur">' +
'</div>' +
'<button type="button" class="button primary mt-3" onclick="collisionWizardVariableNext()">Continuer</button>';
const variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
.map((name) => `<option value="${name}">${name}</option>`).join('');
const operationOptions = Object.keys(DATA_OPERATION_LABELS_MAP).map((key) => `<option value="${key}">${DATA_OPERATION_LABELS_MAP[key]}</option>`).join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Modifier...</p>'
+ '<div class="collisionWizardConditionForm">'
+ `<select id="varWizVariable">${variableOptions}</select>`
+ `<select id="varWizOperation" onchange="collisionWizardVariableOperationChanged()">${operationOptions}</select>`
+ '<input type="text" id="varWizValue" placeholder="Valeur">'
+ '</div>'
+ '<button type="button" class="button primary mt-3" onclick="collisionWizardVariableNext()">Continuer</button>';
_collisionWizard.onVariableChosen = onChoose;
collisionWizardVariableOperationChanged();
}
function collisionWizardVariableOperationChanged() {
var operation = document.getElementById('varWizOperation').value;
var valueInput = document.getElementById('varWizValue');
const operation = document.getElementById('varWizOperation').value;
const valueInput = document.getElementById('varWizValue');
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1 ? '' : 'none';
}
function collisionWizardVariableNext() {
var operation = document.getElementById('varWizOperation').value;
var action = {
type: 'variable', id: collisionGenerateActionId(),
const operation = document.getElementById('varWizOperation').value;
const action = {
type: 'variable',
id: collisionGenerateActionId(),
target_variable: document.getElementById('varWizVariable').value,
data_operation: operation,
};
if (COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1) {
action.data_value = document.getElementById('varWizValue').value;
}
var onChoose = _collisionWizard.onVariableChosen;
const onChoose = _collisionWizard.onVariableChosen;
_collisionWizard.onVariableChosen = null;
onChoose(action);
}
@@ -461,30 +456,27 @@ function collisionWizardVariableNext() {
// dialogue, "attendre" disparaissait silencieusement.
function renderCollisionWizardChainStep(leafAction, onDone, headAction) {
headAction = headAction || leafAction;
var completionHtml = leafAction.type === 'dialogue'
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">' +
'<input type="checkbox" id="condWizMarkCompleted"' + (leafAction.mark_completed ? ' checked' : '') + '> Marquer ce dialogue comme terminé une fois joué' +
'</label>'
const completionHtml = leafAction.type === 'dialogue'
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">'
+ `<input type="checkbox" id="condWizMarkCompleted"${leafAction.mark_completed ? ' checked' : ''}> Marquer ce dialogue comme terminé une fois joué`
+ '</label>'
: '';
var choiceButtons = COLLISION_CHAIN_CHOICES.map(function (choice, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + ((i + 1) * 40) + 'ms" onclick="collisionWizardChainDone(\'' + choice + '\')"><span>' + COLLISION_ACTION_LABELS[choice] + '</span></button>';
}).join('');
collisionWizardBodyEl().innerHTML =
completionHtml +
'<p class="hint mb-2">Ensuite ?</p>' +
'<div class="collisionWizardChoices">' +
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChainDone(\'none\')"><span>Rien de plus</span></button>' +
choiceButtons +
'</div>';
const choiceButtons = COLLISION_CHAIN_CHOICES.map((choice, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${(i + 1) * 40}ms" onclick="collisionWizardChainDone('${choice}')"><span>${COLLISION_ACTION_LABELS[choice]}</span></button>`).join('');
collisionWizardBodyEl().innerHTML = `${completionHtml
}<p class="hint mb-2">Ensuite ?</p>`
+ '<div class="collisionWizardChoices">'
+ `<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChainDone('none')"><span>Rien de plus</span></button>${
choiceButtons
}</div>`;
_collisionWizard.chainLeaf = leafAction;
_collisionWizard.chainHead = headAction;
_collisionWizard.onChainDone = onDone;
}
function collisionWizardChainDone(choice) {
var leafAction = _collisionWizard.chainLeaf;
var headAction = _collisionWizard.chainHead;
var onDone = _collisionWizard.onChainDone;
const leafAction = _collisionWizard.chainLeaf;
const headAction = _collisionWizard.chainHead;
const onDone = _collisionWizard.onChainDone;
if (leafAction.type === 'dialogue') {
if (document.getElementById('condWizMarkCompleted').checked) leafAction.mark_completed = true;
else delete leafAction.mark_completed;
@@ -493,8 +485,8 @@ function collisionWizardChainDone(choice) {
onDone(headAction);
return;
}
collisionWizardBuildLeafAction(choice, function (nextLeaf) {
leafAction.then = nextLeaf;
collisionWizardBuildLeafAction(choice, (nextLeaf) => {
leafAction.then = nextLeaf; // NOSONAR S7739 - chainage d'actions (donnee), jamais une promesse/await
renderCollisionWizardChainStep(nextLeaf, onDone, headAction);
});
}
@@ -506,19 +498,16 @@ function collisionWizardChainDone(choice) {
// `nested` (interagir -> condition) est reporté jusqu'à la finalisation,
// une fois les deux branches Vrai/Faux choisies ci-dessous.
function renderCollisionWizardConditionStep(nested) {
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
var operatorOptions = Object.keys(CONDITION_OPERATOR_LABELS_MAP).map(function (key) {
return '<option value="' + key + '">' + CONDITION_OPERATOR_LABELS_MAP[key] + '</option>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Si...</p>' +
'<div class="collisionWizardConditionForm">' +
'<select id="condWizVariable">' + variableOptions + '</select>' +
'<select id="condWizOperator">' + operatorOptions + '</select>' +
'<input type="text" id="condWizValue" placeholder="Valeur">' +
'</div>' +
'<button type="button" class="button primary mt-3" onclick="collisionWizardConditionNext(' + (nested ? 'true' : 'false') + ')">Continuer</button>';
const variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
.map((name) => `<option value="${name}">${name}</option>`).join('');
const operatorOptions = Object.keys(CONDITION_OPERATOR_LABELS_MAP).map((key) => `<option value="${key}">${CONDITION_OPERATOR_LABELS_MAP[key]}</option>`).join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Si...</p>'
+ '<div class="collisionWizardConditionForm">'
+ `<select id="condWizVariable">${variableOptions}</select>`
+ `<select id="condWizOperator">${operatorOptions}</select>`
+ '<input type="text" id="condWizValue" placeholder="Valeur">'
+ '</div>'
+ `<button type="button" class="button primary mt-3" onclick="collisionWizardConditionNext(${nested ? 'true' : 'false'})">Continuer</button>`;
}
function collisionWizardConditionNext(nested) {
@@ -527,7 +516,8 @@ function collisionWizardConditionNext(nested) {
variable: document.getElementById('condWizVariable').value,
operateur: document.getElementById('condWizOperator').value,
valeur: document.getElementById('condWizValue').value,
si_vrai: null, si_faux: null,
si_vrai: null,
si_faux: null,
};
_collisionWizard.pendingConditionNested = nested;
renderCollisionWizardBranchStep('si_vrai');
@@ -541,18 +531,15 @@ function collisionWizardConditionNext(nested) {
// peuvent chacune déclencher LEUR PROPRE feuille (et sa propre chaîne),
// indépendamment l'une de l'autre.
function renderCollisionWizardBranchStep(branchKey) {
var label = branchKey === 'si_vrai' ? '✓ Si VRAI...' : '✕ Si FAUX...';
var choiceButtons = COLLISION_CHAIN_CHOICES.map(function (choice, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'' + choice + '\')"><span>' + COLLISION_ACTION_LABELS[choice] + '</span></button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">' + label + '</p>' +
'<div class="collisionWizardChoices">' +
choiceButtons +
'<button type="button" class="collisionWizardChoice" style="animation-delay:' + (COLLISION_CHAIN_CHOICES.length * 40) + 'ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'none\')">' +
'<span>Ne rien faire</span>' +
'</button>' +
'</div>';
const label = branchKey === 'si_vrai' ? '✓ Si VRAI...' : '✕ Si FAUX...';
const choiceButtons = COLLISION_CHAIN_CHOICES.map((choice, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseBranch('${branchKey}', '${choice}')"><span>${COLLISION_ACTION_LABELS[choice]}</span></button>`).join('');
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${label}</p>`
+ `<div class="collisionWizardChoices">${
choiceButtons
}<button type="button" class="collisionWizardChoice" style="animation-delay:${COLLISION_CHAIN_CHOICES.length * 40}ms" onclick="collisionWizardChooseBranch('${branchKey}', 'none')">`
+ '<span>Ne rien faire</span>'
+ '</button>'
+ '</div>';
}
function collisionWizardChooseBranch(branchKey, choice) {
@@ -561,8 +548,8 @@ function collisionWizardChooseBranch(branchKey, choice) {
collisionWizardBranchDone(branchKey);
return;
}
collisionWizardBuildLeafAction(choice, function (leaf) {
renderCollisionWizardChainStep(leaf, function (finalLeaf) {
collisionWizardBuildLeafAction(choice, (leaf) => {
renderCollisionWizardChainStep(leaf, (finalLeaf) => {
_collisionWizard.pendingCondition[branchKey] = finalLeaf;
collisionWizardBranchDone(branchKey);
});
@@ -574,8 +561,8 @@ function collisionWizardBranchDone(branchKey) {
renderCollisionWizardBranchStep('si_faux');
return;
}
var nested = _collisionWizard.pendingConditionNested;
var condition = _collisionWizard.pendingCondition;
const nested = _collisionWizard.pendingConditionNested;
const condition = _collisionWizard.pendingCondition;
_collisionWizard.pendingCondition = null;
collisionWizardFinalizeAction(condition, nested);
}
+28 -28
View File
@@ -7,61 +7,61 @@
// besoin d'un bouton "Enregistrer" séparé (demande explicite : "que la
// modification s'enregistre automatiquement").
function saveGlobalVariable(varId) {
var row = document.querySelector('tr[data-var-row="' + varId + '"]');
const row = document.querySelector(`tr[data-var-row="${varId}"]`);
if (!row) return;
var type = row.querySelector('select[name="type"]').value;
var value = row.querySelector('input[name="value"]').value;
var status = document.getElementById('varSaveStatus' + varId);
var body = new FormData();
const type = row.querySelector('select[name="type"]').value;
const { value } = row.querySelector('input[name="value"]');
const status = document.getElementById(`varSaveStatus${varId}`);
const body = new FormData();
body.append('type', type);
body.append('value', value);
fetch('/game/' + GAME_SLUG + '/variables/' + varId + '/edit', {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
fetch(`/game/${GAME_SLUG}/variables/${varId}/edit`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then(function (r) {
.then((r) => {
if (!r.ok) { window.location.reload(); return; }
if (status) {
status.textContent = 'Enregistré ✓';
setTimeout(function () { status.textContent = ''; }, 2000);
setTimeout(() => { status.textContent = ''; }, 2000);
}
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
function deleteGlobalVariable(varId, name) {
if (!confirm('Supprimer la variable « ' + name + ' » ? Toute condition/action qui la référence encore cessera de fonctionner.')) return;
var body = new FormData();
fetch('/game/' + GAME_SLUG + '/variables/' + varId + '/delete', {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
if (!window.confirm(`Supprimer la variable « ${name} » ? Toute condition/action qui la référence encore cessera de fonctionner.`)) return;
const body = new FormData();
fetch(`/game/${GAME_SLUG}/variables/${varId}/delete`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then(function (r) {
.then((r) => {
if (!r.ok) { window.location.reload(); return; }
var row = document.querySelector('tr[data-var-row="' + varId + '"]');
const row = document.querySelector(`tr[data-var-row="${varId}"]`);
if (row) row.remove();
var body2 = document.getElementById('globalVariablesBody');
var emptyHint = document.getElementById('globalVariablesEmptyHint');
const body2 = document.getElementById('globalVariablesBody');
const emptyHint = document.getElementById('globalVariablesEmptyHint');
if (body2 && emptyHint) emptyHint.style.display = body2.children.length ? 'none' : '';
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
function createGlobalVariable(event) {
event.preventDefault();
var form = event.target;
var body = new FormData(form);
fetch('/game/' + GAME_SLUG + '/variables/new', {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
const form = event.target;
const body = new FormData(form);
fetch(`/game/${GAME_SLUG}/variables/new`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
.then(function (json) {
.then((r) => (r.ok ? r.json() : Promise.reject()))
.then((json) => {
if (!json.row_html) { window.location.reload(); return; }
var tbody = document.getElementById('globalVariablesBody');
const tbody = document.getElementById('globalVariablesBody');
tbody.insertAdjacentHTML('beforeend', json.row_html);
var emptyHint = document.getElementById('globalVariablesEmptyHint');
const emptyHint = document.getElementById('globalVariablesEmptyHint');
if (emptyHint) emptyHint.style.display = 'none';
form.reset();
form.style.display = 'none'; // même comportement que toggleDashCreate() après création
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
return false;
}
+281 -239
View File
@@ -17,7 +17,7 @@
// 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, assetId, x, y) {
var body = new FormData();
const body = new FormData();
body.append('kind', kind);
if (forgeCharacter) body.append('forge_character', forgeCharacter);
if (backgroundSlug) body.append('background_slug', backgroundSlug);
@@ -28,35 +28,34 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
// du dépôt — absent pour un simple clic sur la galerie, qui garde le
// défaut habituel (100,100, voir screens/scenes/add_scene_object.py).
if (x != null && y != null) { body.append('x', x); body.append('y', y); }
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add", {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/scene-objects/add`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then(function (r) {
.then((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') || '';
const 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) {
.then((json) => {
if (!json) return;
var canvas = document.getElementById('canvas');
var el = document.createElement('div');
const canvas = document.getElementById('canvas');
const el = document.createElement('div');
el.className = 'canvasElement';
el.id = 'el-' + json.id;
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>';
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);
if (json.animations) {
ELEMENT_ANIMATIONS_MAP[json.id] = json.animations;
@@ -66,7 +65,7 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
// 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(); });
.catch(() => { window.location.reload(); });
}
// ---------- Vrai glisser-déposer depuis la galerie (demande explicite :
@@ -83,8 +82,10 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
function onGalleryTileDragStart(ev, kind, forgeCharacter, backgroundSlug, assetId) {
ev.dataTransfer.effectAllowed = 'copy';
ev.dataTransfer.setData('application/json', JSON.stringify({
kind: kind, forgeCharacter: forgeCharacter || null,
backgroundSlug: backgroundSlug || null, assetId: assetId || null,
kind,
forgeCharacter: forgeCharacter || null,
backgroundSlug: backgroundSlug || null,
assetId: assetId || null,
}));
}
@@ -98,18 +99,18 @@ function onCanvasDragOver(ev) {
function onCanvasDrop(ev) {
ev.preventDefault();
var raw = ev.dataTransfer.getData('application/json');
const raw = ev.dataTransfer.getData('application/json');
if (!raw) return;
var data;
let data;
try { data = JSON.parse(raw); } catch (e) { return; }
var canvas = document.getElementById('canvas');
var rect = canvas.getBoundingClientRect();
const canvas = document.getElementById('canvas');
const rect = canvas.getBoundingClientRect();
// Pas de zoom/échelle CSS sur #canvas (voir templates/scene_edit.html,
// juste width/height en px fixes) : une simple soustraction suffit,
// aucune division par un facteur d'échelle à appliquer. getBoundingClientRect()
// intègre déjà nativement le défilement de .canvasFrame.
var x = ev.clientX - rect.left;
var y = ev.clientY - rect.top;
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
addSceneObject(data.kind, data.forgeCharacter, data.backgroundSlug, data.assetId, x, y);
}
@@ -123,24 +124,23 @@ function onCanvasDrop(ev) {
// screens/scenes/add_scene_object.py) — absent pour un son/une vidéo,
// qui n'ont rien à "poser".
function uploadUserAsset(inputEl, sceneKind) {
var file = inputEl.files && inputEl.files[0];
const file = inputEl.files && inputEl.files[0];
if (!file) return;
var body = new FormData();
const body = new FormData();
body.append('file', file);
if (sceneKind) body.append('scene_kind', sceneKind);
fetch('/assets/upload', { method: 'POST', body: body })
.then(function (r) {
fetch('/assets/upload', { method: 'POST', body })
.then((r) =>
// Bug corrigé : un rechargement inconditionnel masquait toute
// erreur serveur (ex. son de plus de 3 minutes, voir
// auth/validate_audio_duration.py) — le fichier semblait juste
// "disparaître" sans explication, jamais ajouté à la galerie.
return r.json().then(function (json) { return { ok: r.ok, json: json }; });
})
.then(function (result) {
r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || "Impossible d'importer ce fichier."); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// Suppression d'un fichier de "Mes assets" (demande explicite : "voir,
@@ -149,14 +149,14 @@ function uploadUserAsset(inputEl, sceneKind) {
// comme uploadUserAsset() juste au-dessus : ce panneau ne se recompose
// pas assez souvent pour justifier une suppression ciblée du DOM.
function deleteUserAsset(assetId, name) {
if (!confirm('Supprimer "' + name + '" ? Cette action est définitive.')) return;
fetch('/assets/' + assetId, { method: 'DELETE' })
.then(function (r) { return r.json().then(function (json) { return { ok: r.ok, json: json }; }); })
.then(function (result) {
if (!result.ok) { alert(result.json.error || "Impossible de supprimer ce fichier."); return; }
if (!window.confirm(`Supprimer "${name}" ? Cette action est définitive.`)) return;
fetch(`/assets/${assetId}`, { method: 'DELETE' })
.then((r) => r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || 'Impossible de supprimer ce fichier.'); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// "→ Fond"/"→ Décor" (demande diagnostiquée depuis un "décalage" : une
@@ -167,16 +167,17 @@ function deleteUserAsset(assetId, name) {
// de l'ASSET, pour que la prochaine pose utilise la bonne section/le bon
// rendu (object-fit:fill pour un fond, :contain pour un décor).
function reclassifyUserAsset(assetId, sceneKind) {
fetch('/assets/' + assetId + '/scene-kind', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
fetch(`/assets/${assetId}/scene-kind`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scene_kind: sceneKind }),
})
.then(function (r) { return r.json().then(function (json) { return { ok: r.ok, json: json }; }); })
.then(function (result) {
if (!result.ok) { alert(result.json.error || "Reclassement impossible."); return; }
.then((r) => r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || 'Reclassement impossible.'); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// AJAX (voir routes/scenes/scene_object_delete.py) : bouton 🗑️ du panneau
@@ -187,33 +188,33 @@ function reclassifyUserAsset(assetId, sceneKind) {
// 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', {
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') || '';
.then((r) => {
const 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) {
.then((json) => {
if (!json || !json.ok) return;
var el = document.getElementById('el-' + id);
const el = document.getElementById(`el-${id}`);
if (el) el.remove();
if (typeof _stopPersonnageIdlePreview === 'function') _stopPersonnageIdlePreview(id);
// 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');
const 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);
window.history.replaceState(null, '', SCREEN_EDIT_URL);
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// Taille de la caméra (voir "🎮 Jeu 2D" en haut de templates/
@@ -224,19 +225,25 @@ function deleteSceneObject(id) {
// éventuel doivent tous deux refléter la nouvelle taille, plus simple
// qu'une mise à jour partielle en JS.
function applyCameraSize() {
var wEl = document.getElementById('scene-camera-w'), hEl = document.getElementById('scene-camera-h');
var width = parseInt(wEl.value, 10) || 1, height = parseInt(hEl.value, 10) || 1;
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-size", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ width: width, height: height }),
}).then(function () { window.location.reload(); });
const wEl = document.getElementById('scene-camera-w'); const
hEl = document.getElementById('scene-camera-h');
const width = parseInt(wEl.value, 10) || 1; const
height = parseInt(hEl.value, 10) || 1;
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/scene-size`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ width, height }),
}).then(() => { window.location.reload(); });
}
function saveSceneObjectGeometry(id, x, y, width, height) {
return fetch("/game/" + GAME_SLUG + "/scene-objects/" + id + "/geometry", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: x, y: y, width: width, height: height }),
}).then(function (r) { return r.json(); }).then(function (data) {
return fetch(`/game/${GAME_SLUG}/scene-objects/${id}/geometry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
x, y, width, height,
}),
}).then((r) => r.json()).then((data) => {
// Bug corrigé (même famille que iaChatRefreshAfterTurn(), voir
// static/js/ia/ia-chat.js) : un "fond" redimensionné/déplacé à la
// souris au-delà du champ de la caméra doit élargir le "monde" tout
@@ -248,18 +255,20 @@ function saveSceneObjectGeometry(id, x, y, width, height) {
if (data && typeof data.world_width === 'number' && typeof data.world_height === 'number') {
window.SCENE_WIDTH = data.world_width;
window.SCENE_HEIGHT = data.world_height;
var canvas = document.getElementById('canvas');
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.style.width = data.world_width + 'px';
canvas.style.height = data.world_height + 'px';
canvas.style.width = `${data.world_width}px`;
canvas.style.height = `${data.world_height}px`;
}
}
});
}
function updateScenePosFields(x, y, w, h) {
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');
const xEl = document.getElementById('scene-pos-x'); const
yEl = document.getElementById('scene-pos-y');
const wEl = document.getElementById('scene-pos-w'); const
hEl = document.getElementById('scene-pos-h');
if (xEl) xEl.value = Math.round(x);
if (yEl) yEl.value = Math.round(y);
if (wEl) wEl.value = Math.round(w);
@@ -279,13 +288,15 @@ function updateScenePosFields(x, y, w, h) {
// bouge plus (immobile près du bord) — mirror du comportement standard
// d'un glisser-déposer proche d'un bord de zone scrollable.
function forgeStartCanvasAutoScroll(getMouse, onNudge) {
var MARGIN = 40, SPEED = 14;
var timer = setInterval(function () {
var frame = document.querySelector('.canvasFrame');
const MARGIN = 40; const
SPEED = 14;
const timer = setInterval(() => {
const frame = document.querySelector('.canvasFrame');
if (!frame) return;
var rect = frame.getBoundingClientRect();
var mouse = getMouse();
var dx = 0, dy = 0;
const rect = frame.getBoundingClientRect();
const mouse = getMouse();
let dx = 0; let
dy = 0;
if (mouse.x > rect.right - MARGIN) dx = SPEED;
else if (mouse.x < rect.left + MARGIN) dx = -SPEED;
if (mouse.y > rect.bottom - MARGIN) dy = SPEED;
@@ -309,12 +320,16 @@ function forgeStartCanvasAutoScroll(getMouse, onNudge) {
function onSceneObjectMouseDown(e, id) {
if (e.target.classList.contains('resizeHandle')) return;
e.preventDefault();
var el = document.getElementById('el-' + id);
var startX = e.clientX, startY = e.clientY;
var lastClientX = e.clientX, lastClientY = e.clientY;
var curLeft = parseFloat(el.style.left) || 0, curTop = parseFloat(el.style.top) || 0;
var width = parseFloat(el.style.width) || 0, height = parseFloat(el.style.height) || 0;
var moved = false;
const el = document.getElementById(`el-${id}`);
const startX = e.clientX; const
startY = e.clientY;
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curLeft = parseFloat(el.style.left) || 0; let
curTop = parseFloat(el.style.top) || 0;
const width = parseFloat(el.style.width) || 0; const
height = parseFloat(el.style.height) || 0;
let moved = false;
// Les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" (voir static/style.css
// .floatPanel, z-index:60) restent DÉLIBÉRÉMENT en position:fixed
// PAR-DESSUS le canevas, pour maximiser l'espace utile — mais un objet
@@ -324,7 +339,7 @@ function onSceneObjectMouseDown(e, id) {
// le geste) : impossible de voir où on le dépose. Passé au-dessus des
// panneaux (z-index très élevé) le temps du glisser SEULEMENT, restauré
// à sa valeur d'origine à la fin.
var previousZIndex = el.style.zIndex;
const previousZIndex = el.style.zIndex;
// Bug corrigé ("toujours bloqué", confirmé lié à l'introduction du fond/
// caméra, voir add_scene_object.py) : un "fond" est posé à sa taille
@@ -338,15 +353,18 @@ function onSceneObjectMouseDown(e, id) {
// à (0,0), impossible à repositionner. min/max sont maintenant calculés
// dans le bon ordre quel que soit lequel des deux est le plus grand.
function applyDelta(dx, dy) {
var minLeft = Math.min(0, SCENE_WIDTH - width), maxLeft = Math.max(0, SCENE_WIDTH - width);
var minTop = Math.min(0, SCENE_HEIGHT - height), maxTop = Math.max(0, SCENE_HEIGHT - height);
const minLeft = Math.min(0, SCENE_WIDTH - width); const
maxLeft = Math.max(0, SCENE_WIDTH - width);
const minTop = Math.min(0, SCENE_HEIGHT - height); const
maxTop = Math.max(0, SCENE_HEIGHT - height);
curLeft = Math.max(minLeft, Math.min(maxLeft, curLeft + dx));
curTop = Math.max(minTop, Math.min(maxTop, curTop + dy));
el.style.left = curLeft + 'px';
el.style.top = curTop + 'px';
el.style.left = `${curLeft}px`;
el.style.top = `${curTop}px`;
}
function onMove(ev) {
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
const dx = ev.clientX - lastClientX; const
dy = ev.clientY - lastClientY;
lastClientX = ev.clientX; lastClientY = ev.clientY;
if (Math.abs(ev.clientX - startX) > 3 || Math.abs(ev.clientY - startY) > 3) {
if (!moved) el.style.zIndex = 9999;
@@ -354,9 +372,7 @@ function onSceneObjectMouseDown(e, id) {
}
applyDelta(dx, dy);
}
var stopAutoScroll = forgeStartCanvasAutoScroll(
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
);
const stopAutoScroll = forgeStartCanvasAutoScroll(() => ({ x: lastClientX, y: lastClientY }), applyDelta);
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
@@ -386,14 +402,17 @@ function onSceneObjectMouseDown(e, id) {
function onSceneObjectResizeMouseDown(e, id) {
e.stopPropagation();
e.preventDefault();
var el = document.getElementById('el-' + id);
var lastClientX = e.clientX, lastClientY = e.clientY;
var curWidth = parseFloat(el.style.width) || 0, curHeight = parseFloat(el.style.height) || 0;
var left = parseFloat(el.style.left) || 0, top = parseFloat(el.style.top) || 0;
const el = document.getElementById(`el-${id}`);
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curWidth = parseFloat(el.style.width) || 0; let
curHeight = parseFloat(el.style.height) || 0;
const left = parseFloat(el.style.left) || 0; const
top = parseFloat(el.style.top) || 0;
// Voir la même note dans onSceneObjectMouseDown ci-dessus : au-dessus
// des panneaux flottants le temps du geste, pour rester visible même
// en redimensionnant sous l'un d'eux.
var previousZIndex = el.style.zIndex;
const previousZIndex = el.style.zIndex;
el.style.zIndex = 9999;
// Pas de plafond à "SCENE_WIDTH - left" ici (bug corrigé) : un objet
@@ -405,23 +424,23 @@ function onSceneObjectResizeMouseDown(e, id) {
function applyDelta(dx, dy) {
curWidth = Math.max(8, curWidth + dx);
curHeight = Math.max(8, curHeight + dy);
el.style.width = curWidth + 'px';
el.style.height = curHeight + 'px';
el.style.width = `${curWidth}px`;
el.style.height = `${curHeight}px`;
}
function onMove(ev) {
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
const dx = ev.clientX - lastClientX; const
dy = ev.clientY - lastClientY;
lastClientX = ev.clientX; lastClientY = ev.clientY;
applyDelta(dx, dy);
}
var stopAutoScroll = forgeStartCanvasAutoScroll(
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
);
const stopAutoScroll = forgeStartCanvasAutoScroll(() => ({ x: lastClientX, y: lastClientY }), applyDelta);
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
stopAutoScroll();
el.style.zIndex = previousZIndex;
var newWidth = curWidth, newHeight = curHeight;
const newWidth = curWidth; const
newHeight = curHeight;
updateScenePosFields(left, top, newWidth, newHeight);
saveSceneObjectGeometry(id, left, top, newWidth, newHeight);
}
@@ -433,20 +452,24 @@ function onSceneObjectResizeMouseDown(e, id) {
// du comportement équivalent pour l'éditeur document (voir l'IIFE dans
// initBuilderPanel(), panel-init.js), mais ids/route distincts (pixels).
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');
const xEl = document.getElementById('scene-pos-x'); const
yEl = document.getElementById('scene-pos-y');
const wEl = document.getElementById('scene-pos-w'); const
hEl = document.getElementById('scene-pos-h');
if (!xEl || !wEl || !CURRENT_SELECTED_ID) return;
function apply() {
var canvasEl = document.getElementById('el-' + CURRENT_SELECTED_ID);
var x = parseFloat(xEl.value) || 0, y = parseFloat(yEl.value) || 0;
var w = parseFloat(wEl.value) || 1, h = parseFloat(hEl.value) || 1;
const canvasEl = document.getElementById(`el-${CURRENT_SELECTED_ID}`);
const x = parseFloat(xEl.value) || 0; const
y = parseFloat(yEl.value) || 0;
const w = parseFloat(wEl.value) || 1; const
h = parseFloat(hEl.value) || 1;
if (canvasEl) {
canvasEl.style.left = x + 'px'; canvasEl.style.top = y + 'px';
canvasEl.style.width = w + 'px'; canvasEl.style.height = h + 'px';
canvasEl.style.left = `${x}px`; canvasEl.style.top = `${y}px`;
canvasEl.style.width = `${w}px`; canvasEl.style.height = `${h}px`;
}
saveSceneObjectGeometry(CURRENT_SELECTED_ID, x, y, w, h);
}
[xEl, yEl, wEl, hEl].forEach(function (el) { el.addEventListener('change', apply); });
[xEl, yEl, wEl, hEl].forEach((el) => { el.addEventListener('change', apply); });
}
initScenePosFields();
@@ -458,18 +481,19 @@ initScenePosFields();
// preview.js, déjà générique : ne dépend que de data-element-id/
// data-personnage, présents sur un objet de scène comme sur un élément).
function swapSceneObjectCharacter(forgeCharacter) {
var body = document.getElementById('personnagePanelBody');
const body = document.getElementById('personnagePanelBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
document.querySelectorAll('#personnageGallerySwap .iconGalleryTile').forEach(function (tile) {
tile.classList.toggle('selected', tile.getAttribute('onclick') === "swapSceneObjectCharacter('" + forgeCharacter + "')");
const objectId = parseInt(body.dataset.elementId, 10);
document.querySelectorAll('#personnageGallerySwap .iconGalleryTile').forEach((tile) => {
tile.classList.toggle('selected', tile.getAttribute('onclick') === `swapSceneObjectCharacter('${forgeCharacter}')`);
});
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/personnage', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/personnage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: 'forge', forge_character: forgeCharacter }),
})
.then(function (r) { return r.json(); })
.then(function (json) {
.then((r) => r.json())
.then((json) => {
if (json.animations) {
ELEMENT_ANIMATIONS_MAP[objectId] = json.animations;
restartPersonnageIdlePreview(objectId, json.animations);
@@ -482,12 +506,13 @@ function swapSceneObjectCharacter(forgeCharacter) {
// que swapSceneObjectCharacter() ci-dessus (POST JSON, aucun rechargement
// de page), sur routes/scenes/scene_object_role.py.
function saveSceneObjectRole(role) {
var body = document.getElementById('personnagePanelBody');
const body = document.getElementById('personnagePanelBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/role', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: role }),
const objectId = parseInt(body.dataset.elementId, 10);
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/role`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role }),
});
}
@@ -500,37 +525,43 @@ function saveSceneObjectRole(role) {
// viennent d'être tapés à la main ou ajustés au glisser (voir
// onCollisionBoxMouseDown/onCollisionBoxResizeMouseDown plus bas).
function saveSceneObjectCollision() {
var body = document.getElementById('collisionPanelBody');
const body = document.getElementById('collisionPanelBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
var enabled = document.getElementById('collision-enabled').checked;
var shape = document.getElementById('collision-shape').value;
var width = parseFloat(document.getElementById('collision-width').value) || 0;
var height = parseFloat(document.getElementById('collision-height').value) || 0;
var offsetX = parseFloat(document.getElementById('collision-offset-x').value) || 0;
var offsetY = parseFloat(document.getElementById('collision-offset-y').value) || 0;
const objectId = parseInt(body.dataset.elementId, 10);
const enabled = document.getElementById('collision-enabled').checked;
const shape = document.getElementById('collision-shape').value;
const width = parseFloat(document.getElementById('collision-width').value) || 0;
const height = parseFloat(document.getElementById('collision-height').value) || 0;
const offsetX = parseFloat(document.getElementById('collision-offset-x').value) || 0;
const offsetY = parseFloat(document.getElementById('collision-offset-y').value) || 0;
var overlay = document.getElementById('collisionBoxOverlay');
var objEl = document.getElementById('el-' + objectId);
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${objectId}`);
if (overlay && objEl) {
var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0;
overlay.style.width = width + 'px';
overlay.style.height = height + 'px';
overlay.style.left = ((objWidth - width) / 2 + offsetX) + 'px';
overlay.style.top = ((objHeight - height) / 2 + offsetY) + 'px';
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
overlay.style.width = `${width}px`;
overlay.style.height = `${height}px`;
overlay.style.left = `${(objWidth - width) / 2 + offsetX}px`;
overlay.style.top = `${(objHeight - height) / 2 + offsetY}px`;
overlay.classList.toggle('is-circle', shape === 'cercle');
overlay.classList.toggle('is-disabled', !enabled);
}
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/collision', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled, shape: shape, width: width, height: height, offset_x: offsetX, offset_y: offsetY }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/collision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled, shape, width, height, offset_x: offsetX, offset_y: offsetY,
}),
});
}
function updateCollisionFields(width, height, offsetX, offsetY) {
var wEl = document.getElementById('collision-width'), hEl = document.getElementById('collision-height');
var oxEl = document.getElementById('collision-offset-x'), oyEl = document.getElementById('collision-offset-y');
const wEl = document.getElementById('collision-width'); const
hEl = document.getElementById('collision-height');
const oxEl = document.getElementById('collision-offset-x'); const
oyEl = document.getElementById('collision-offset-y');
if (wEl) wEl.value = Math.round(width);
if (hEl) hEl.value = Math.round(height);
if (oxEl) oxEl.value = Math.round(offsetX);
@@ -546,22 +577,26 @@ function onCollisionBoxMouseDown(e, id) {
e.stopPropagation();
if (e.target.classList.contains('collisionResizeHandle')) return;
e.preventDefault();
var overlay = document.getElementById('collisionBoxOverlay');
var objEl = document.getElementById('el-' + id);
var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0;
var startX = e.clientX, startY = e.clientY;
var startLeft = parseFloat(overlay.style.left) || 0, startTop = parseFloat(overlay.style.top) || 0;
var boxWidth = parseFloat(overlay.style.width) || 0, boxHeight = parseFloat(overlay.style.height) || 0;
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startLeft = parseFloat(overlay.style.left) || 0; const
startTop = parseFloat(overlay.style.top) || 0;
const boxWidth = parseFloat(overlay.style.width) || 0; const
boxHeight = parseFloat(overlay.style.height) || 0;
function onMove(ev) {
overlay.style.left = (startLeft + (ev.clientX - startX)) + 'px';
overlay.style.top = (startTop + (ev.clientY - startY)) + 'px';
overlay.style.left = `${startLeft + (ev.clientX - startX)}px`;
overlay.style.top = `${startTop + (ev.clientY - startY)}px`;
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
var offsetX = (parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2;
var offsetY = (parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2;
const offsetX = (parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2;
const offsetY = (parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2;
updateCollisionFields(boxWidth, boxHeight, offsetX, offsetY);
saveSceneObjectCollision();
}
@@ -577,23 +612,28 @@ function onCollisionBoxMouseDown(e, id) {
function onCollisionBoxResizeMouseDown(e, id) {
e.stopPropagation();
e.preventDefault();
var overlay = document.getElementById('collisionBoxOverlay');
var objEl = document.getElementById('el-' + id);
var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0;
var startX = e.clientX, startY = e.clientY;
var startWidth = parseFloat(overlay.style.width) || 0, startHeight = parseFloat(overlay.style.height) || 0;
var left = parseFloat(overlay.style.left) || 0, top = parseFloat(overlay.style.top) || 0;
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startWidth = parseFloat(overlay.style.width) || 0; const
startHeight = parseFloat(overlay.style.height) || 0;
const left = parseFloat(overlay.style.left) || 0; const
top = parseFloat(overlay.style.top) || 0;
function onMove(ev) {
overlay.style.width = Math.max(4, startWidth + (ev.clientX - startX)) + 'px';
overlay.style.height = Math.max(4, startHeight + (ev.clientY - startY)) + 'px';
overlay.style.width = `${Math.max(4, startWidth + (ev.clientX - startX))}px`;
overlay.style.height = `${Math.max(4, startHeight + (ev.clientY - startY))}px`;
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
var newWidth = parseFloat(overlay.style.width), newHeight = parseFloat(overlay.style.height);
var offsetX = left - (objWidth - newWidth) / 2;
var offsetY = top - (objHeight - newHeight) / 2;
const newWidth = parseFloat(overlay.style.width); const
newHeight = parseFloat(overlay.style.height);
const offsetX = left - (objWidth - newWidth) / 2;
const offsetY = top - (objHeight - newHeight) / 2;
updateCollisionFields(newWidth, newHeight, offsetX, offsetY);
saveSceneObjectCollision();
}
@@ -608,19 +648,20 @@ function onCollisionBoxResizeMouseDown(e, id) {
// ci-dessus (POST JSON vers routes/scenes/scene_object_commands.py,
// aucun rechargement de page).
function saveSceneObjectCommands() {
var body = document.getElementById('personnageCommandsBody');
const body = document.getElementById('personnageCommandsBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
var extra = [];
document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach(function (row) {
var toucheEl = row.querySelector('.personnageKeyCapture');
var animEl = row.querySelector('.cmdExtraAnim');
const objectId = parseInt(body.dataset.elementId, 10);
const extra = [];
document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach((row) => {
const toucheEl = row.querySelector('.personnageKeyCapture');
const animEl = row.querySelector('.cmdExtraAnim');
if (toucheEl && toucheEl.value && animEl && animEl.value) {
extra.push({ touche: toucheEl.value, animation: animEl.value });
}
});
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/commandes', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/commandes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
haut: document.getElementById('cmd-haut').value,
bas: document.getElementById('cmd-bas').value,
@@ -629,7 +670,7 @@ function saveSceneObjectCommands() {
interagir: document.getElementById('cmd-interagir').value,
axe: document.getElementById('cmd-axe').value,
vitesse: document.getElementById('cmd-vitesse').value,
extra: extra,
extra,
}),
});
}
@@ -645,8 +686,8 @@ function saveSceneObjectCommands() {
function bindPersonnageKeyCapture(el) {
if (!el || el.dataset.keyCaptureBound) return;
el.dataset.keyCaptureBound = '1';
el.addEventListener('focus', function () {
var previous = el.value;
el.addEventListener('focus', () => {
const previous = el.value;
el.value = 'Appuie sur une touche...';
el.classList.add('is-warning');
function onKey(ev) {
@@ -668,17 +709,14 @@ function bindPersonnageKeyCapture(el) {
// déjà connu de personnage-panel.js/personnage-preview.js) — jamais un nom
// tapé à la main, qui pourrait ne correspondre à aucune pose existante.
function personnageCommandExtraRowEl(objectId, touche, animation) {
var row = document.createElement('div');
const row = document.createElement('div');
row.className = 'field is-grouped personnageCommandExtraRow';
var animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {});
var options = animNames.map(function (name) {
return '<option value="' + name + '"' + (name === animation ? ' selected' : '') + '>' + name + '</option>';
}).join('');
row.innerHTML =
'<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="' + (touche || '').replace(/"/g, '&quot;') + '"></div>' +
'<div class="control"><div class="select is-small"><select class="cmdExtraAnim">' + options + '</select></div></div>' +
'<div class="control"><button type="button" class="button is-small" onclick="this.closest(\'.personnageCommandExtraRow\').remove(); saveSceneObjectCommands();">×</button></div>';
var keyInput = row.querySelector('.personnageKeyCapture');
const animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {});
const options = animNames.map((name) => `<option value="${name}"${name === animation ? ' selected' : ''}>${name}</option>`).join('');
row.innerHTML = `<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="${(touche || '').replace(/"/g, '&quot;')}"></div>`
+ `<div class="control"><div class="select is-small"><select class="cmdExtraAnim">${options}</select></div></div>`
+ '<div class="control"><button type="button" class="button is-small" onclick="this.closest(\'.personnageCommandExtraRow\').remove(); saveSceneObjectCommands();">×</button></div>';
const keyInput = row.querySelector('.personnageKeyCapture');
bindPersonnageKeyCapture(keyInput);
keyInput.addEventListener('change', saveSceneObjectCommands);
row.querySelector('.cmdExtraAnim').addEventListener('change', saveSceneObjectCommands);
@@ -686,10 +724,10 @@ function personnageCommandExtraRowEl(objectId, touche, animation) {
}
function addPersonnageCommandExtraRow() {
var body = document.getElementById('personnageCommandsBody');
var container = document.getElementById('personnageCommandsExtra');
const body = document.getElementById('personnageCommandsBody');
const container = document.getElementById('personnageCommandsExtra');
if (!body || !container) return;
var objectId = parseInt(body.dataset.elementId, 10);
const objectId = parseInt(body.dataset.elementId, 10);
container.appendChild(personnageCommandExtraRowEl(objectId, '', ''));
}
@@ -697,9 +735,10 @@ function addPersonnageCommandExtraRow() {
// personnage, réutilisé comme "qui parle" dans l'éditeur de dialogue d'un
// déclencheur (voir static/js/triggers/trigger-editor.js).
function saveSceneObjectName(objectId, name) {
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/name', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/name`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
}).then(refreshSceneObjectNames);
}
@@ -714,9 +753,9 @@ function saveSceneObjectName(objectId, name) {
// 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; });
fetch(`/game/${GAME_SLUG}/scene-object-names`)
.then((r) => r.json())
.then((json) => { SCENE_OBJECT_NAMES = json; });
}
// RGAA/WCAG 1.4.3 (contraste minimum) : les couleurs du "🎨 Style"
@@ -727,16 +766,16 @@ function refreshSceneObjectNames() {
// son choix esthétique, ex. un jeu volontairement sombre/stylisé).
function forgeHexToRgb(hex) {
hex = String(hex || '').replace('#', '');
if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');
var num = parseInt(hex, 16);
if (isNaN(num) || hex.length !== 6) return [0, 0, 0];
if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');
const num = parseInt(hex, 16);
if (Number.isNaN(num) || hex.length !== 6) return [0, 0, 0];
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
}
function forgeRelativeLuminance(rgb) {
var srgb = rgb.map(function (c) {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const srgb = rgb.map((c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}
@@ -744,23 +783,24 @@ function forgeRelativeLuminance(rgb) {
// Formule de contraste WCAG standard (relative luminance) — 4.5:1 est le
// seuil "AA" pour du texte normal (RGAA 3.2/critère 1.4.3).
function forgeContrastRatio(hex1, hex2) {
var l1 = forgeRelativeLuminance(forgeHexToRgb(hex1));
var l2 = forgeRelativeLuminance(forgeHexToRgb(hex2));
var lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
const l1 = forgeRelativeLuminance(forgeHexToRgb(hex1));
const l2 = forgeRelativeLuminance(forgeHexToRgb(hex2));
const lighter = Math.max(l1, l2); const
darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function checkDialogueBoxContrast(style) {
var warningEl = document.getElementById('dboxContrastWarning');
const warningEl = document.getElementById('dboxContrastWarning');
if (!warningEl) return;
var ratios = [
const ratios = [
forgeContrastRatio(style.text_color, style.header_bg),
forgeContrastRatio(style.text_color, style.body_bg),
forgeContrastRatio(style.text_color, style.footer_bg),
];
var minRatio = Math.min.apply(null, ratios);
const minRatio = Math.min.apply(null, ratios);
warningEl.textContent = minRatio < 4.5
? '⚠️ Contraste insuffisant (' + minRatio.toFixed(1) + ':1, minimum recommandé 4.5:1) — le texte risque d\'être difficile à lire.'
? `⚠️ Contraste insuffisant (${minRatio.toFixed(1)}:1, minimum recommandé 4.5:1) — le texte risque d'être difficile à lire.`
: '';
}
@@ -769,7 +809,7 @@ function checkDialogueBoxContrast(style) {
// convention "replace-all" que saveSceneObjectCollision), jamais un seul
// champ isolé.
function saveDialogueBoxStyle(objectId) {
var style = {
const style = {
font_family: document.getElementById('dbox-font-family').value,
font_size: Number(document.getElementById('dbox-font-size').value),
font_weight: document.getElementById('dbox-font-weight').value,
@@ -779,20 +819,21 @@ function saveDialogueBoxStyle(objectId) {
footer_bg: document.getElementById('dbox-footer-bg').value,
};
checkDialogueBoxContrast(style);
var el = document.getElementById('el-' + objectId);
var widget = el && el.querySelector('.dialogueBoxWidget');
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.dialogueBoxWidget');
if (widget) {
widget.style.fontFamily = style.font_family;
widget.style.fontSize = style.font_size + 'px';
widget.style.fontSize = `${style.font_size}px`;
widget.style.fontWeight = style.font_weight;
widget.style.color = style.text_color;
var header = widget.querySelector('.dialogueBoxHeader'); if (header) header.style.background = style.header_bg;
var b = widget.querySelector('.dialogueBoxBody'); if (b) b.style.background = style.body_bg;
var footer = widget.querySelector('.dialogueBoxFooter'); if (footer) footer.style.background = style.footer_bg;
const header = widget.querySelector('.dialogueBoxHeader'); if (header) header.style.background = style.header_bg;
const b = widget.querySelector('.dialogueBoxBody'); if (b) b.style.background = style.body_bg;
const footer = widget.querySelector('.dialogueBoxFooter'); if (footer) footer.style.background = style.footer_bg;
}
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/dialogue-box-style', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ style: style }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/dialogue-box-style`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ style }),
});
}
@@ -804,11 +845,11 @@ function saveDialogueBoxStyle(objectId) {
// grille (data-selected-template) puis sauvegarde tout de suite (même
// geste qu'un clic sur une tuile de personnage/décor).
function _selectQuizBoxTemplate(objectId, key, gridId) {
var grid = document.getElementById(gridId);
const grid = document.getElementById(gridId);
if (grid) {
grid.setAttribute('data-selected-template', key);
var tiles = grid.querySelectorAll('.quizTemplateTile');
for (var i = 0; i < tiles.length; i++) {
const tiles = grid.querySelectorAll('.quizTemplateTile');
for (let i = 0; i < tiles.length; i++) {
tiles[i].classList.toggle('is-selected', tiles[i].getAttribute('data-template') === key);
}
}
@@ -841,47 +882,48 @@ function selectQuizBoxPageTemplate(objectId, key) { _selectQuizBoxTemplate(objec
// galerie de modèles à afficher change entièrement (boîte <-> page) :
// un simple patch DOM ne pourrait recalculer ni l'un ni l'autre ici.
function saveQuizBoxConfig(objectId) {
var meta = document.getElementById('quizBoxTemplateMeta');
const meta = document.getElementById('quizBoxTemplateMeta');
function currentTemplate(gridId, metaAttr) {
var grid = document.getElementById(gridId);
const grid = document.getElementById(gridId);
if (!grid) return meta ? meta.getAttribute(metaAttr) : null;
var selectedTile = grid.querySelector('.quizTemplateTile.is-selected');
const selectedTile = grid.querySelector('.quizTemplateTile.is-selected');
return grid.getAttribute('data-selected-template') || (selectedTile && selectedTile.getAttribute('data-template'));
}
var fullscreenEl = document.getElementById('quiz-fullscreen');
var fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.getAttribute('data-was-checked') === '1');
var config = {
const fullscreenEl = document.getElementById('quiz-fullscreen');
const fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.getAttribute('data-was-checked') === '1');
const config = {
fullscreen: fullscreenEl.checked,
timer_mode: document.getElementById('quiz-timer-mode').value,
timer_seconds: Number(document.getElementById('quiz-timer-seconds').value),
dialog_template: currentTemplate('quizDialogTemplateGrid', 'data-dialog-template') || 'defaut',
page_template: currentTemplate('quizPageTemplateGrid', 'data-page-template') || 'manga',
};
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/quiz-config', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: config }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/quiz-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
.then((r) => r.json())
.then((data) => {
if (fullscreenChanged) { window.location.reload(); return; }
var el = document.getElementById('el-' + objectId);
var widget = el && el.querySelector('.quizBoxWidget');
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.quizBoxWidget');
if (widget && data.rendered_html) widget.outerHTML = data.rendered_html;
});
}
function initPersonnageCommandsPanel() {
var body = document.getElementById('personnageCommandsBody');
const body = document.getElementById('personnageCommandsBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach(function (id) {
const objectId = parseInt(body.dataset.elementId, 10);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach((id) => {
bindPersonnageKeyCapture(document.getElementById(id));
});
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach(function (id) {
var el = document.getElementById(id);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach((id) => {
const el = document.getElementById(id);
if (el) el.addEventListener('change', saveSceneObjectCommands);
});
var container = document.getElementById('personnageCommandsExtra');
const container = document.getElementById('personnageCommandsExtra');
// 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
@@ -890,9 +932,9 @@ function initPersonnageCommandsPanel() {
// 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 = [];
let extra = [];
try { extra = JSON.parse(body.dataset.commandsExtra || '[]'); } catch (e) { extra = []; }
extra.forEach(function (e) {
extra.forEach((e) => {
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
});
}