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
@@ -19,6 +19,7 @@ global.forgeApplyVariableActionOffline = global.forgeApplyVariableActionOffline
|
||||
const {
|
||||
forgeCollisionRuleTriggerActive, forgeRunCollisionRuleAction, forgeCollisionRuleTick,
|
||||
forgeStartCollisionRuleControllers, forgeStopCollisionRuleControllers,
|
||||
forgeBindClicSurvolRules, forgeUnbindClicSurvolRules,
|
||||
} = require('../collision-rules-controller.js');
|
||||
|
||||
// La bulle "Appuie sur X" est ajoutée comme SŒUR de l'objet dans son
|
||||
@@ -179,6 +180,137 @@ test('forgeRunCollisionRuleAction — "dialogue" transmet son "then" à forgeSho
|
||||
assert.deepEqual(calls, [['d_1', 'Crokine', [], undefined, thenAction]]);
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "surbrillance" bascule .forgeHighlight sur l\'objet CIBLÉ (object_id), pas forcément l\'objet du déclencheur', () => {
|
||||
const targetEl = fakeEl();
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: sel => (sel.includes('"9"') ? targetEl : undefined) };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({ type: 'surbrillance', object_id: 9, valeur: 'on' }, fakeEl(), { id: 1, name: 'Interrupteur' });
|
||||
assert.equal(targetEl.classList.contains('forgeHighlight'), true);
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "visibilite" masque l\'objet CIBLÉ', () => {
|
||||
const targetEl = fakeEl();
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({ type: 'visibilite', object_id: 4, valeur: 'masque' }, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(targetEl.style.display, 'none');
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "surbrillance" sans objet trouvé (référence orpheline) ne plante pas et exécute quand même "then"', () => {
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => undefined };
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'surbrillance', object_id: 999, valeur: 'on',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "son" joue l\'URL fournie via Audio, instantané (then exécuté tout de suite)', () => {
|
||||
const previousAudio = global.Audio;
|
||||
const played = [];
|
||||
global.Audio = function (url) {
|
||||
played.push(url);
|
||||
return { play: () => Promise.resolve() };
|
||||
};
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'son', asset_url: '/assets/1/bip.mp3',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.deepEqual(played, ['/assets/1/bip.mp3']);
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
} finally { global.Audio = previousAudio; }
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "video" attend forgeShowVideoOverlay avant d\'exécuter "then"', () => {
|
||||
const previousShow = global.forgeShowVideoOverlay;
|
||||
let capturedDone;
|
||||
global.forgeShowVideoOverlay = (url, mode, onDone) => { capturedDone = onDone; };
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'video', asset_url: '/assets/1/intro.mp4', mode: 'plein_ecran',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '0'); // pas encore, la vidéo "joue"
|
||||
capturedDone();
|
||||
assert.equal(gameData.variables.score.value, '1'); // maintenant, la vidéo est "finie"
|
||||
} finally { global.forgeShowVideoOverlay = previousShow; }
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "attendre" suspend "then" jusqu\'à la fin du délai (data_value en secondes)', async () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
forgeRunCollisionRuleAction({
|
||||
type: 'attendre', data_value: '0',
|
||||
then: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}, fakeEl(), { id: 1, name: 'x' });
|
||||
assert.equal(gameData.variables.score.value, '0'); // pas encore, setTimeout pas encore résolu
|
||||
await new Promise(function (resolve) { setTimeout(resolve, 10); });
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
});
|
||||
|
||||
test('forgeRunCollisionRuleAction — "indication" affiche une bulle temporisée près de l\'objet CIBLÉ', () => {
|
||||
const parentEl = fakeParentEl();
|
||||
const targetEl = fakeEl(parentEl);
|
||||
const previousDoc = global.document;
|
||||
global.document = fakeDocument({ 4: targetEl });
|
||||
try {
|
||||
forgeRunCollisionRuleAction({ type: 'indication', object_id: 4, texte: 'Regarde ici !' }, fakeEl(), { id: 1, name: 'x' });
|
||||
const bubble = parentEl.querySelector('.collisionIndicationBubble[data-for-object-id="4"]');
|
||||
assert.equal(bubble.textContent, 'Regarde ici !');
|
||||
assert.equal(bubble.style.display, 'block');
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — "clic" exécute l\'action au clic, sans joueur ni tick', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
try {
|
||||
forgeBindClicSurvolRules({
|
||||
elements: [{
|
||||
id: 5, collision_rules: [{
|
||||
trigger: 'clic',
|
||||
action: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}],
|
||||
}],
|
||||
});
|
||||
targetEl._handlers.click();
|
||||
assert.equal(gameData.variables.score.value, '1');
|
||||
} finally { global.document = previousDoc; forgeUnbindClicSurvolRules(); }
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — "survol" se lie sur mouseenter, jamais sur click', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
try {
|
||||
forgeBindClicSurvolRules({ elements: [{ id: 6, collision_rules: [{ trigger: 'survol', action: { type: 'dialogue' } }] }] });
|
||||
assert.equal(typeof targetEl._handlers.mouseenter, 'function');
|
||||
assert.equal(targetEl._handlers.click, undefined);
|
||||
} finally { global.document = previousDoc; forgeUnbindClicSurvolRules(); }
|
||||
});
|
||||
|
||||
test('forgeBindClicSurvolRules — un déclencheur "collision" n\'est jamais lié en écouteur DOM (reste géré par le tick)', () => {
|
||||
const targetEl = { addEventListener(event, handler) { this._handlers = this._handlers || {}; this._handlers[event] = handler; }, removeEventListener() {} };
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => targetEl };
|
||||
try {
|
||||
forgeBindClicSurvolRules({ elements: [{ id: 7, collision_rules: [{ trigger: 'collision', action: { type: 'dialogue' } }] }] });
|
||||
assert.equal(targetEl._handlers, undefined);
|
||||
} finally { global.document = previousDoc; forgeUnbindClicSurvolRules(); }
|
||||
});
|
||||
|
||||
test('tick — la collision entre le joueur et un objet ne redéclenche pas "dialogue" tant que le contact persiste', () => {
|
||||
const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } };
|
||||
const npc = { id: 2, kind: 'personnage', name: 'Crokine', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue', dialogue: { id: 'd_7', lines: [] } } }] };
|
||||
@@ -261,6 +393,14 @@ test('tick — "interagir" affiche la bulle "Appuie sur [touche]" pendant la col
|
||||
});
|
||||
|
||||
test('forgeStartCollisionRuleControllers — inactif sans joueur ou sans règle (aucun intervalle démarré)', () => {
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', personnage_role: 'joueur' }] }));
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue' } }] }] }));
|
||||
// "clic"/"survol" sont liés inconditionnellement (forgeBindClicSurvolRules,
|
||||
// voir plus bas) — jamais de joueur requis pour EUX, donc document.querySelector
|
||||
// est bien appelé même dans ces deux cas où le tick de collision, lui, ne
|
||||
// démarre pas ; un document minimal (aucun élément trouvé) suffit ici.
|
||||
const previousDoc = global.document;
|
||||
global.document = { querySelector: () => undefined };
|
||||
try {
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', personnage_role: 'joueur' }] }));
|
||||
assert.doesNotThrow(() => forgeStartCollisionRuleControllers({ kind: 'jeu_2d', elements: [{ kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'dialogue' } }] }] }));
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
@@ -103,6 +103,54 @@ test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les ré
|
||||
} finally { global.document = previousDoc; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — une réplique avec audio_url joue la voix off dès son affichage', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
const previousAudio = global.Audio;
|
||||
const played = [];
|
||||
global.document = { querySelector: () => widget };
|
||||
global.Audio = function (url) {
|
||||
played.push(url);
|
||||
return { play: () => Promise.resolve(), pause: () => {} };
|
||||
};
|
||||
try {
|
||||
forgeShowDialogueBox('d_9', 'Marc', [{ speaker: 'Marc', text: 'Bonjour !', audio_url: '/assets/1/bonjour.mp3' }]);
|
||||
assert.deepEqual(played, ['/assets/1/bonjour.mp3']);
|
||||
} finally { global.document = previousDoc; global.Audio = previousAudio; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox — une réplique SANS audio_url ne tente jamais de jouer un son', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
const previousAudio = global.Audio;
|
||||
let called = false;
|
||||
global.document = { querySelector: () => widget };
|
||||
global.Audio = function () { called = true; return { play: () => Promise.resolve(), pause: () => {} }; };
|
||||
try {
|
||||
forgeShowDialogueBox('d_9', 'Marc', [{ speaker: 'Marc', text: 'Bonjour !' }]);
|
||||
assert.equal(called, false);
|
||||
} finally { global.document = previousDoc; global.Audio = previousAudio; }
|
||||
});
|
||||
|
||||
test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — la voix off de la réplique précédente s\'arrête sur la suivante', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
const previousAudio = global.Audio;
|
||||
const paused = [];
|
||||
global.document = { querySelector: () => widget };
|
||||
global.Audio = function (url) {
|
||||
return { url: url, play: () => Promise.resolve(), pause: function () { paused.push(this.url); } };
|
||||
};
|
||||
try {
|
||||
forgeShowDialogueBox('d_9', 'Marc', [
|
||||
{ speaker: 'Marc', text: 'Bonjour !', audio_url: '/assets/1/a.mp3' },
|
||||
{ speaker: 'Marc', text: 'Ça va ?', audio_url: '/assets/1/b.mp3' },
|
||||
]);
|
||||
forgeDialogueBoxAdvance(42);
|
||||
assert.deepEqual(paused, ['/assets/1/a.mp3']);
|
||||
} finally { global.document = previousDoc; global.Audio = previousAudio; }
|
||||
});
|
||||
|
||||
test('fin de dialogue SANS mark_completed : le dialogue n\'est jamais marqué terminé', () => {
|
||||
const widget = fakeWidget(42);
|
||||
const previousDoc = global.document;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Déclencheurs D'ÉCRAN "à l'affichage" (voir
|
||||
// static/js/play/screen-triggers-controller.js, screens/rendering/
|
||||
// screen_triggers.py) — SANS objet requis, narration/cinématique.
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
global.window = global.window || {};
|
||||
global.gameData = global.gameData || {};
|
||||
|
||||
const actionsMod = require('../actions.js');
|
||||
Object.assign(global, actionsMod);
|
||||
require('../personnage-controller.js');
|
||||
global.forgeApplyVariableActionOffline = global.forgeApplyVariableActionOffline || require('../offline/apply-actions.js').forgeApplyVariableActionOffline;
|
||||
global.forgeRunCollisionRuleAction = require('../collision-rules-controller.js').forgeRunCollisionRuleAction;
|
||||
|
||||
const { forgeRunScreenTriggers } = require('../screen-triggers-controller.js');
|
||||
|
||||
test('forgeRunScreenTriggers — ignore un écran sans screen_triggers', () => {
|
||||
assert.doesNotThrow(() => forgeRunScreenTriggers({ id: 1 }));
|
||||
assert.doesNotThrow(() => forgeRunScreenTriggers(null));
|
||||
});
|
||||
|
||||
test('forgeRunScreenTriggers — exécute une action "variable" pour un déclencheur "affichage"', () => {
|
||||
global.gameData = { variables: { intro_vue: { type: 'booleen', value: 'faux' } } };
|
||||
forgeRunScreenTriggers({
|
||||
id: 1,
|
||||
screen_triggers: [{
|
||||
trigger: 'affichage',
|
||||
action: { type: 'variable', target_variable: 'intro_vue', data_operation: 'definir_bool_vrai' },
|
||||
}],
|
||||
});
|
||||
assert.equal(gameData.variables.intro_vue.value, '1');
|
||||
});
|
||||
|
||||
test('forgeRunScreenTriggers — ignore tout déclencheur autre que "affichage" (jamais posé en pratique, garde-fou)', () => {
|
||||
global.gameData = { variables: { score: { type: 'nombre_entier', value: '0' } } };
|
||||
forgeRunScreenTriggers({
|
||||
id: 1,
|
||||
screen_triggers: [{
|
||||
trigger: 'collision',
|
||||
action: { type: 'variable', target_variable: 'score', data_operation: 'incrementer', data_value: '1' },
|
||||
}],
|
||||
});
|
||||
assert.equal(gameData.variables.score.value, '0');
|
||||
});
|
||||
Reference in New Issue
Block a user