Files
Forge-Engine/static/js/play/dialogue-box-controller.js
T
williamandClaude Sonnet 5 e90d941527
Build and deploy / test-python (push) Successful in 7m13s
Build and deploy / test-js (push) Successful in 1m5s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Quête "nouvelle" : écran Accepter/Refuser à la fin du dialogue
Une fois le dialogue épuisé, si la quête est encore "nouvelle", la
boîte de dialogue bascule sur un écran d'offre au lieu de se masquer :
header "Quête : <titre>", body l'objectif, pied deux boutons.

- Accepter -> la quête passe "en_cours" (en MÉMOIRE seulement,
  gameData.quests — jamais persisté en base : le statut en base est
  celui de DÉPART pour toute nouvelle partie, pas un état de partie en
  cours, voir PLAYER_SHARED/full_game_payload.py).
- Refuser -> la boîte se referme SANS toucher au statut, qui reste
  "nouvelle" : la prochaine interaction rejoue exactement le même
  dialogue depuis le début — impossible d'avancer la quête sans
  l'accepter un jour.

Pour tout autre statut (déjà "en_cours"/"terminee"), la boîte se
masque normalement à la fin du dialogue, comme avant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 14:00:02 +02:00

148 lines
7.0 KiB
JavaScript

// ---------- 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".
//
// Une fois le dialogue épuisé : si la quête est encore "nouvelle", la
// boîte bascule en écran d'OFFRE ("Quête : <titre>" / objectif / boutons
// Accepter-Refuser) au lieu de se masquer — accepter fait passer la
// quête "en_cours" (LOCAL à cette partie, jamais persisté en base :
// gameData.quests n'est qu'un instantané par joueur, voir
// full_game_payload.py ; muter la ligne en base changerait le statut de
// DÉPART de la quête pour toutes les parties futures) ; refuser ferme
// simplement la boîte SANS changer le statut — la quête reste "nouvelle",
// donc la prochaine interaction rejoue EXACTEMENT le même dialogue depuis
// le début (impossible d'avancer sans accepter). Pour tout autre statut
// (déjà "en_cours"/"terminee"), la boîte se masque normalement à la fin.
// Silencieux (aucun effet) si la quête est introuvable ou si l'auteur n'a
// posé aucun widget sur l'écran — même esprit que le reste du moteur.
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 forgeDialogueBoxFooterEl(widget) {
return widget.querySelector('[data-dialogue-role="footer"]');
}
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;
}
// Remet le pied de la boîte dans son état "dialogue" (un seul bouton
// "Suivant →", voir render_scene_object.py) — nécessaire pour effacer
// les boutons Accepter/Refuser d'une offre PRÉCÉDENTE avant de rejouer
// un nouveau dialogue dans la même boîte.
function forgeResetDialogueBoxFooter(widget, objectId) {
var footer = forgeDialogueBoxFooterEl(widget);
if (footer) footer.innerHTML = '<button type="button" class="dialogueBoxNextBtn" data-dialogue-role="next-btn" onclick="forgeDialogueBoxAdvance(' + objectId + ')">Suivant →</button>';
}
// 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]) || [];
var widget = forgeDialogueBoxWidgetOnCurrentScreen();
if (!widget) return;
var objectId = widget.getAttribute('data-object-id');
// Aucune réplique posée pour ce statut : une quête "nouvelle" propose
// quand même directement l'offre (accepter/refuser), les autres
// statuts restent silencieux (rien à montrer).
if (!lines.length) {
if (quest.status === 'nouvelle') forgeShowQuestOffer(widget, objectId, quest);
return;
}
forgeResetDialogueBoxFooter(widget, objectId);
forgeDialogueBoxState[objectId] = { questId: questId, 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) {
var quest = forgeQuestById(state.questId);
if (quest && quest.status === 'nouvelle') {
forgeShowQuestOffer(widget, objectId, quest);
return;
}
widget.classList.remove('is-active');
delete forgeDialogueBoxState[objectId];
return;
}
forgeRenderDialogueBoxLine(widget, state.lines[state.index]);
}
// Écran d'offre : header = "Quête : <titre>", body = l'objectif, pied =
// Accepter/Refuser au lieu du "Suivant" habituel.
function forgeShowQuestOffer(widget, objectId, quest) {
var header = widget.querySelector('[data-dialogue-role="header"]');
var body = widget.querySelector('[data-dialogue-role="body"]');
var footer = forgeDialogueBoxFooterEl(widget);
if (header) header.textContent = 'Quête : ' + quest.title;
if (body) body.textContent = quest.objectif || '';
if (footer) {
footer.innerHTML =
'<button type="button" class="dialogueBoxNextBtn dialogueBoxAcceptBtn" onclick="forgeDialogueBoxAcceptQuest(' + quest.id + ', ' + objectId + ')">Accepter</button>' +
'<button type="button" class="dialogueBoxNextBtn dialogueBoxDeclineBtn" onclick="forgeDialogueBoxDeclineQuest(' + objectId + ')">Refuser</button>';
}
forgeDialogueBoxState[objectId] = { questId: quest.id, lines: [], index: 0 };
widget.classList.add('is-active');
}
// Fait passer la quête "en_cours" — UNIQUEMENT dans l'instantané en
// mémoire (gameData.quests), jamais persisté en base : voir la note en
// tête de fichier (le statut en base est celui de DÉPART pour toute
// nouvelle partie, pas un état de partie en cours).
function forgeDialogueBoxAcceptQuest(questId, objectId) {
var quest = forgeQuestById(questId);
if (quest) quest.status = 'en_cours';
var widget = forgeDialogueBoxWidgetById(objectId);
if (widget) widget.classList.remove('is-active');
delete forgeDialogueBoxState[objectId];
}
// Referme la boîte SANS toucher au statut de la quête (reste "nouvelle")
// — la prochaine interaction rejoue donc le même dialogue depuis le
// début : impossible d'avancer la quête sans un jour l'accepter.
function forgeDialogueBoxDeclineQuest(objectId) {
var widget = forgeDialogueBoxWidgetById(objectId);
if (widget) widget.classList.remove('is-active');
delete forgeDialogueBoxState[objectId];
}
// 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,
forgeShowQuestOffer, forgeDialogueBoxAcceptQuest, forgeDialogueBoxDeclineQuest, forgeResetDialogueBoxFooter,
};
}