Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance, video, son, visibilite, indication, attendre comme actions, utilisables aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par Ruby (IA), avec blocs deplacables/supprimables dans une chaine. - Corrige plusieurs variantes du bug "impossible de poser un objet hors du champ de la camera" (troncature du chainage d'actions a 4 maillons, fond importe pose a 128x128 au lieu de sa taille reelle, decalage du fond au vrai glisser-depose, redimensionnement manuel jamais propage au monde). - Ajoute un vrai glisser-depose depuis la galerie vers la scene, la gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/ Videos, suppression, reclassement fond<->decor sans re-upload). - Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement, limite 5 min), avec validation de la duree reelle du fichier, et une replique audio optionnelle dans une bulle de dialogue. - Fixe la taille de pose d'un objet/decor importe a 200x200 avec une boite de collision de 150x150. - Filtre le selecteur de fichier des actions son/video par type reel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b07b231a61
commit
559331f9cf
@@ -0,0 +1,196 @@
|
||||
// Assistant "déclencheur -> action" (voir static/js/scenes/
|
||||
// collision-rules-editor.js, static/js/triggers/trigger-editor.js) — ce
|
||||
// fichier n'a pas de module.exports (script de navigateur, pas un module
|
||||
// CommonJS) : on l'exécute via vm.runInThisContext (comme une vraie
|
||||
// balise <script>, les déclarations top-level deviennent globales) plutôt
|
||||
// que require(), qui les aurait gardées privées au module.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
function loadAsScript(relativePath) {
|
||||
const fullPath = path.join(__dirname, relativePath);
|
||||
vm.runInThisContext(fs.readFileSync(fullPath, 'utf8'), { filename: fullPath });
|
||||
}
|
||||
|
||||
function makeEl() {
|
||||
const el = {
|
||||
_html: '', style: {}, value: '',
|
||||
classList: { add() {}, remove() {}, toggle() {} },
|
||||
children: [],
|
||||
appendChild(c) { this.children.push(c); },
|
||||
querySelector() { return null; },
|
||||
setAttribute() {}, getAttribute() { return null; },
|
||||
addEventListener() {}, removeEventListener() {},
|
||||
};
|
||||
Object.defineProperty(el, 'innerHTML', {
|
||||
get() { return this._html; },
|
||||
set(v) { this._html = v; },
|
||||
});
|
||||
return el;
|
||||
}
|
||||
|
||||
function setupGlobals() {
|
||||
global.window = global.window || {};
|
||||
global.GAME_SLUG = 'pytest_wizard';
|
||||
global.CURRENT_SCREEN_ID = 7;
|
||||
global.SCENE_OBJECT_NAMES_JSON = [];
|
||||
global.GLOBAL_VARIABLE_NAMES = [];
|
||||
global.DATA_OPERATION_LABELS_MAP = { incrementer: 'Incrémenter' };
|
||||
global.CONDITION_OPERATOR_LABELS_MAP = { egal: 'égal à' };
|
||||
global.SURBRILLANCE_LABELS_MAP = { on: 'Activer' };
|
||||
global.ELEMENT_VISIBILITY_LABELS_MAP = { visible: 'Visible' };
|
||||
global.VIDEO_MODE_LABELS_MAP = { plein_ecran: 'Plein écran' };
|
||||
global.USER_ASSETS_OPTIONS = [];
|
||||
|
||||
const elementsById = {};
|
||||
global.document = {
|
||||
createElement() { return makeEl(); },
|
||||
getElementById(id) { return elementsById[id] || (elementsById[id] = makeEl()); },
|
||||
querySelector() { return null; },
|
||||
body: makeEl(),
|
||||
};
|
||||
|
||||
const posted = [];
|
||||
global.fetch = function (url, opts) {
|
||||
if (opts && opts.method === 'POST') posted.push({ url, body: opts.body ? JSON.parse(opts.body) : null });
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve([]) });
|
||||
};
|
||||
return posted;
|
||||
}
|
||||
|
||||
// setupGlobals() DOIT être appelé avant de charger les scripts —
|
||||
// trigger-editor.js exécute initTriggersTab() à son propre chargement
|
||||
// (voir son commentaire "Point d'entrée"), qui a besoin de `document`.
|
||||
// Les scripts eux-mêmes ne sont chargés qu'UNE FOIS (déclarations
|
||||
// top-level -> globales, voir loadAsScript) ; chaque test rappelle
|
||||
// setupGlobals() seulement pour repartir d'un DOM/fetch factices propres,
|
||||
// jamais pour recharger les scripts.
|
||||
setupGlobals();
|
||||
loadAsScript('../collision-rules-editor.js');
|
||||
loadAsScript('../../triggers/trigger-editor.js');
|
||||
|
||||
test('assistant — une chaîne "attendre -> puis dialogue" sauvegarde bien LES DEUX, dans le bon ordre', () => {
|
||||
// Bug corrigé : "Rien de plus" renvoyait la DERNIÈRE feuille ajoutée
|
||||
// (_collisionWizard.chainLeaf, réassignée à chaque étape) plutôt que la
|
||||
// PREMIÈRE — une chaîne de 2 actions ou plus perdait silencieusement
|
||||
// tout sauf la toute dernière avant sauvegarde.
|
||||
const posted = setupGlobals();
|
||||
const body = document.getElementById('questTriggerModalBody');
|
||||
|
||||
global.openScreenTriggerWizard(7, body);
|
||||
global.collisionWizardChooseTrigger('affichage');
|
||||
global.collisionWizardChooseAction('attendre', false);
|
||||
document.getElementById('attendreWizSecondes').value = '2';
|
||||
global.collisionWizardAttendreNext();
|
||||
global.collisionWizardChainDone('dialogue');
|
||||
global.collisionWizardChainDone('none');
|
||||
|
||||
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;
|
||||
assert.equal(action.type, 'attendre');
|
||||
assert.equal(action.data_value, '2');
|
||||
assert.equal(action.then.type, 'dialogue');
|
||||
});
|
||||
|
||||
test('assistant — une chaîne de 3 actions ("variable" -> "attendre" -> "dialogue") garde la tête et l\'ordre', () => {
|
||||
const posted = setupGlobals();
|
||||
const body = document.getElementById('questTriggerModalBody');
|
||||
|
||||
global.openScreenTriggerWizard(7, body);
|
||||
global.collisionWizardChooseTrigger('affichage');
|
||||
global.collisionWizardChooseAction('variable', false);
|
||||
document.getElementById('varWizVariable').value = 'score';
|
||||
document.getElementById('varWizOperation').value = 'incrementer';
|
||||
document.getElementById('varWizValue').value = '1';
|
||||
global.collisionWizardVariableNext();
|
||||
global.collisionWizardChainDone('attendre');
|
||||
document.getElementById('attendreWizSecondes').value = '1.5';
|
||||
global.collisionWizardAttendreNext();
|
||||
global.collisionWizardChainDone('dialogue');
|
||||
global.collisionWizardChainDone('none');
|
||||
|
||||
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
|
||||
const action = saveCall.body.rule.action;
|
||||
assert.equal(action.type, 'variable');
|
||||
assert.equal(action.then.type, 'attendre');
|
||||
assert.equal(action.then.data_value, '1.5');
|
||||
assert.equal(action.then.then.type, 'dialogue');
|
||||
});
|
||||
|
||||
test('assistant — une seule action ("Rien de plus" tout de suite) sauvegarde bien cette action seule', () => {
|
||||
const posted = setupGlobals();
|
||||
const body = document.getElementById('questTriggerModalBody');
|
||||
|
||||
global.openScreenTriggerWizard(7, body);
|
||||
global.collisionWizardChooseTrigger('affichage');
|
||||
global.collisionWizardChooseAction('dialogue', false);
|
||||
global.collisionWizardChainDone('none');
|
||||
|
||||
const saveCall = posted.find((p) => p.url === '/game/pytest_wizard/screens/7/triggers');
|
||||
const action = saveCall.body.rule.action;
|
||||
assert.equal(action.type, 'dialogue');
|
||||
assert.equal(action.then, undefined);
|
||||
});
|
||||
|
||||
test('assistant — l\'action "vidéo" ne propose QUE les fichiers vidéo (jamais les images/sons)', () => {
|
||||
// Bug corrigé : "toute une liste de png et mp3" pour un déclencheur
|
||||
// vidéo — renderCollisionWizardAssetStep listait TOUJOURS
|
||||
// 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' },
|
||||
];
|
||||
const body = document.getElementById('questTriggerModalBody');
|
||||
|
||||
global.openScreenTriggerWizard(7, body);
|
||||
global.collisionWizardChooseTrigger('affichage');
|
||||
global.collisionWizardChooseAction('video', false);
|
||||
|
||||
const html = body.innerHTML;
|
||||
assert.ok(html.includes('clip.mp4'), 'le fichier vidéo doit apparaître');
|
||||
assert.ok(!html.includes('photo.png'), 'une image ne doit jamais apparaître pour une action vidéo');
|
||||
assert.ok(!html.includes('bip.mp3'), 'un son ne doit jamais apparaître pour une action vidéo');
|
||||
});
|
||||
|
||||
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' },
|
||||
];
|
||||
const body = document.getElementById('questTriggerModalBody');
|
||||
|
||||
global.openScreenTriggerWizard(7, body);
|
||||
global.collisionWizardChooseTrigger('affichage');
|
||||
global.collisionWizardChooseAction('son', false);
|
||||
|
||||
const html = body.innerHTML;
|
||||
assert.ok(html.includes('bip.mp3'), 'le fichier son doit apparaître');
|
||||
assert.ok(!html.includes('photo.png'), 'une image ne doit jamais apparaître pour une action son');
|
||||
assert.ok(!html.includes('clip.mp4'), 'une vidéo ne doit jamais apparaître pour une action son');
|
||||
});
|
||||
|
||||
test('triggerActionChainHtml — un bouton "+ Ajouter une action" apparaît après CHAQUE bloc, pas seulement le dernier', () => {
|
||||
// Demande explicite : "pouvoir ajouter n'importe quelle action à
|
||||
// n'importe quel niveau du schéma" — chaque bloc doit pouvoir insérer
|
||||
// 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: 'attendre', id: 'a_1', data_value: '2' },
|
||||
{ type: 'dialogue', dialogue: { id: 'd_1', lines: [] } },
|
||||
];
|
||||
const html = global.triggerActionChainHtml(chain, 'screen', null);
|
||||
const matches = html.match(/\+ Ajouter une action/g) || [];
|
||||
assert.equal(matches.length, 3);
|
||||
assert.ok(html.includes("screenTriggerOpenAppendActionModal('v_1')"));
|
||||
assert.ok(html.includes("screenTriggerOpenAppendActionModal('a_1')"));
|
||||
assert.ok(html.includes("screenTriggerOpenAppendActionModal('d_1')"));
|
||||
});
|
||||
Reference in New Issue
Block a user