Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
Supprime le concept de "quête" au profit d'un onglet unique "Déclencheurs"
portant toute la logique (dialogue, condition, marquage terminé) directement
sur l'objet de scène. Ajoute une nouvelle action "Modifier une variable"
(réutilisant le vocabulaire du graphe de flow) utilisable après une
collision, une interaction ou une branche de condition, ainsi qu'un
chaînage d'actions ("then") permettant d'enchaîner plusieurs actions à la
suite et d'étendre un déclencheur déjà posé sans le recréer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
c504ace167
commit
d664ed5637
@@ -1,185 +1,77 @@
|
||||
// ---------- Éditeur de collision (nouvel onglet "🧩 Collision") ----------
|
||||
// Une carte par objet ÉLIGIBLE (jamais un "fond", jamais le personnage
|
||||
// "joueur" — voir COLLISION_RULE_OBJECTS, construit par routes/scenes/
|
||||
// scene_edit_view.py), listant ses règles "déclencheur -> action" déjà
|
||||
// posées + un bouton "+ Action" qui ouvre un assistant pas à pas (jamais
|
||||
// un vrai formulaire à plusieurs champs) : un clic choisit une option, la
|
||||
// carte suivante s'emboîte aussitôt. Voir screens/rendering/
|
||||
// collision_rules.py pour la forme exacte d'une règle (côté serveur, où
|
||||
// elle est validée une seconde fois avant d'être enregistrée).
|
||||
// ---------- Assistant "déclencheur -> action" (fusionné dans l'onglet "Déclencheurs") ----------
|
||||
// L'onglet "🧩 Collision" séparé a été retiré (redondant une fois la
|
||||
// vue/gestion des déclencheurs intégrée à l'onglet "Déclencheurs", voir
|
||||
// static/js/triggers/trigger-editor.js::openAddTriggerModal) — ce
|
||||
// fichier ne porte plus que le MOTEUR de l'assistant lui-même
|
||||
// (déclencheur -> action, un clic choisit une option, la carte suivante
|
||||
// s'emboîte aussitôt), invoqué exclusivement via openTriggerWizard()
|
||||
// ci-dessous. Voir screens/rendering/collision_rules.py pour la forme
|
||||
// exacte d'une règle (côté serveur, où elle est validée une seconde fois
|
||||
// avant d'être enregistrée).
|
||||
|
||||
var COLLISION_ACTION_LABELS = {
|
||||
quete: 'Déclencher une quête', attaque: 'Attaquer la cible',
|
||||
evenement: 'Déclencher un événement', interagir: 'Interagir avec une touche',
|
||||
dialogue: 'Déclencher ce dialogue', variable: 'Modifier une variable',
|
||||
interagir: 'Interagir avec une touche', condition: 'Si une condition est vraie',
|
||||
};
|
||||
// "attaque"/"evenement" retirés de l'assistant (pas besoin pour le
|
||||
// moment, demande explicite) — server-side (screens/rendering/
|
||||
// collision_rules.py::ACTION_TYPES) les accepte encore telles quelles,
|
||||
// pour ne jamais casser une règle déjà enregistrée avec l'un des deux.
|
||||
var COLLISION_WIZARD_ACTION_TYPES = ['quete', 'interagir'];
|
||||
var COLLISION_TRIGGER_LABELS = { collision: 'À la collision', perimetre: 'Dans un périmètre' };
|
||||
|
||||
function collisionRuleObjectLabel(obj) {
|
||||
var kindLabel = obj.kind === 'personnage' ? (obj.role === 'ennemie' ? 'Ennemi' : 'PNJ') : 'Décor';
|
||||
return (obj.name || kindLabel);
|
||||
}
|
||||
|
||||
// Un maillon = un segment de la "chaîne de pièces" affichée dans la carte
|
||||
// (déclencheur, puis action, puis éventuellement sous-action) — texte
|
||||
// seul, la logique de lecture vit dans collisionRuleActionSummary/
|
||||
// collisionRuleTriggerSummary ci-dessous.
|
||||
function collisionRuleTriggerSummary(rule) {
|
||||
if (rule.trigger === 'perimetre') return COLLISION_TRIGGER_LABELS.perimetre + ' (' + rule.perimetre_px + 'px)';
|
||||
return COLLISION_TRIGGER_LABELS.collision;
|
||||
}
|
||||
|
||||
function collisionRuleActionSummary(action) {
|
||||
if (action.type === 'quete') {
|
||||
var quest = (typeof questById === 'function') ? questById(action.quete_id) : null;
|
||||
return COLLISION_ACTION_LABELS.quete + ' « ' + (quest ? quest.title : '#' + action.quete_id) + ' »';
|
||||
}
|
||||
if (action.type === 'evenement') {
|
||||
var ev = CUSTOM_EVENTS_MAP[action.evenement_id];
|
||||
return COLLISION_ACTION_LABELS.evenement + ' « ' + (ev ? ev.name : '?') + ' »';
|
||||
}
|
||||
return COLLISION_ACTION_LABELS[action.type] || action.type;
|
||||
}
|
||||
|
||||
// Une règle = une chaîne de "maillons" en forme de pièces qui s'emboîtent
|
||||
// (déclencheur -> action -> sous-action éventuelle), pas une liste plate —
|
||||
// chaque maillon porte un data-kind (couleur en CSS) et un délai d'entrée
|
||||
// en cascade (voir @keyframes collisionLinkIn, static/style.css).
|
||||
function collisionRuleChainHtml(rule, ruleIndex, objectId) {
|
||||
var links = [
|
||||
{ kind: 'trigger', text: collisionRuleTriggerSummary(rule) },
|
||||
{ kind: rule.action.type, text: collisionRuleActionSummary(rule.action), questId: rule.action.quete_id },
|
||||
];
|
||||
var action = rule.action;
|
||||
while (action.type === 'interagir' && action.sub_action) {
|
||||
links.push({ kind: action.sub_action.type, text: collisionRuleActionSummary(action.sub_action), questId: action.sub_action.quete_id });
|
||||
action = action.sub_action;
|
||||
}
|
||||
// Un maillon "quete" (kind==='quete') ouvre directement sa modale de
|
||||
// dialogue (voir openQuestDialogueModal(), static/js/quests/
|
||||
// quest-editor.js) au clic — demande explicite, plutôt qu'un simple
|
||||
// texte informatif comme les autres maillons. Reste un <span> comme les
|
||||
// autres (jamais un <button>, dont le style natif — apparence système,
|
||||
// marges par défaut — grignotait la pointe en clip-path malgré les
|
||||
// resets CSS) : un onclick sur un <span> suffit à le rendre cliquable,
|
||||
// ".collisionRuleLink[data-kind='quete']" ajoute juste le curseur/hover
|
||||
// qui le distingue visuellement (voir static/style.css).
|
||||
var linksHtml = links.map(function (link, i) {
|
||||
var clickable = link.kind === 'quete' && link.questId != null;
|
||||
var onclick = clickable ? ' onclick="openQuestDialogueModal(' + link.questId + ')"' : '';
|
||||
return '<span class="collisionRuleLink" data-kind="' + link.kind + '" style="animation-delay:' + (i * 60) + 'ms"' + onclick + '>' +
|
||||
link.text.replace(/</g, '<') + '</span>';
|
||||
}).join('');
|
||||
return (
|
||||
'<div class="collisionRuleChain">' +
|
||||
'<div class="collisionRuleChainLinks">' + linksHtml + '</div>' +
|
||||
'<button type="button" class="collisionRuleChainDelete" title="Supprimer cette règle" ' +
|
||||
'onclick="deleteCollisionRule(' + objectId + ', ' + ruleIndex + ')">🗑️</button>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
var COLLISION_WIZARD_ACTION_TYPES = ['dialogue', 'variable', 'interagir', 'condition'];
|
||||
var COLLISION_TRIGGER_LABELS = { collision: 'À la collision' };
|
||||
// Opérations qui n'ont besoin d'AUCUNE valeur (voir screens/labels/
|
||||
// data_operations.py::DATA_OPERATIONS) — le champ valeur de l'étape
|
||||
// "Modifier une variable" (renderCollisionWizardVariableStep) se masque
|
||||
// pour celles-ci.
|
||||
var COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
|
||||
|
||||
// obj.thumbnail_html (screens.render_scene_object) porte un style inline
|
||||
// PENSÉ POUR LE CANEVAS (position:absolute + left/top en dur, voir
|
||||
// screens/scenes/render_scene_object.py) — un personnage déplacé loin
|
||||
// dans la scène (left/top de plusieurs centaines de px) ressortait donc
|
||||
// hors du petit cadre 120×120 de cette miniature : la règle CSS
|
||||
// `.collisionRuleCardThumb img` (voir static/style.css) est censée le
|
||||
// neutraliser (position:static !important etc.), mais s'appuyer sur un
|
||||
// forçage CSS pour annuler un style inline entier reste fragile. Plus
|
||||
// robuste : reconstruire ici une balise <img> "propre", sans AUCUN style
|
||||
// hérité du canevas — seul le src (l'image elle-même) est repris.
|
||||
// hors du petit cadre de la miniature : la règle CSS `.questTriggerObjectCard
|
||||
// img`/`.questTriggerObjectThumb img` (static/style.css) est censée le
|
||||
// neutraliser, mais s'appuyer sur un forçage CSS pour annuler un style
|
||||
// inline entier reste fragile. Plus robuste : reconstruire ici une balise
|
||||
// <img> "propre", sans AUCUN style hérité du canevas — seul le src
|
||||
// (l'image elle-même) est repris. Utilisée par static/js/triggers/
|
||||
// trigger-editor.js (chaîne de bulles + grille de choix d'objet).
|
||||
function collisionRuleThumbHtml(obj) {
|
||||
var match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
|
||||
return match ? '<img src="' + match[1] + '" alt="">' : '';
|
||||
}
|
||||
|
||||
function collisionRuleCardHtml(obj) {
|
||||
var rulesHtml = obj.rules.length
|
||||
? obj.rules.map(function (rule, i) { return collisionRuleChainHtml(rule, i, obj.id); }).join('')
|
||||
: '<p class="hint">Aucune règle pour l\'instant.</p>';
|
||||
return (
|
||||
'<div class="collisionRuleCard" id="collisionRuleCard-' + obj.id + '">' +
|
||||
'<div class="collisionRuleCardHeader">' +
|
||||
'<div class="collisionRuleCardThumb">' + collisionRuleThumbHtml(obj) + '</div>' +
|
||||
'<h3 class="collisionRuleCardTitle">' + collisionRuleObjectLabel(obj).replace(/</g, '<') + '</h3>' +
|
||||
'</div>' +
|
||||
'<div class="collisionRuleCardRules">' + rulesHtml + '</div>' +
|
||||
'<button type="button" class="collisionRuleAddBtn" onclick="openCollisionRuleModal(' + obj.id + ')">Action</button>' +
|
||||
'</div>'
|
||||
);
|
||||
// Id court généré CÔTÉ CLIENT pour une NOUVELLE feuille ("dialogue" ou
|
||||
// "variable", voir screens/rendering/collision_rules.py) — jamais
|
||||
// recalculé/réattribué côté serveur. Pour un dialogue, sert de
|
||||
// dialogue.id (édité en ligne, voir trigger-editor.js) ; pour une
|
||||
// variable, sert d'id à la feuille elle-même — dans les deux cas,
|
||||
// permet d'adresser CETTE feuille précise pour y attacher un "then"
|
||||
// plus tard (voir "+ Ajouter une action", trigger-editor.js).
|
||||
function collisionGenerateActionId() {
|
||||
return 'd_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
}
|
||||
|
||||
function renderCollisionRuleCards() {
|
||||
var container = document.getElementById('collisionRulesCards');
|
||||
if (!container) return;
|
||||
if (!COLLISION_RULE_OBJECTS.length) {
|
||||
container.innerHTML = '<p class="hint">Aucun objet éligible pour l\'instant — un décor, un ennemi ou un PNJ (jamais le joueur lui-même).</p>';
|
||||
return;
|
||||
}
|
||||
// Chaque carte entre en cascade (voir @keyframes collisionCardIn,
|
||||
// static/style.css) plutôt que toutes en même temps.
|
||||
container.innerHTML = COLLISION_RULE_OBJECTS.map(function (obj, i) {
|
||||
return collisionRuleCardHtml(obj).replace('<div class="collisionRuleCard"', '<div style="animation-delay:' + (i * 50) + 'ms" class="collisionRuleCard"');
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function collisionRuleObjectById(objectId) {
|
||||
return COLLISION_RULE_OBJECTS.find(function (o) { return o.id === objectId; });
|
||||
}
|
||||
|
||||
// Enregistre la liste COMPLÈTE des règles de cet objet (voir
|
||||
// routes/scenes/scene_object_collision_rules.py — toujours la liste
|
||||
// entière, jamais une règle isolée) puis redessine SA carte.
|
||||
function saveCollisionRules(objectId) {
|
||||
var obj = collisionRuleObjectById(objectId);
|
||||
if (!obj) return;
|
||||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/collision-rules', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ rules: obj.rules }),
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
if (json.rules) obj.rules = json.rules; // reflète la validation serveur (voir sanitize_collision_rules)
|
||||
renderCollisionRuleCards();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteCollisionRule(objectId, ruleIndex) {
|
||||
var obj = collisionRuleObjectById(objectId);
|
||||
if (!obj) return;
|
||||
obj.rules.splice(ruleIndex, 1);
|
||||
saveCollisionRules(objectId);
|
||||
}
|
||||
|
||||
// ---------- Assistant "+ Action" ----------
|
||||
// ---------- Assistant "déclencheur -> action" ----------
|
||||
// État du parcours en cours — un seul à la fois (la modale n'autorise
|
||||
// qu'une règle en construction) : l'objet ciblé, le déclencheur choisi,
|
||||
// et l'action en cours de construction (posée directement dans
|
||||
// `action`, ou dans `action.sub_action` au second niveau pour "interagir").
|
||||
// Plus de "quête cible" à connaître d'avance — la règle est posée sur
|
||||
// l'objet, un déclencheur "dialogue" se suffit à lui-même.
|
||||
var _collisionWizard = null;
|
||||
|
||||
function openCollisionRuleModal(objectId) {
|
||||
_collisionWizard = { objectId: objectId, trigger: null, perimetrePx: COLLISION_DEFAULT_PERIMETRE_PX };
|
||||
document.getElementById('collisionRuleModal').classList.add('is-active');
|
||||
// Appelée après avoir choisi l'objet dans la modale d'ajout de déclencheur
|
||||
// (voir triggerSelectObject, trigger-editor.js).
|
||||
function openTriggerWizard(objectId, bodyEl) {
|
||||
_collisionWizard = { objectId: objectId, trigger: null, bodyEl: bodyEl };
|
||||
renderCollisionWizardTriggerStep();
|
||||
}
|
||||
|
||||
function closeCollisionRuleModal() {
|
||||
document.getElementById('collisionRuleModal').classList.remove('is-active');
|
||||
_collisionWizard = null;
|
||||
}
|
||||
|
||||
function collisionWizardBodyEl() {
|
||||
return document.getElementById('collisionRuleModalBody');
|
||||
return _collisionWizard.bodyEl;
|
||||
}
|
||||
|
||||
// Étape 1 — déclencheur : collision directe, ou périmètre (px réglable,
|
||||
// valeur par défaut COLLISION_DEFAULT_PERIMETRE_PX).
|
||||
// Étape 1 — déclencheur (un seul choix pour l'instant, "collision") : la
|
||||
// carte à choix reste affichée telle quelle (plutôt que sautée
|
||||
// automatiquement) pour rester extensible à de futurs types sans rien
|
||||
// changer à la structure de l'assistant.
|
||||
function renderCollisionWizardTriggerStep() {
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
|
||||
@@ -187,22 +79,11 @@ function renderCollisionWizardTriggerStep() {
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseTrigger(\'collision\')">' +
|
||||
'<span>' + COLLISION_TRIGGER_LABELS.collision + '</span>' +
|
||||
'</button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:40ms" onclick="collisionWizardChooseTrigger(\'perimetre\')">' +
|
||||
'<span>' + COLLISION_TRIGGER_LABELS.perimetre + '</span>' +
|
||||
'<span class="fieldRow" style="align-items:center; gap:6px;" onclick="event.stopPropagation()">' +
|
||||
'<input type="number" min="1" id="collisionWizardPerimetrePx" value="' + COLLISION_DEFAULT_PERIMETRE_PX + '" style="width:64px;" class="input is-small">' +
|
||||
'<span class="hint">px</span>' +
|
||||
'</span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function collisionWizardChooseTrigger(trigger) {
|
||||
_collisionWizard.trigger = trigger;
|
||||
if (trigger === 'perimetre') {
|
||||
var pxEl = document.getElementById('collisionWizardPerimetrePx');
|
||||
_collisionWizard.perimetrePx = (pxEl && parseFloat(pxEl.value)) || COLLISION_DEFAULT_PERIMETRE_PX;
|
||||
}
|
||||
renderCollisionWizardActionStep(false);
|
||||
}
|
||||
|
||||
@@ -240,54 +121,233 @@ function collisionWizardFinalizeAction(action, nested) {
|
||||
collisionWizardSaveRule(action);
|
||||
}
|
||||
|
||||
// Enregistre via la route /game/<slug>/triggers (routes/triggers/triggers.py)
|
||||
// — même contrat "liste ENTIÈRE" que l'ancien éditeur de collision
|
||||
// (recharge les règles existantes de l'objet, y ajoute la nouvelle,
|
||||
// sauvegarde tout), géré côté serveur : le client ne poste que la
|
||||
// nouvelle règle + l'id d'objet.
|
||||
function collisionWizardSaveRule(action) {
|
||||
var rule = { trigger: _collisionWizard.trigger, action: action };
|
||||
if (_collisionWizard.trigger === 'perimetre') rule.perimetre_px = _collisionWizard.perimetrePx;
|
||||
var obj = collisionRuleObjectById(_collisionWizard.objectId);
|
||||
obj.rules.push(rule);
|
||||
var objectId = _collisionWizard.objectId;
|
||||
closeCollisionRuleModal();
|
||||
saveCollisionRules(objectId);
|
||||
closeTriggerModal();
|
||||
fetch('/game/' + GAME_SLUG + '/triggers', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ object_id: _collisionWizard.objectId, rule: rule }),
|
||||
}).then(function () { triggerLoadAll(); });
|
||||
}
|
||||
|
||||
// Chaque type d'action demande éventuellement une précision avant de
|
||||
// pouvoir se finaliser ("quête" ouvre l'éditeur de quête lui-même — voir
|
||||
// static/js/quests/quest-editor.js::renderQuestPickerStep, pas un simple
|
||||
// champ texte ; événement -> lequel) — "attaque" n'a besoin de rien de
|
||||
// plus, se finalise directement.
|
||||
// "dialogue"/"variable" (des FEUILLES, voir screens/rendering/
|
||||
// collision_rules.py) enchaînent sur "Ensuite ?" (voir
|
||||
// renderCollisionWizardChainStep — Marquer terminé + chaîner une autre
|
||||
// action, demande explicite : "pouvoir modifier un déclencheur pour lui
|
||||
// ajouter des actions") ; "interagir" enchaîne sur une carte de plus
|
||||
// (voir collisionWizardFinalizeAction) ; "condition" enchaîne sur son
|
||||
// propre mini-formulaire (voir renderCollisionWizardConditionStep).
|
||||
function collisionWizardChooseAction(type, nested) {
|
||||
if (type === 'attaque' || type === 'interagir') {
|
||||
if (type === 'interagir') {
|
||||
collisionWizardFinalizeAction({ type: type }, nested);
|
||||
return;
|
||||
}
|
||||
if (type === 'quete') {
|
||||
renderQuestPickerStep(function (quest) {
|
||||
collisionWizardFinalizeAction({ type: 'quete', quete_id: quest.id }, nested);
|
||||
if (type === 'dialogue') {
|
||||
var leaf = { type: 'dialogue', dialogue: { id: collisionGenerateActionId(), lines: [] } };
|
||||
renderCollisionWizardChainStep(leaf, function (finalLeaf) { collisionWizardFinalizeAction(finalLeaf, nested); });
|
||||
return;
|
||||
}
|
||||
if (type === 'variable') {
|
||||
renderCollisionWizardVariableStep(function (action) {
|
||||
renderCollisionWizardChainStep(action, function (finalLeaf) { collisionWizardFinalizeAction(finalLeaf, nested); });
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === 'evenement') {
|
||||
var eventIds = Object.keys(CUSTOM_EVENTS_MAP);
|
||||
if (!eventIds.length) {
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint">Aucun événement personnalisé pour l\'instant — crée-en un dans l\'onglet "Événements", puis reviens ici.</p>';
|
||||
return;
|
||||
}
|
||||
var buttons = eventIds.map(function (id, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardFinalizeAction({type:\'evenement\', evenement_id:' + id + '}, ' + (nested ? 'true' : 'false') + ')">' +
|
||||
'<span>' + CUSTOM_EVENTS_MAP[id].name.replace(/</g, '<') + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel événement ?</p><div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
if (type === 'condition') {
|
||||
renderCollisionWizardConditionStep(nested);
|
||||
}
|
||||
}
|
||||
|
||||
// Appelé directement (pas de DOMContentLoaded) : ce fichier est chargé par
|
||||
// un <script src> posé à la fin du contenu de la page — les éléments dont
|
||||
// on a besoin existent déjà à ce stade. Nécessaire aussi pour la
|
||||
// navigation pjax (voir static/pjax.js — runScriptsIn ré-exécute ce
|
||||
// script à chaque page swappée, mais DOMContentLoaded, lui, ne se
|
||||
// redéclenche jamais après le tout premier chargement réel) — sans ça,
|
||||
// l'onglet "🧩 Collision" restait vide après un remplacement de <main>
|
||||
// (ex. suppression d'un objet de scène, voir scene_object_delete.py) qui
|
||||
// ne redéclenche jamais cet événement.
|
||||
if (typeof COLLISION_RULE_OBJECTS !== 'undefined') renderCollisionRuleCards();
|
||||
// ---------- Étape "Modifier une variable" ----------
|
||||
// Même vocabulaire EXACT que l'action "Modifier une variable" du graphe
|
||||
// de logique document (GLOBAL_VARIABLE_NAMES et DATA_OPERATION_LABELS_MAP
|
||||
// déjà posés par templates/scene_edit.html — ce dernier n'était utilisé
|
||||
// nulle part avant cette fonctionnalité) : variable ciblée, opération,
|
||||
// valeur (masquée pour les opérations qui n'en ont pas besoin, voir
|
||||
// COLLISION_NO_VALUE_OPERATIONS). `onChoose(action)`.
|
||||
function renderCollisionWizardVariableStep(onChoose) {
|
||||
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
|
||||
var operationOptions = Object.keys(DATA_OPERATION_LABELS_MAP).map(function (key) {
|
||||
return '<option value="' + key + '">' + DATA_OPERATION_LABELS_MAP[key] + '</option>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Modifier...</p>' +
|
||||
'<div class="collisionWizardConditionForm">' +
|
||||
'<select id="varWizVariable">' + variableOptions + '</select>' +
|
||||
'<select id="varWizOperation" onchange="collisionWizardVariableOperationChanged()">' + operationOptions + '</select>' +
|
||||
'<input type="text" id="varWizValue" placeholder="Valeur">' +
|
||||
'</div>' +
|
||||
'<button type="button" class="button primary mt-3" onclick="collisionWizardVariableNext()">Continuer</button>';
|
||||
_collisionWizard.onVariableChosen = onChoose;
|
||||
collisionWizardVariableOperationChanged();
|
||||
}
|
||||
|
||||
function collisionWizardVariableOperationChanged() {
|
||||
var operation = document.getElementById('varWizOperation').value;
|
||||
var valueInput = document.getElementById('varWizValue');
|
||||
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1 ? '' : 'none';
|
||||
}
|
||||
|
||||
function collisionWizardVariableNext() {
|
||||
var operation = document.getElementById('varWizOperation').value;
|
||||
var action = {
|
||||
type: 'variable', id: collisionGenerateActionId(),
|
||||
target_variable: document.getElementById('varWizVariable').value,
|
||||
data_operation: operation,
|
||||
};
|
||||
if (COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1) {
|
||||
action.data_value = document.getElementById('varWizValue').value;
|
||||
}
|
||||
var onChoose = _collisionWizard.onVariableChosen;
|
||||
_collisionWizard.onVariableChosen = null;
|
||||
onChoose(action);
|
||||
}
|
||||
|
||||
// ---------- Étape "Ensuite ?" (chaînage + Marquer terminé) ----------
|
||||
// Après CHAQUE feuille construite (dialogue ou variable, au premier
|
||||
// niveau, sous "interagir", ou dans une branche de condition) : la case
|
||||
// "Marquer terminé" (seulement pour un dialogue) PUIS le choix de
|
||||
// chaîner une autre action à la suite (voir screens/rendering/
|
||||
// collision_rules.py::"then", demande explicite "pouvoir modifier un
|
||||
// déclencheur pour lui ajouter des actions") — jamais bloqué à une seule
|
||||
// action par déclencheur. `onDone(leafAction)` reçoit la PREMIÈRE
|
||||
// feuille de la chaîne (les suivantes sont attachées via `.then`).
|
||||
function renderCollisionWizardChainStep(leafAction, onDone) {
|
||||
var completionHtml = leafAction.type === 'dialogue'
|
||||
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">' +
|
||||
'<input type="checkbox" id="condWizMarkCompleted"' + (leafAction.mark_completed ? ' checked' : '') + '> Marquer ce dialogue comme terminé une fois joué' +
|
||||
'</label>'
|
||||
: '';
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
completionHtml +
|
||||
'<p class="hint mb-2">Ensuite ?</p>' +
|
||||
'<div class="collisionWizardChoices">' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChainDone(\'none\')"><span>Rien de plus</span></button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:40ms" onclick="collisionWizardChainDone(\'dialogue\')"><span>Déclencher un dialogue</span></button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:80ms" onclick="collisionWizardChainDone(\'variable\')"><span>Modifier une variable</span></button>' +
|
||||
'</div>';
|
||||
_collisionWizard.chainLeaf = leafAction;
|
||||
_collisionWizard.onChainDone = onDone;
|
||||
}
|
||||
|
||||
function collisionWizardChainDone(choice) {
|
||||
var leafAction = _collisionWizard.chainLeaf;
|
||||
var onDone = _collisionWizard.onChainDone;
|
||||
if (leafAction.type === 'dialogue') {
|
||||
if (document.getElementById('condWizMarkCompleted').checked) leafAction.mark_completed = true;
|
||||
else delete leafAction.mark_completed;
|
||||
}
|
||||
if (choice === 'none') {
|
||||
onDone(leafAction);
|
||||
return;
|
||||
}
|
||||
if (choice === 'dialogue') {
|
||||
var nextLeaf = { type: 'dialogue', dialogue: { id: collisionGenerateActionId(), lines: [] } };
|
||||
leafAction.then = nextLeaf;
|
||||
renderCollisionWizardChainStep(nextLeaf, onDone);
|
||||
return;
|
||||
}
|
||||
// 'variable'
|
||||
renderCollisionWizardVariableStep(function (action) {
|
||||
leafAction.then = action;
|
||||
renderCollisionWizardChainStep(action, onDone);
|
||||
});
|
||||
}
|
||||
|
||||
// Étape "condition" — variable/opérateur/valeur (même vocabulaire que le
|
||||
// nœud Condition de l'éditeur de logique, voir screens/flow/constants.py
|
||||
// ::CONDITION_OPERATORS ; GLOBAL_VARIABLE_NAMES et
|
||||
// CONDITION_OPERATOR_LABELS_MAP posés par templates/scene_edit.html).
|
||||
// `nested` (interagir -> condition) est reporté jusqu'à la finalisation,
|
||||
// une fois les deux branches Vrai/Faux choisies ci-dessous.
|
||||
function renderCollisionWizardConditionStep(nested) {
|
||||
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
|
||||
var operatorOptions = Object.keys(CONDITION_OPERATOR_LABELS_MAP).map(function (key) {
|
||||
return '<option value="' + key + '">' + CONDITION_OPERATOR_LABELS_MAP[key] + '</option>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Si...</p>' +
|
||||
'<div class="collisionWizardConditionForm">' +
|
||||
'<select id="condWizVariable">' + variableOptions + '</select>' +
|
||||
'<select id="condWizOperator">' + operatorOptions + '</select>' +
|
||||
'<input type="text" id="condWizValue" placeholder="Valeur">' +
|
||||
'</div>' +
|
||||
'<button type="button" class="button primary mt-3" onclick="collisionWizardConditionNext(' + (nested ? 'true' : 'false') + ')">Continuer</button>';
|
||||
}
|
||||
|
||||
function collisionWizardConditionNext(nested) {
|
||||
_collisionWizard.pendingCondition = {
|
||||
type: 'condition',
|
||||
variable: document.getElementById('condWizVariable').value,
|
||||
operateur: document.getElementById('condWizOperator').value,
|
||||
valeur: document.getElementById('condWizValue').value,
|
||||
si_vrai: null, si_faux: null,
|
||||
};
|
||||
_collisionWizard.pendingConditionNested = nested;
|
||||
renderCollisionWizardBranchStep('si_vrai');
|
||||
}
|
||||
|
||||
// Étape branche (Vrai puis Faux) — chaque branche est soit "Ne rien
|
||||
// faire", soit une FEUILLE ("Déclencher un dialogue" ou "Modifier une
|
||||
// variable", voir screens/rendering/collision_rules.py::
|
||||
// _sanitize_condition_branch : une branche reste une feuille, jamais une
|
||||
// nouvelle "interagir"/"condition" imbriquée) — les DEUX branches
|
||||
// peuvent chacune déclencher LEUR PROPRE feuille (et sa propre chaîne),
|
||||
// indépendamment l'une de l'autre.
|
||||
function renderCollisionWizardBranchStep(branchKey) {
|
||||
var label = branchKey === 'si_vrai' ? '✓ Si VRAI...' : '✕ Si FAUX...';
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">' + label + '</p>' +
|
||||
'<div class="collisionWizardChoices">' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'dialogue\')">' +
|
||||
'<span>Déclencher un dialogue</span>' +
|
||||
'</button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:40ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'variable\')">' +
|
||||
'<span>Modifier une variable</span>' +
|
||||
'</button>' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:80ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'none\')">' +
|
||||
'<span>Ne rien faire</span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function collisionWizardChooseBranch(branchKey, choice) {
|
||||
if (choice === 'none') {
|
||||
_collisionWizard.pendingCondition[branchKey] = null;
|
||||
collisionWizardBranchDone(branchKey);
|
||||
return;
|
||||
}
|
||||
if (choice === 'dialogue') {
|
||||
var leaf = { type: 'dialogue', dialogue: { id: collisionGenerateActionId(), lines: [] } };
|
||||
renderCollisionWizardChainStep(leaf, function (finalLeaf) {
|
||||
_collisionWizard.pendingCondition[branchKey] = finalLeaf;
|
||||
collisionWizardBranchDone(branchKey);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 'variable'
|
||||
renderCollisionWizardVariableStep(function (action) {
|
||||
renderCollisionWizardChainStep(action, function (finalLeaf) {
|
||||
_collisionWizard.pendingCondition[branchKey] = finalLeaf;
|
||||
collisionWizardBranchDone(branchKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function collisionWizardBranchDone(branchKey) {
|
||||
if (branchKey === 'si_vrai') {
|
||||
renderCollisionWizardBranchStep('si_faux');
|
||||
return;
|
||||
}
|
||||
var nested = _collisionWizard.pendingConditionNested;
|
||||
var condition = _collisionWizard.pendingCondition;
|
||||
_collisionWizard.pendingCondition = null;
|
||||
collisionWizardFinalizeAction(condition, nested);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user