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:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
@@ -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: [] } },
|
||||
];
|
||||
|
||||
@@ -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, '<') + '</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, '<')}</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, '<') + '</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, '<')}</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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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, '"') + '"></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, '"')}"></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));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user