Quêtes/collision : personnages nommés, dialogues qui parlent, boîte de dialogue en jeu
- "ℹ️ 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:
co-authored by
Claude Sonnet 5
parent
390ee831ca
commit
2347a70c45
@@ -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; }
|
||||
});
|
||||
@@ -80,10 +80,12 @@ function forgeRunCollisionRuleAction(action, targetEl, obj) {
|
||||
return;
|
||||
}
|
||||
if (action.type === 'quete') {
|
||||
// Éditeur de quête pas encore construit (voir plan) — mémorise
|
||||
// l'intention pour un futur système de quêtes plutôt que de la perdre.
|
||||
window.forgeQuestLog = window.forgeQuestLog || [];
|
||||
window.forgeQuestLog.push(action.quete_id);
|
||||
// Affiche la réplique en cours du dialogue de cette quête, selon son
|
||||
// statut actuel, dans le widget "💬 Boîte de dialogue" posé sur
|
||||
// l'écran (voir static/js/play/dialogue-box-controller.js) — sans
|
||||
// effet si aucun widget n'est posé ou si la quête n'a aucune
|
||||
// réplique pour son statut, comme le reste de ce moteur.
|
||||
forgeShowDialogueBox(action.quete_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -185,4 +187,6 @@ if (typeof module !== 'undefined' && module.exports) {
|
||||
const personnageMod = require('./personnage-controller.js');
|
||||
global.forgePersonnageAnimExists = global.forgePersonnageAnimExists || personnageMod.forgePersonnageAnimExists;
|
||||
global.FORGE_PERSONNAGE_FPS = global.FORGE_PERSONNAGE_FPS || 8;
|
||||
const dialogueBoxMod = require('./dialogue-box-controller.js');
|
||||
global.forgeShowDialogueBox = global.forgeShowDialogueBox || dialogueBoxMod.forgeShowDialogueBox;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// ---------- Widget "💬 Boîte de dialogue" — moteur d'exécution ----------
|
||||
// Fait le lien entre l'éditeur de collision (action "quete", voir
|
||||
// static/js/play/collision-rules-controller.js) et le widget d'interface
|
||||
// posé par l'auteur (voir "🖥️ Interface", templates/scene_edit.html,
|
||||
// screens/rendering/dialogue_box_style.py) : à l'exécution d'une action
|
||||
// "quete", affiche la réplique en cours du dialogue correspondant au
|
||||
// STATUT ACTUEL de la quête (gameData.quests, voir full_game_payload.py),
|
||||
// avance d'une réplique à chaque clic sur "Suivant", se masque à la fin.
|
||||
// Silencieux (aucun effet) si la quête est introuvable, n'a aucune
|
||||
// réplique pour son statut, ou si l'auteur n'a posé aucun widget sur
|
||||
// l'écran — même esprit que le reste du moteur (jamais planter).
|
||||
|
||||
var forgeDialogueBoxState = {};
|
||||
|
||||
function forgeQuestById(questId) {
|
||||
return (gameData.quests || []).find(function (q) { return q.id === questId; });
|
||||
}
|
||||
|
||||
function forgeDialogueBoxWidgetOnCurrentScreen() {
|
||||
return document.querySelector('.playScreen[data-screen-id="' + window.currentScreenId + '"] .dialogueBoxWidget');
|
||||
}
|
||||
|
||||
function forgeDialogueBoxWidgetById(objectId) {
|
||||
return document.querySelector('.dialogueBoxWidget[data-object-id="' + objectId + '"]');
|
||||
}
|
||||
|
||||
function forgeRenderDialogueBoxLine(widget, line) {
|
||||
var header = widget.querySelector('[data-dialogue-role="header"]');
|
||||
var body = widget.querySelector('[data-dialogue-role="body"]');
|
||||
if (header) header.textContent = line.speaker;
|
||||
if (body) body.textContent = line.text;
|
||||
}
|
||||
|
||||
// Appelée par l'action "quete" (collision-rules-controller.js) — trouve
|
||||
// le widget posé sur l'écran ACTUELLEMENT affiché (un seul à la fois,
|
||||
// comme le reste du moteur 2D).
|
||||
function forgeShowDialogueBox(questId) {
|
||||
var quest = forgeQuestById(questId);
|
||||
if (!quest) return;
|
||||
var lines = (quest.dialogues && quest.dialogues[quest.status]) || [];
|
||||
if (!lines.length) return;
|
||||
var widget = forgeDialogueBoxWidgetOnCurrentScreen();
|
||||
if (!widget) return;
|
||||
var objectId = widget.getAttribute('data-object-id');
|
||||
forgeDialogueBoxState[objectId] = { lines: lines, index: 0 };
|
||||
forgeRenderDialogueBoxLine(widget, lines[0]);
|
||||
widget.classList.add('is-active');
|
||||
}
|
||||
|
||||
// Appelée par le bouton "Suivant →" rendu dans le widget lui-même (voir
|
||||
// render_scene_object.py, onclick="forgeDialogueBoxAdvance(<id>)").
|
||||
function forgeDialogueBoxAdvance(objectId) {
|
||||
var state = forgeDialogueBoxState[objectId];
|
||||
var widget = forgeDialogueBoxWidgetById(objectId);
|
||||
if (!state || !widget) return;
|
||||
state.index += 1;
|
||||
if (state.index >= state.lines.length) {
|
||||
widget.classList.remove('is-active');
|
||||
delete forgeDialogueBoxState[objectId];
|
||||
return;
|
||||
}
|
||||
forgeRenderDialogueBoxLine(widget, state.lines[state.index]);
|
||||
}
|
||||
|
||||
// static/js/play/__tests__/ (node:test) — même garde que les autres
|
||||
// contrôleurs de ce dossier.
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = {
|
||||
forgeQuestById, forgeShowDialogueBox, forgeDialogueBoxAdvance,
|
||||
forgeDialogueBoxWidgetOnCurrentScreen, forgeDialogueBoxWidgetById, forgeRenderDialogueBoxLine,
|
||||
};
|
||||
}
|
||||
@@ -113,7 +113,7 @@ function forgeObjectCollisionRectAt(left, top, width, height, collision) {
|
||||
// se déplace lui-même.
|
||||
function forgeSolidObstacles(screenData, movingId) {
|
||||
return (screenData.elements || []).filter(function (o) {
|
||||
return o.id !== movingId && o.kind !== 'fond' && !(o.collision && o.collision.enabled === false);
|
||||
return o.id !== movingId && o.kind !== 'fond' && o.kind !== 'dialogue_box' && !(o.collision && o.collision.enabled === false);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user