Dialogues de quête : bulles illimitées, n'importe quel objet, timeline reliée
Build and deploy / test-python (push) Successful in 7m32s
Build and deploy / test-js (push) Successful in 1m2s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- "qui parle" n'est plus réservé au personnage : N'IMPORTE QUEL objet de
  scène nommé (personnage, décor, fond — voir "ℹ️ Informations",
  screens/rendering/scene_object_names.py, ex-personnage_names.py) peut
  parler dans un dialogue.
- Confirmé/documenté : aucune limite au nombre de répliques par colonne
  (bouton "+ Réplique" reste toujours disponible).
- Bulle = header (menu déroulant du "qui parle") + body (le texte),
  centrée dans sa colonne à 70% de largeur, alignée verticalement et
  reliée à la suivante par un trait — remplace l'ancien alignement
  gauche/droite façon chat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-03 11:54:06 +02:00
co-authored by Claude Sonnet 5
parent 2347a70c45
commit 521792fe00
11 changed files with 105 additions and 67 deletions
+23 -13
View File
@@ -27,14 +27,15 @@ var QUEST_STATUS_ORDER = ['nouvelle', 'en_cours', 'terminee'];
// besoin (collisionRuleActionSummary).
var QUESTS = (typeof QUESTS_JSON !== 'undefined') ? QUESTS_JSON : [];
// Noms de personnages nommés (voir "ℹ️ Informations", templates/
// scene_edit.html) — proposés comme "qui parle" pour une réplique, en
// plus de "Joueur" (toujours disponible, jamais un objet de scène).
var PERSONNAGE_NAMES = (typeof PERSONNAGE_NAMES_JSON !== 'undefined') ? PERSONNAGE_NAMES_JSON : [];
// Noms d'objets de scène nommés (voir "ℹ️ Informations", templates/
// scene_edit.html) — N'IMPORTE QUEL objet (personnage, décor, fond), pas
// seulement un personnage — proposés comme "qui parle" pour une réplique,
// en plus de "Joueur" (toujours disponible, jamais un objet de scène).
var SCENE_OBJECT_NAMES = (typeof SCENE_OBJECT_NAMES_JSON !== 'undefined') ? SCENE_OBJECT_NAMES_JSON : [];
// Une réplique n'a que deux COULEURS possibles (voir .questBubble-joueur/
// .questBubble-pnj, static/style.css) même si le nom affiché est
// n'importe quel personnage nommé — "Joueur" à droite en vert, n'importe
// n'importe quel objet nommé — "Joueur" en vert, n'importe
// quel autre nom à gauche en bleu (voir demande utilisateur : "les bulles
// du pnj et du joueur sont d'une couleur différente").
function questBubbleKind(speaker) {
@@ -198,24 +199,33 @@ function renderQuestFieldsModalBody(questId) {
// renommé/supprimé depuis) pour ne jamais perdre silencieusement la
// valeur déjà enregistrée.
function questSpeakerOptions(currentSpeaker) {
var options = ['Joueur'].concat(PERSONNAGE_NAMES);
var options = ['Joueur'].concat(SCENE_OBJECT_NAMES);
if (currentSpeaker && options.indexOf(currentSpeaker) === -1) options.push(currentSpeaker);
return options;
}
// Une bulle = header (qui parle, menu déroulant des objets de scène
// nommés — voir "ℹ️ Informations") + body (le texte) — deux zones
// distinctes demandées explicitement, pas un seul bloc. Toujours centrée
// dans sa colonne, 70% de la largeur (voir .questBubble, static/style.css) —
// aucune limite au nombre de bulles par colonne (voir questAddBubble).
function questDialogueBubbleHtml(line, status, index) {
var kind = questBubbleKind(line.speaker);
var speakerSelect = questSpeakerOptions(line.speaker).map(function (name) {
return '<option value="' + name.replace(/"/g, '&quot;') + '"' + (name === line.speaker ? ' selected' : '') + '>' + (name === 'Joueur' ? '🧑 Joueur' : '🗨️ ' + name) + '</option>';
}).join('');
return (
'<div class="questBubbleRow questBubbleRow-' + kind + '">' +
'<div class="questBubbleRow">' +
'<div class="questBubble questBubble-' + kind + '">' +
'<select class="questBubbleSpeaker" title="Qui parle" onchange="questSetBubbleSpeaker(\'' + status + '\', ' + index + ', this.value)">' + speakerSelect + '</select>' +
'<textarea class="questBubbleText" rows="1" placeholder="Réplique..." ' +
'oninput="this.style.height=\'auto\'; this.style.height=this.scrollHeight+\'px\';" ' +
'onchange="questUpdateBubbleText(\'' + status + '\', ' + index + ', this.value)">' + line.text.replace(/</g, '&lt;') + '</textarea>' +
'<button type="button" class="questBubbleDelete" title="Supprimer" onclick="questDeleteBubble(\'' + status + '\', ' + index + ')">✕</button>' +
'<div class="questBubbleHeader">' +
'<select class="questBubbleSpeaker" title="Qui parle" onchange="questSetBubbleSpeaker(\'' + status + '\', ' + index + ', this.value)">' + speakerSelect + '</select>' +
'<button type="button" class="questBubbleDelete" title="Supprimer" onclick="questDeleteBubble(\'' + status + '\', ' + index + ')">✕</button>' +
'</div>' +
'<div class="questBubbleBody">' +
'<textarea class="questBubbleText" rows="1" placeholder="Réplique..." ' +
'oninput="this.style.height=\'auto\'; this.style.height=this.scrollHeight+\'px\';" ' +
'onchange="questUpdateBubbleText(\'' + status + '\', ' + index + ', this.value)">' + line.text.replace(/</g, '&lt;') + '</textarea>' +
'</div>' +
'</div>' +
'</div>'
);
@@ -324,7 +334,7 @@ function questSaveDialogues(quest) {
// — juste un défaut de départ, l'auteur choisit ensuite librement dans
// le menu déroulant de la réplique.
function questNextDefaultSpeaker(lastSpeaker) {
if (lastSpeaker === 'Joueur') return PERSONNAGE_NAMES[0] || 'PNJ';
if (lastSpeaker === 'Joueur') return SCENE_OBJECT_NAMES[0] || 'PNJ';
return 'Joueur';
}
+26 -14
View File
@@ -817,31 +817,43 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
display:flex; flex-direction:column; gap:8px; min-height:160px;
}
.questDialogueColumnTitle{ margin:0 0 2px; font-size:13px; text-align:center; }
.questDialogueColumnBubbles{ display:flex; flex-direction:column; gap:8px; flex:1; }
.questDialogueAddBtn{ align-self:center; padding:6px 14px; font-size:12px; }
/* Bulles alignées VERTICALEMENT, centrées dans la colonne, reliées par un
trait entre chacune (voir ::before sur .questBubbleRow ci-dessous) —
demandé explicitement plutôt qu'un alignement gauche/droite. */
.questDialogueColumnBubbles{ display:flex; flex-direction:column; align-items:center; flex:1; padding-top:4px; }
.questDialogueAddBtn{ align-self:center; padding:6px 14px; font-size:12px; margin-top:4px; }
.questBubbleRow{ display:flex; }
.questBubbleRow-pnj{ justify-content:flex-start; }
.questBubbleRow-joueur{ justify-content:flex-end; }
.questBubble{
display:flex; flex-direction:column; gap:4px; max-width:88%; border-radius:14px; padding:8px 10px;
animation:collisionLinkIn .25s ease backwards;
.questBubbleRow{ display:flex; justify-content:center; width:100%; position:relative; }
.questBubbleRow:not(:first-child)::before{
content:''; position:absolute; top:-14px; left:50%; transform:translateX(-50%);
width:2px; height:14px; background:var(--border);
}
.questBubble-pnj{ background:linear-gradient(135deg, #2f6fed, #1e4fb0); border-bottom-left-radius:4px; }
.questBubble-joueur{ background:linear-gradient(135deg, #2ecc71, #1e9e57); border-bottom-right-radius:4px; }
/* Occupe 70% de la largeur de la colonne, header + body distincts (voir
demande utilisateur) plutôt qu'un seul bloc — header : qui parle
(sélectionné parmi les objets de scène nommés) ; body : le texte. */
.questBubble{
width:70%; display:flex; flex-direction:column; border-radius:12px; overflow:hidden;
margin-bottom:14px; box-shadow:0 4px 14px rgba(0,0,0,.3); animation:collisionLinkIn .25s ease backwards;
}
.questBubbleHeader{ display:flex; align-items:center; justify-content:space-between; gap:6px; padding:6px 10px; }
.questBubbleBody{ padding:10px; }
.questBubble-pnj .questBubbleHeader{ background:#1e4fb0; }
.questBubble-pnj .questBubbleBody{ background:rgba(47,111,237,.16); }
.questBubble-joueur .questBubbleHeader{ background:#1e9e57; }
.questBubble-joueur .questBubbleBody{ background:rgba(46,204,113,.16); }
.questBubbleSpeaker{
font-size:10.5px; font-weight:700; color:rgba(255,255,255,.85); cursor:pointer;
background:transparent; border:none; padding:0; font-family:inherit; max-width:100%;
font-size:11px; font-weight:700; color:#fff; cursor:pointer;
background:transparent; border:none; padding:0; font-family:inherit; max-width:100%; flex:1;
}
.questBubbleSpeaker option{ color:#111; }
.questBubbleText{
resize:none; overflow:hidden; border:none; background:transparent; color:#fff; font-size:13px;
font-family:inherit; padding:0; min-height:1.4em;
font-family:inherit; padding:0; min-height:1.4em; width:100%;
}
.questBubbleText::placeholder{ color:rgba(255,255,255,.6); }
.questBubbleText:focus{ outline:none; }
.questBubbleDelete{
align-self:flex-end; background:none; border:none; color:rgba(255,255,255,.6); cursor:pointer;
flex-shrink:0; background:none; border:none; color:rgba(255,255,255,.75); cursor:pointer;
font-size:11px; padding:0; transition:color .15s ease;
}
.questBubbleDelete:hover{ color:#fff; }