Files
Forge-Engine/static/js/scenes/collision-rules-editor.js
T
williamandClaude Sonnet 5 d664ed5637
Build and deploy / test-python (push) Successful in 8m45s
Build and deploy / test-js (push) Successful in 49s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
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>
2026-09-07 22:05:06 +02:00

354 lines
17 KiB
JavaScript

// ---------- 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 = {
dialogue: 'Déclencher ce dialogue', variable: 'Modifier une variable',
interagir: 'Interagir avec une touche', condition: 'Si une condition est vraie',
};
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 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="">' : '';
}
// 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);
}
// ---------- 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;
// 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 collisionWizardBodyEl() {
return _collisionWizard.bodyEl;
}
// É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>' +
'<div class="collisionWizardChoices">' +
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseTrigger(\'collision\')">' +
'<span>' + COLLISION_TRIGGER_LABELS.collision + '</span>' +
'</button>' +
'</div>';
}
function collisionWizardChooseTrigger(trigger) {
_collisionWizard.trigger = trigger;
renderCollisionWizardActionStep(false);
}
// Étape 2 (et, pour "interagir", étape 3 en second niveau — `nested`
// distingue les deux, "interagir" n'étant proposé qu'au premier niveau,
// voir screens/rendering/collision_rules.py::_MAX_ACTION_DEPTH) :
// quelle action exécuter.
function renderCollisionWizardActionStep(nested) {
var types = nested ? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_WIZARD_ACTION_TYPES;
var buttons = types.map(function (type, i) {
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
'<span>' + COLLISION_ACTION_LABELS[type] + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">' + (nested ? 'Une fois la touche pressée...' : 'Alors...') + '</p>' +
'<div class="collisionWizardChoices">' + buttons + '</div>';
}
function collisionWizardFinalizeAction(action, nested) {
if (nested) {
// Deuxième niveau ("interagir" -> sous-action) : referme la maille
// sur l'action de premier niveau, déjà mémorisée sur le wizard.
_collisionWizard.pendingInteragir.sub_action = action;
collisionWizardSaveRule(_collisionWizard.pendingInteragir);
return;
}
if (action.type === 'interagir') {
// "interagir" n'est jamais final : une carte supplémentaire s'emboîte
// pour choisir CE QUI se passe une fois la touche pressée.
_collisionWizard.pendingInteragir = action;
renderCollisionWizardActionStep(true);
return;
}
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 };
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(); });
}
// "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 === 'interagir') {
collisionWizardFinalizeAction({ type: type }, nested);
return;
}
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 === 'condition') {
renderCollisionWizardConditionStep(nested);
}
}
// ---------- É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);
}