Quêtes/collision : personnages nommés, dialogues qui parlent, boîte de dialogue en jeu
Build and deploy / test-python (push) Successful in 11m32s
Build and deploy / test-js (push) Successful in 1m12s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- "ℹ️ Informations" (propriétés d'un personnage) : nom éditable, réutilisé
  comme "qui parle" dans l'éditeur de dialogue de quête (menu déroulant,
  plus "Joueur" toujours disponible) — remplace l'ancien choix binaire
  joueur/pnj. db.sanitize_quest_dialogues accepte maintenant un nom libre.

- Nouveau menu "🖥️ Interface" (palette d'objets) avec le premier widget :
  "💬 Boîte de dialogue" (kind="dialogue_box", screens/rendering/
  dialogue_box_style.py) — position/taille comme tout objet de scène,
  panneau "🎨 Style" dédié (police, taille, épaisseur, couleur du texte,
  couleur header/body/footer). Rendu en <div> à 3 zones, jamais soumis à
  la collision ni à l'éditeur de collision (exclu partout : obstacles,
  liste des règles, payload). Rendu côté jeu dans .sceneUI, une couche
  FIXE au viewport (jamais .sceneWorld, qui défile avec la caméra).

- static/js/play/dialogue-box-controller.js : fait le lien moteur entre
  l'action "quete" de l'éditeur de collision et ce widget — affiche la
  réplique en cours du dialogue correspondant au STATUT ACTUEL de la
  quête (gameData.quests, désormais exposé game-wide par
  full_game_payload.py), avance au clic sur "Suivant" (header = qui
  parle, body = texte, footer = bouton), se masque à la dernière réplique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-03 11:20:58 +02:00
co-authored by Claude Sonnet 5
parent 390ee831ca
commit 2347a70c45
27 changed files with 774 additions and 41 deletions
@@ -71,10 +71,13 @@ test('forgeRunCollisionRuleAction — "evenement" appelle window.dispatchGameEve
assert.deepEqual(calls, [42]);
});
test('forgeRunCollisionRuleAction — "quete" mémorise l\'id dans window.forgeQuestLog (pas d\'éditeur de quête pour le moment)', () => {
window.forgeQuestLog = [];
forgeRunCollisionRuleAction({ type: 'quete', quete_id: 'q-1' }, fakeEl(), { id: 1 });
assert.deepEqual(window.forgeQuestLog, ['q-1']);
test('forgeRunCollisionRuleAction — "quete" délègue à forgeShowDialogueBox (voir dialogue-box-controller.js)', () => {
const calls = [];
const previousShow = global.forgeShowDialogueBox;
global.forgeShowDialogueBox = id => calls.push(id);
forgeRunCollisionRuleAction({ type: 'quete', quete_id: 42 }, fakeEl(), { id: 1 });
global.forgeShowDialogueBox = previousShow;
assert.deepEqual(calls, [42]);
});
test('tick — la collision entre le joueur et un ennemi déclenche "attaque" une seule fois tant que le contact persiste', () => {
@@ -0,0 +1,98 @@
// Widget "💬 Boîte de dialogue" — moteur d'exécution (voir
// static/js/play/dialogue-box-controller.js) : affiche la réplique en
// cours du dialogue d'une quête (gameData.quests) correspondant à son
// STATUT ACTUEL, avance au clic sur "Suivant", se masque à la fin.
const test = require('node:test');
const assert = require('node:assert/strict');
global.window = global.window || {};
global.gameData = {};
const {
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
} = require('../dialogue-box-controller.js');
function fakeWidget(objectId) {
const roles = {
header: { textContent: '' },
body: { textContent: '' },
};
const classes = new Set();
return {
getAttribute: attr => (attr === 'data-object-id' ? String(objectId) : null),
classList: { add: c => classes.add(c), remove: c => classes.delete(c), contains: c => classes.has(c) },
querySelector: sel => {
if (sel === '[data-dialogue-role="header"]') return roles.header;
if (sel === '[data-dialogue-role="body"]') return roles.body;
return null;
},
_roles: roles, _classes: classes,
};
}
test.beforeEach(() => {
global.window.currentScreenId = 1;
global.gameData = {
quests: [
{ id: 5, status: 'nouvelle', dialogues: { nouvelle: [{ speaker: 'Le Sorcier', text: 'Bonjour !' }, { speaker: 'Joueur', text: 'Salut.' }], en_cours: [], terminee: [] } },
{ id: 6, status: 'en_cours', dialogues: { nouvelle: [{ speaker: 'PNJ', text: 'x' }], en_cours: [], terminee: [] } },
],
};
});
test('forgeQuestById retrouve une quête par id', () => {
assert.equal(forgeQuestById(5).id, 5);
assert.equal(forgeQuestById(999), undefined);
});
test('forgeShowDialogueBox — quête introuvable : aucun effet (pas de plantage)', () => {
const previousDoc = global.document;
global.document = { querySelector: () => { throw new Error('ne devrait jamais chercher de widget'); } };
try {
assert.doesNotThrow(() => forgeShowDialogueBox(999));
} finally { global.document = previousDoc; }
});
test('forgeShowDialogueBox — quête sans réplique pour son statut actuel : aucun effet', () => {
const previousDoc = global.document;
global.document = { querySelector: () => { throw new Error('ne devrait jamais chercher de widget'); } };
try {
assert.doesNotThrow(() => forgeShowDialogueBox(6)); // "en_cours" -> dialogues.en_cours est vide
} finally { global.document = previousDoc; }
});
test('forgeShowDialogueBox — aucun widget posé sur l\'écran : aucun effet', () => {
const previousDoc = global.document;
global.document = { querySelector: () => null };
try {
assert.doesNotThrow(() => forgeShowDialogueBox(5));
} finally { global.document = previousDoc; }
});
test('forgeShowDialogueBox puis forgeDialogueBoxAdvance — cycle toutes les répliques puis se masque', () => {
const widget = fakeWidget(42);
const previousDoc = global.document;
global.document = { querySelector: sel => (sel.indexOf('data-object-id="42"') !== -1 ? widget : (sel.indexOf('dialogueBoxWidget') !== -1 ? widget : null)) };
try {
forgeShowDialogueBox(5);
assert.equal(widget._roles.header.textContent, 'Le Sorcier');
assert.equal(widget._roles.body.textContent, 'Bonjour !');
assert.equal(widget._classes.has('is-active'), true);
forgeDialogueBoxAdvance(42);
assert.equal(widget._roles.header.textContent, 'Joueur');
assert.equal(widget._roles.body.textContent, 'Salut.');
assert.equal(widget._classes.has('is-active'), true); // encore une réplique visible
forgeDialogueBoxAdvance(42); // dernière réplique consommée -> se masque
assert.equal(widget._classes.has('is-active'), false);
} finally { global.document = previousDoc; }
});
test('forgeDialogueBoxAdvance — objet inconnu (jamais affiché) : aucun effet', () => {
const previousDoc = global.document;
global.document = { querySelector: () => null };
try {
assert.doesNotThrow(() => forgeDialogueBoxAdvance(999));
} finally { global.document = previousDoc; }
});