Éditeur de collision : cartes "pièces qui s'emboîtent", animées
Build and deploy / test-python (push) Successful in 6m27s
Build and deploy / test-js (push) Successful in 56s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Refonte visuelle de l'onglet "🧩 Collision" — l'ancien rendu (pastilles
plates + flèches) ne rendait pas la métaphore "pièces de puzzle qui
s'emboîtent" demandée. Chaque maillon (déclencheur/action/sous-action)
est maintenant une pièce chevronnée colorée par type, imbriquée dans
la suivante via clip-path + marge négative, avec une entrée animée en
cascade. Cartes objet et choix de l'assistant modal redessinés en
grille de cartes avec icône + libellé, hover/entrée animés.

Purement visuel — aucun changement de logique/données (déjà couvert
par tests/test_collision_rules.py et
static/js/play/__tests__/collision-rules-controller.test.js).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-02 17:23:42 +02:00
co-authored by Claude Sonnet 5
parent ee270d0286
commit b64541856f
2 changed files with 112 additions and 32 deletions
+48 -20
View File
@@ -37,22 +37,28 @@ function collisionRuleActionSummary(action) {
return COLLISION_ACTION_LABELS[action.type] || action.type;
}
// Une règle = une chaîne de "maillons" (déclencheur -> action -> sous-action
// éventuelle) — chacun sa propre petite carte, visuellement emboîtées.
// 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 = [collisionRuleTriggerSummary(rule), collisionRuleActionSummary(rule.action)];
var links = [
{ kind: 'trigger', text: collisionRuleTriggerSummary(rule) },
{ kind: rule.action.type, text: collisionRuleActionSummary(rule.action) },
];
var action = rule.action;
while (action.type === 'interagir' && action.sub_action) {
links.push(collisionRuleActionSummary(action.sub_action));
links.push({ kind: action.sub_action.type, text: collisionRuleActionSummary(action.sub_action) });
action = action.sub_action;
}
var linksHtml = links.map(function (text) {
return '<span class="collisionRuleLink">' + text.replace(/</g, '&lt;') + '</span>';
}).join('<span class="collisionRuleArrow">→</span>');
var linksHtml = links.map(function (link, i) {
return '<span class="collisionRuleLink" data-kind="' + link.kind + '" style="animation-delay:' + (i * 60) + 'ms">' +
link.text.replace(/</g, '&lt;') + '</span>';
}).join('');
return (
'<div class="collisionRuleChain">' +
'<div class="collisionRuleChainLinks">' + linksHtml + '</div>' +
'<button type="button" class="button is-small is-danger" title="Supprimer cette règle" ' +
'<button type="button" class="collisionRuleChainDelete" title="Supprimer cette règle" ' +
'onclick="deleteCollisionRule(' + objectId + ', ' + ruleIndex + ')">🗑️</button>' +
'</div>'
);
@@ -69,7 +75,7 @@ function collisionRuleCardHtml(obj) {
'<h3 class="collisionRuleCardTitle">' + collisionRuleObjectLabel(obj).replace(/</g, '&lt;') + '</h3>' +
'</div>' +
'<div class="collisionRuleCardRules">' + rulesHtml + '</div>' +
'<button type="button" class="button is-small primary" onclick="openCollisionRuleModal(' + obj.id + ')">+ Action</button>' +
'<button type="button" class="collisionRuleAddBtn" onclick="openCollisionRuleModal(' + obj.id + ')">✚ Action</button>' +
'</div>'
);
}
@@ -81,7 +87,11 @@ function renderCollisionRuleCards() {
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;
}
container.innerHTML = COLLISION_RULE_OBJECTS.map(collisionRuleCardHtml).join('');
// 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) {
@@ -136,16 +146,29 @@ function collisionWizardBodyEl() {
// Étape 1 — déclencheur : collision directe, ou périmètre (px réglable,
// valeur par défaut COLLISION_DEFAULT_PERIMETRE_PX).
// Sépare "emoji" et "libellé" d'un texte "🗺️ Déclencher une quête" pour
// les afficher sur deux lignes dans une carte-choix (voir .collisionWizardChoice).
function collisionSplitEmojiLabel(text) {
var m = text.match(/^(\S+)\s+(.*)$/);
return m ? { emoji: m[1], label: m[2] } : { emoji: '', label: text };
}
function renderCollisionWizardTriggerStep() {
var collision = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.collision);
var perimetre = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.perimetre);
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
'<div class="collisionWizardChoices">' +
'<button type="button" class="button is-fullwidth mb-2" onclick="collisionWizardChooseTrigger(\'collision\')">' + COLLISION_TRIGGER_LABELS.collision + '</button>' +
'<div class="fieldRow" style="align-items:center; gap:8px;">' +
'<button type="button" class="button is-fullwidth" onclick="collisionWizardChooseTrigger(\'perimetre\')">' + COLLISION_TRIGGER_LABELS.perimetre + '</button>' +
'<input type="number" min="1" id="collisionWizardPerimetrePx" value="' + COLLISION_DEFAULT_PERIMETRE_PX + '" style="width:80px;" class="input is-small">' +
'<span class="hint">px</span>' +
'</div>' +
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseTrigger(\'collision\')">' +
'<span class="emoji">' + collision.emoji + '</span><span>' + collision.label + '</span>' +
'</button>' +
'<button type="button" class="collisionWizardChoice" style="animation-delay:40ms" onclick="collisionWizardChooseTrigger(\'perimetre\')">' +
'<span class="emoji">' + perimetre.emoji + '</span><span>' + perimetre.label + '</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>';
}
@@ -164,8 +187,11 @@ function collisionWizardChooseTrigger(trigger) {
// quelle action exécuter.
function renderCollisionWizardActionStep(nested) {
var types = nested ? COLLISION_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_ACTION_TYPES;
var buttons = types.map(function (type) {
return '<button type="button" class="button is-fullwidth mb-2" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' + COLLISION_ACTION_LABELS[type] + '</button>';
var buttons = types.map(function (type, i) {
var split = collisionSplitEmojiLabel(COLLISION_ACTION_LABELS[type]);
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
'<span class="emoji">' + split.emoji + '</span><span>' + split.label + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML =
'<p class="hint mb-2">' + (nested ? 'Une fois la touche pressée...' : 'Alors...') + '</p>' +
@@ -223,8 +249,10 @@ function collisionWizardChooseAction(type, nested) {
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) {
return '<button type="button" class="button is-fullwidth mb-2" onclick="collisionWizardFinalizeAction({type:\'evenement\', evenement_id:' + id + '}, ' + (nested ? 'true' : 'false') + ')">📣 ' + CUSTOM_EVENTS_MAP[id].name + '</button>';
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 class="emoji">📣</span><span>' + CUSTOM_EVENTS_MAP[id].name.replace(/</g, '&lt;') + '</span>' +
'</button>';
}).join('');
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel événement ?</p><div class="collisionWizardChoices">' + buttons + '</div>';
}