Éditeur de collision : cartes "pièces qui s'emboîtent", animées
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:
co-authored by
Claude Sonnet 5
parent
ee270d0286
commit
b64541856f
@@ -37,22 +37,28 @@ function collisionRuleActionSummary(action) {
|
|||||||
return COLLISION_ACTION_LABELS[action.type] || action.type;
|
return COLLISION_ACTION_LABELS[action.type] || action.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Une règle = une chaîne de "maillons" (déclencheur -> action -> sous-action
|
// Une règle = une chaîne de "maillons" en forme de pièces qui s'emboîtent
|
||||||
// éventuelle) — chacun sa propre petite carte, visuellement emboîtées.
|
// (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) {
|
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;
|
var action = rule.action;
|
||||||
while (action.type === 'interagir' && action.sub_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;
|
action = action.sub_action;
|
||||||
}
|
}
|
||||||
var linksHtml = links.map(function (text) {
|
var linksHtml = links.map(function (link, i) {
|
||||||
return '<span class="collisionRuleLink">' + text.replace(/</g, '<') + '</span>';
|
return '<span class="collisionRuleLink" data-kind="' + link.kind + '" style="animation-delay:' + (i * 60) + 'ms">' +
|
||||||
}).join('<span class="collisionRuleArrow">→</span>');
|
link.text.replace(/</g, '<') + '</span>';
|
||||||
|
}).join('');
|
||||||
return (
|
return (
|
||||||
'<div class="collisionRuleChain">' +
|
'<div class="collisionRuleChain">' +
|
||||||
'<div class="collisionRuleChainLinks">' + linksHtml + '</div>' +
|
'<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>' +
|
'onclick="deleteCollisionRule(' + objectId + ', ' + ruleIndex + ')">🗑️</button>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
@@ -69,7 +75,7 @@ function collisionRuleCardHtml(obj) {
|
|||||||
'<h3 class="collisionRuleCardTitle">' + collisionRuleObjectLabel(obj).replace(/</g, '<') + '</h3>' +
|
'<h3 class="collisionRuleCardTitle">' + collisionRuleObjectLabel(obj).replace(/</g, '<') + '</h3>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'<div class="collisionRuleCardRules">' + rulesHtml + '</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>'
|
'</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>';
|
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;
|
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) {
|
function collisionRuleObjectById(objectId) {
|
||||||
@@ -136,16 +146,29 @@ function collisionWizardBodyEl() {
|
|||||||
|
|
||||||
// Étape 1 — déclencheur : collision directe, ou périmètre (px réglable,
|
// Étape 1 — déclencheur : collision directe, ou périmètre (px réglable,
|
||||||
// valeur par défaut COLLISION_DEFAULT_PERIMETRE_PX).
|
// 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() {
|
function renderCollisionWizardTriggerStep() {
|
||||||
|
var collision = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.collision);
|
||||||
|
var perimetre = collisionSplitEmojiLabel(COLLISION_TRIGGER_LABELS.perimetre);
|
||||||
collisionWizardBodyEl().innerHTML =
|
collisionWizardBodyEl().innerHTML =
|
||||||
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
|
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
|
||||||
'<div class="collisionWizardChoices">' +
|
'<div class="collisionWizardChoices">' +
|
||||||
'<button type="button" class="button is-fullwidth mb-2" onclick="collisionWizardChooseTrigger(\'collision\')">' + COLLISION_TRIGGER_LABELS.collision + '</button>' +
|
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChooseTrigger(\'collision\')">' +
|
||||||
'<div class="fieldRow" style="align-items:center; gap:8px;">' +
|
'<span class="emoji">' + collision.emoji + '</span><span>' + collision.label + '</span>' +
|
||||||
'<button type="button" class="button is-fullwidth" onclick="collisionWizardChooseTrigger(\'perimetre\')">' + COLLISION_TRIGGER_LABELS.perimetre + '</button>' +
|
'</button>' +
|
||||||
'<input type="number" min="1" id="collisionWizardPerimetrePx" value="' + COLLISION_DEFAULT_PERIMETRE_PX + '" style="width:80px;" class="input is-small">' +
|
'<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 class="hint">px</span>' +
|
||||||
'</div>' +
|
'</span>' +
|
||||||
|
'</button>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +187,11 @@ function collisionWizardChooseTrigger(trigger) {
|
|||||||
// quelle action exécuter.
|
// quelle action exécuter.
|
||||||
function renderCollisionWizardActionStep(nested) {
|
function renderCollisionWizardActionStep(nested) {
|
||||||
var types = nested ? COLLISION_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_ACTION_TYPES;
|
var types = nested ? COLLISION_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_ACTION_TYPES;
|
||||||
var buttons = types.map(function (type) {
|
var buttons = types.map(function (type, i) {
|
||||||
return '<button type="button" class="button is-fullwidth mb-2" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' + COLLISION_ACTION_LABELS[type] + '</button>';
|
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('');
|
}).join('');
|
||||||
collisionWizardBodyEl().innerHTML =
|
collisionWizardBodyEl().innerHTML =
|
||||||
'<p class="hint mb-2">' + (nested ? 'Une fois la touche pressée...' : 'Alors...') + '</p>' +
|
'<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>';
|
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;
|
return;
|
||||||
}
|
}
|
||||||
var buttons = eventIds.map(function (id) {
|
var buttons = eventIds.map(function (id, i) {
|
||||||
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>';
|
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, '<') + '</span>' +
|
||||||
|
'</button>';
|
||||||
}).join('');
|
}).join('');
|
||||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel événement ?</p><div class="collisionWizardChoices">' + buttons + '</div>';
|
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel événement ?</p><div class="collisionWizardChoices">' + buttons + '</div>';
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-12
@@ -676,28 +676,80 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
|||||||
métaphore "pièces qui s'emboîtent" demandée, sans un vrai graphe à
|
métaphore "pièces qui s'emboîtent" demandée, sans un vrai graphe à
|
||||||
tracer à la souris (l'assistant "+ Action" construit la chaîne pas à
|
tracer à la souris (l'assistant "+ Action" construit la chaîne pas à
|
||||||
pas, voir openCollisionRuleModal). */
|
pas, voir openCollisionRuleModal). */
|
||||||
.collisionRulesCards{ display:flex; flex-direction:column; gap:14px; margin-top:12px; }
|
@keyframes collisionCardIn{ from{ opacity:0; transform:translateY(10px) scale(.97); } to{ opacity:1; transform:none; } }
|
||||||
|
@keyframes collisionLinkIn{ from{ opacity:0; transform:translateX(-10px) scale(.9); } to{ opacity:1; transform:none; } }
|
||||||
|
@keyframes collisionChoiceIn{ from{ opacity:0; transform:translateY(8px) scale(.92); } to{ opacity:1; transform:none; } }
|
||||||
|
|
||||||
|
.collisionRulesCards{ display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:18px; margin-top:14px; }
|
||||||
.collisionRuleCard{
|
.collisionRuleCard{
|
||||||
background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:14px;
|
position:relative; background:linear-gradient(165deg, var(--panel), var(--panel2));
|
||||||
|
border:1px solid var(--border); border-radius:16px; padding:16px;
|
||||||
|
box-shadow:0 6px 18px rgba(0,0,0,.18); animation:collisionCardIn .35s ease backwards;
|
||||||
|
transition:transform .18s ease, box-shadow .18s ease;
|
||||||
}
|
}
|
||||||
.collisionRuleCardHeader{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }
|
.collisionRuleCard:hover{ transform:translateY(-3px); box-shadow:0 10px 26px rgba(0,0,0,.28); }
|
||||||
|
.collisionRuleCardHeader{ display:flex; align-items:center; gap:10px; margin-bottom:12px; }
|
||||||
.collisionRuleCardThumb{
|
.collisionRuleCardThumb{
|
||||||
width:36px; height:36px; border-radius:6px; overflow:hidden; background:var(--panel2);
|
width:40px; height:40px; border-radius:10px; overflow:hidden; background:var(--panel2);
|
||||||
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
display:flex; align-items:center; justify-content:center; flex-shrink:0;
|
||||||
|
border:1px solid var(--border); box-shadow:inset 0 0 0 2px rgba(255,255,255,.03);
|
||||||
}
|
}
|
||||||
.collisionRuleCardThumb img{ max-width:100%; max-height:100%; object-fit:contain; }
|
.collisionRuleCardThumb img{ max-width:100%; max-height:100%; object-fit:contain; }
|
||||||
.collisionRuleCardTitle{ margin:0; font-size:14px; }
|
.collisionRuleCardTitle{ margin:0; font-size:15px; font-weight:700; }
|
||||||
.collisionRuleCardRules{ display:flex; flex-direction:column; gap:8px; margin-bottom:10px; }
|
.collisionRuleCardRules{ display:flex; flex-direction:column; gap:10px; margin-bottom:14px; }
|
||||||
|
|
||||||
|
/* 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 simple liste
|
||||||
|
de pastilles à plat — chaque maillon chevauche le précédent via une
|
||||||
|
marge négative + une pointe en clip-path, coloré selon son type. */
|
||||||
.collisionRuleChain{
|
.collisionRuleChain{
|
||||||
display:flex; align-items:center; justify-content:space-between; gap:8px;
|
display:flex; align-items:center; gap:10px; background:rgba(0,0,0,.15);
|
||||||
background:var(--panel2); border:1px solid var(--border); border-radius:8px; padding:6px 8px;
|
border:1px solid var(--border); border-radius:12px; padding:8px 10px;
|
||||||
}
|
}
|
||||||
.collisionRuleChainLinks{ display:flex; align-items:center; flex-wrap:wrap; gap:6px; }
|
.collisionRuleChainLinks{ display:flex; align-items:center; flex-wrap:wrap; row-gap:6px; }
|
||||||
.collisionRuleLink{
|
.collisionRuleLink{
|
||||||
background:var(--accent); color:#fff; border-radius:12px; padding:3px 10px; font-size:12px; white-space:nowrap;
|
--link-color:var(--accent);
|
||||||
|
position:relative; display:inline-flex; align-items:center; gap:4px;
|
||||||
|
background:linear-gradient(135deg, var(--link-color), color-mix(in srgb, var(--link-color) 70%, #000 30%));
|
||||||
|
color:#fff; padding:7px 16px 7px 20px; font-size:12.5px; font-weight:600; white-space:nowrap;
|
||||||
|
clip-path:polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%, 10% 50%);
|
||||||
|
margin-left:-12px; z-index:1; animation:collisionLinkIn .3s ease backwards;
|
||||||
|
transition:transform .15s ease, filter .15s ease;
|
||||||
}
|
}
|
||||||
.collisionRuleArrow{ color:var(--forge-text-muted, #8b95a7); font-size:12px; }
|
.collisionRuleLink:first-child{ margin-left:0; clip-path:polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%); padding-left:14px; border-radius:8px 0 0 8px; }
|
||||||
.collisionWizardChoices{ display:flex; flex-direction:column; }
|
.collisionRuleLink:hover{ transform:translateY(-2px) scale(1.04); filter:brightness(1.12); z-index:2; }
|
||||||
|
.collisionRuleLink[data-kind="trigger"]{ --link-color:#f2994a; }
|
||||||
|
.collisionRuleLink[data-kind="quete"]{ --link-color:#9b59b6; }
|
||||||
|
.collisionRuleLink[data-kind="attaque"]{ --link-color:#e0524d; }
|
||||||
|
.collisionRuleLink[data-kind="evenement"]{ --link-color:#17a2b8; }
|
||||||
|
.collisionRuleLink[data-kind="interagir"]{ --link-color:#d4a72c; }
|
||||||
|
.collisionRuleChainDelete{
|
||||||
|
margin-left:auto; flex-shrink:0; width:26px; height:26px; border-radius:50%; border:none;
|
||||||
|
background:rgba(224,82,77,.15); color:#e0524d; display:flex; align-items:center; justify-content:center;
|
||||||
|
cursor:pointer; transition:background .15s ease, transform .15s ease; font-size:13px;
|
||||||
|
}
|
||||||
|
.collisionRuleChainDelete:hover{ background:#e0524d; color:#fff; transform:scale(1.08); }
|
||||||
|
|
||||||
|
.collisionRuleAddBtn{
|
||||||
|
display:inline-flex; align-items:center; gap:6px; background:linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 70%, #000 30%));
|
||||||
|
color:#fff; border:none; border-radius:999px; padding:8px 16px; font-size:13px; font-weight:700;
|
||||||
|
cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.25); transition:transform .15s ease, box-shadow .15s ease;
|
||||||
|
}
|
||||||
|
.collisionRuleAddBtn:hover{ transform:translateY(-2px) scale(1.03); box-shadow:0 6px 18px rgba(0,0,0,.35); }
|
||||||
|
.collisionRuleAddBtn:active{ transform:translateY(0) scale(.98); }
|
||||||
|
|
||||||
|
/* Assistant "+ Action" — une grille de cartes-choix animées en cascade
|
||||||
|
plutôt qu'une pile de boutons pleine largeur. */
|
||||||
|
.collisionWizardChoices{ display:grid; grid-template-columns:repeat(auto-fill, minmax(190px, 1fr)); gap:10px; }
|
||||||
|
.collisionWizardChoice{
|
||||||
|
display:flex; flex-direction:column; align-items:flex-start; gap:4px; text-align:left;
|
||||||
|
background:var(--panel2); border:1px solid var(--border); border-radius:12px; padding:12px 14px;
|
||||||
|
cursor:pointer; font-size:13.5px; font-weight:600; color:inherit;
|
||||||
|
animation:collisionChoiceIn .25s ease backwards; transition:transform .15s ease, border-color .15s ease, box-shadow .15s ease;
|
||||||
|
}
|
||||||
|
.collisionWizardChoice:hover{ transform:translateY(-3px) scale(1.02); border-color:var(--accent); box-shadow:0 8px 18px rgba(0,0,0,.25); }
|
||||||
|
.collisionWizardChoice .emoji{ font-size:22px; }
|
||||||
|
|
||||||
.collisionInteractBubble{ display:none; position:absolute; left:50%; bottom:100%; transform:translateX(-50%); margin-bottom:6px; background:#111; color:#fff; border-radius:10px; padding:4px 10px; font-size:12px; white-space:nowrap; pointer-events:none; z-index:20; }
|
.collisionInteractBubble{ display:none; position:absolute; left:50%; bottom:100%; transform:translateX(-50%); margin-bottom:6px; background:#111; color:#fff; border-radius:10px; padding:4px 10px; font-size:12px; white-space:nowrap; pointer-events:none; z-index:20; }
|
||||||
|
|
||||||
.listRow.selected{ border-color:var(--accent); background:var(--panel2); }
|
.listRow.selected{ border-color:var(--accent); background:var(--panel2); }
|
||||||
|
|||||||
Reference in New Issue
Block a user