Phase 5 : position/déplacement d'élément + condition de collision
Ajoute le positionnement absolu (pos_x/pos_y, réutilise left/top en % déjà en place) et relatif (pos_x_relatif/pos_y_relatif, ajoute un delta à la position actuelle plutôt que de l'écraser) comme nouvelles propriétés de l'action "Modifier un élément". Ajoute une nouvelle source de condition "collision" (aux côtés de "objet"/"variable") : deux éléments (cond_element_a/cond_element_b, ALTER TABLE sans contrainte FK, même patron que block_id/ trigger_custom_event_id) dont on compare les rectangles à l'écran via getBoundingClientRect() côté client (elementsOverlap(), dans conditions.js). Pas d'opérateur/valeur à choisir : le chevauchement EST directement le booléen vrai/faux du nœud — le créateur relie le port "Faux" pour "ne se touchent pas", exactement comme pour n'importe quelle autre condition (design plus simple que réinterpréter égal/différent, qui n'a pas de sens pour superieur/inferieur). delete_element.py et flow_nodes_referencing_element.py nettoient désormais aussi les nœuds de collision référençant un élément supprimé (ou l'un de ses descendants), pour rester cohérents avec le nettoyage déjà en place pour trigger_element_id/target_element_id. Combiné à la Phase 3 (minuteur récurrent) et au déplacement au clavier, ça couvre des jeux type casse-briques/Pong/ramasse-objets sans construire un vrai moteur physique (pas de vélocité/accélération/ gravité continues, cadrage volontairement limité). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
92fbfbc9dd
commit
9e263435e6
@@ -12,6 +12,12 @@ var ELEMENT_PROPERTY_CSS = {
|
||||
border_color: { css: 'borderColor', suffix: '' },
|
||||
width: { css: 'width', suffix: '%' },
|
||||
height: { css: 'height', suffix: '%' },
|
||||
// Phase 5 (moteur) — les éléments sont DÉJÀ positionnés en absolu avec
|
||||
// left/top en % (voir filters/element_style_filter.py) : piloter ces
|
||||
// deux propriétés est donc cohérent avec l'existant, pas une nouvelle
|
||||
// mécanique de positionnement.
|
||||
pos_x: { css: 'left', suffix: '%' },
|
||||
pos_y: { css: 'top', suffix: '%' },
|
||||
};
|
||||
|
||||
function applyElementProperty(targetEl, property, value) {
|
||||
@@ -70,6 +76,19 @@ function applyElementProperty(targetEl, property, value) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (property === 'pos_x_relatif' || property === 'pos_y_relatif') {
|
||||
// Phase 5 — AJOUTE la valeur à la position actuelle plutôt que de
|
||||
// l'écraser (contrairement à pos_x/pos_y, "absolus") : combiné à un
|
||||
// déclencheur clavier (Phase 3), ça permet un personnage/objet qui se
|
||||
// déplace pas à pas à chaque appui de touche plutôt que de sauter
|
||||
// toujours au même endroit.
|
||||
const relSpec = ELEMENT_PROPERTY_CSS[property === 'pos_x_relatif' ? 'pos_x' : 'pos_y'];
|
||||
const delta = parseFloat(value);
|
||||
if (isNaN(delta)) return;
|
||||
const current = parseFloat(targetEl.style[relSpec.css]) || 0;
|
||||
targetEl.style[relSpec.css] = (current + delta) + relSpec.suffix;
|
||||
return;
|
||||
}
|
||||
|
||||
const spec = ELEMENT_PROPERTY_CSS[property];
|
||||
if (!spec || value === undefined || value === null) return;
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
// Extrait de templates/play.html (voir plan de modularisation) : partie
|
||||
// PURE du moteur (aucun accès DOM) — lecture de champs/variables et
|
||||
// comparaison, c'est la logique la plus amenée à grossir (nouvelles
|
||||
// opérations, condition de collision...), donc la plus utile à tester
|
||||
// (voir static/js/play/__tests__/conditions.test.js).
|
||||
// opérations...), donc la plus utile à tester (voir
|
||||
// static/js/play/__tests__/conditions.test.js). SEULE EXCEPTION :
|
||||
// elementsOverlap() (Phase 5, condition de collision) touche forcément le
|
||||
// DOM (getBoundingClientRect) — isolée dans sa propre fonction, non
|
||||
// testée par node:test (voir son commentaire), le reste du fichier reste
|
||||
// pur et testable.
|
||||
|
||||
// Lit la valeur actuelle d'un champ d'objet dans l'instantané de données
|
||||
// du jeu (gameData.data), pour l'évaluation d'une condition.
|
||||
@@ -97,10 +101,33 @@ function resolveVariablePath(rawValue, path) {
|
||||
// comportement historique), soit une VARIABLE GLOBALE (source
|
||||
// "variable" — voir ensure_flow_schema.py pour cond_source/cond_variable/
|
||||
// cond_variable_chemin).
|
||||
// Phase 5 — condition de collision : chevauchement des rectangles de
|
||||
// deux éléments (getBoundingClientRect, donc leur position RÉELLE à
|
||||
// l'écran — reflète les déplacements posés via "Modifier un élément →
|
||||
// Position", voir static/js/play/actions.js). Aucun élément trouvé (id
|
||||
// invalide, élément masqué avec display:none — getBoundingClientRect
|
||||
// renvoie alors une boîte de largeur/hauteur nulles) => pas de
|
||||
// chevauchement plutôt que de planter.
|
||||
function elementsOverlap(elementIdA, elementIdB) {
|
||||
const a = document.querySelector('[data-element-id="' + elementIdA + '"]');
|
||||
const b = document.querySelector('[data-element-id="' + elementIdB + '"]');
|
||||
if (!a || !b) return false;
|
||||
const r1 = a.getBoundingClientRect(), r2 = b.getBoundingClientRect();
|
||||
return r1.left < r2.right && r1.right > r2.left && r1.top < r2.bottom && r1.bottom > r2.top;
|
||||
}
|
||||
|
||||
function evaluateConditionClause(clause) {
|
||||
const source = clause.source ?? clause.cond_source ?? 'objet';
|
||||
const operator = clause.operator ?? clause.cond_operator;
|
||||
const expected = clause.value ?? clause.cond_value;
|
||||
if (source === 'collision') {
|
||||
// Pas d'opérateur/valeur à comparer (contrairement à "objet"/
|
||||
// "variable") : le chevauchement EST directement le booléen vrai/faux
|
||||
// du nœud — pour "ne se touchent pas", le créateur relie simplement
|
||||
// le port "Faux" plutôt que "Vrai" dans le graphe, comme pour
|
||||
// n'importe quelle autre condition.
|
||||
return elementsOverlap(clause.element_a ?? clause.cond_element_a, clause.element_b ?? clause.cond_element_b);
|
||||
}
|
||||
if (source === 'variable') {
|
||||
const varName = clause.variable ?? clause.cond_variable;
|
||||
const path = clause.variable_chemin ?? clause.cond_variable_chemin;
|
||||
|
||||
@@ -50,8 +50,18 @@ function nodeLabel(node) {
|
||||
var clauseSubject = function(c, field, variable) {
|
||||
return (c.source ?? c.cond_source) === 'variable' ? ('🔧 ' + (variable || c.cond_variable || '?')) : (field || c.cond_field || '?');
|
||||
};
|
||||
var mainSubject = clauseSubject(node, node.cond_field, node.cond_variable);
|
||||
var mainClause = '❓ SI ' + mainSubject + ' ' + (CONDITION_OPERATOR_LABELS_MAP[node.cond_operator] || node.cond_operator) + ' ' + (node.cond_value || '');
|
||||
// Phase 5 — condition de collision : pas de champ/opérateur/valeur,
|
||||
// juste les deux éléments comparés (voir evaluateConditionClause()
|
||||
// dans static/js/play/conditions.js).
|
||||
var clauseFullLabel = function(c, field, variable) {
|
||||
if ((c.source ?? c.cond_source) === 'collision') {
|
||||
var elA = ELEMENTS_LABELS[c.element_a ?? c.cond_element_a] || '?';
|
||||
var elB = ELEMENTS_LABELS[c.element_b ?? c.cond_element_b] || '?';
|
||||
return '💥 ' + elA + ' touche ' + elB;
|
||||
}
|
||||
return clauseSubject(c, field, variable) + ' ' + (CONDITION_OPERATOR_LABELS_MAP[c.operator ?? c.cond_operator] || (c.operator ?? c.cond_operator)) + ' ' + ((c.value ?? c.cond_value) || '');
|
||||
};
|
||||
var mainClause = '❓ SI ' + clauseFullLabel(node, node.cond_field, node.cond_variable);
|
||||
if (node.cond_clauses) {
|
||||
// 2.4 — plusieurs clauses combinées : on affiche chaque champ/variable
|
||||
// testé, relié par le connecteur ET/OU choisi, plutôt qu'une seule clause.
|
||||
@@ -59,7 +69,7 @@ function nodeLabel(node) {
|
||||
var clauses = JSON.parse(node.cond_clauses);
|
||||
var combLabel = node.cond_combinator === 'ou' ? ' OU ' : ' ET ';
|
||||
return '❓ SI ' + clauses.map(function(c) {
|
||||
return clauseSubject(c, c.field, c.variable) + ' ' + (CONDITION_OPERATOR_LABELS_MAP[c.operator] || c.operator) + ' ' + (c.value || '');
|
||||
return clauseFullLabel(c, c.field, c.variable);
|
||||
}).join(combLabel);
|
||||
} catch (e) { /* JSON invalide : on retombe sur la clause historique */ }
|
||||
}
|
||||
@@ -363,11 +373,18 @@ function onFlowFieldChange() {
|
||||
// pour une clause SUPPLÉMENTAIRE : voir onFlowClauseSourceChange() plus
|
||||
// bas, sur le même principe mais scopée par index.
|
||||
function onFlowConditionSourceChange() {
|
||||
var isVariable = document.getElementById('nf-cond-source').value === 'variable';
|
||||
document.getElementById('nf-cond-objet-fields').style.display = isVariable ? 'none' : 'block';
|
||||
var source = document.getElementById('nf-cond-source').value;
|
||||
var isVariable = source === 'variable';
|
||||
var isCollision = source === 'collision';
|
||||
document.getElementById('nf-cond-objet-fields').style.display = (isVariable || isCollision) ? 'none' : 'block';
|
||||
document.getElementById('nf-cond-variable-fields').style.display = isVariable ? 'block' : 'none';
|
||||
var hint = document.getElementById('nf-cond-no-objet-hint');
|
||||
if (hint) hint.style.display = isVariable ? 'none' : 'block';
|
||||
if (hint) hint.style.display = (isVariable || isCollision) ? 'none' : 'block';
|
||||
// Collision (Phase 5) : ni opérateur ni valeur de comparaison — le
|
||||
// chevauchement EST directement le booléen vrai/faux du nœud (voir
|
||||
// evaluateConditionClause() dans static/js/play/conditions.js).
|
||||
document.getElementById('nf-cond-collision-fields').style.display = isCollision ? 'block' : 'none';
|
||||
document.getElementById('nf-cond-operator-value-row').style.display = isCollision ? 'none' : 'block';
|
||||
}
|
||||
|
||||
// ---------- 2.4 — clauses supplémentaires (ET/OU) d'un nœud Condition ----------
|
||||
@@ -572,20 +589,30 @@ function submitNodeForm() {
|
||||
} else if (currentNodeFormType === 'condition') {
|
||||
var condSource = document.getElementById('nf-cond-source').value;
|
||||
payload.cond_source = condSource;
|
||||
payload.cond_operator = document.getElementById('nf-cond-operator').value;
|
||||
payload.cond_value = document.getElementById('nf-cond-value').value;
|
||||
if (condSource === 'variable') {
|
||||
payload.cond_variable = document.getElementById('nf-cond-variable').value;
|
||||
payload.cond_variable_chemin = document.getElementById('nf-cond-variable-chemin').value;
|
||||
if (condSource === 'collision') {
|
||||
// Phase 5 — ni opérateur ni valeur de comparaison pour une
|
||||
// collision (voir onFlowConditionSourceChange()) : juste les deux
|
||||
// éléments dont on compare les rectangles. Pas de clauses combinées
|
||||
// (ET/OU) pour une collision — même limitation assumée que pour les
|
||||
// autres sources tant qu'aucune clause supplémentaire n'a été ajoutée.
|
||||
payload.cond_element_a = parseInt(document.getElementById('nf-cond-element-a').value, 10);
|
||||
payload.cond_element_b = parseInt(document.getElementById('nf-cond-element-b').value, 10);
|
||||
} else {
|
||||
var fieldSel = document.getElementById('nf-cond-field');
|
||||
var fieldOpt = fieldSel.options[fieldSel.selectedIndex];
|
||||
payload.cond_definition_id = parseInt(document.getElementById('nf-cond-definition').value, 10);
|
||||
payload.cond_row_id = parseInt(document.getElementById('nf-cond-row').value, 10) || null;
|
||||
payload.cond_field = fieldSel.value;
|
||||
payload.cond_field_type = fieldOpt ? fieldOpt.dataset.type : '';
|
||||
payload.cond_operator = document.getElementById('nf-cond-operator').value;
|
||||
payload.cond_value = document.getElementById('nf-cond-value').value;
|
||||
if (condSource === 'variable') {
|
||||
payload.cond_variable = document.getElementById('nf-cond-variable').value;
|
||||
payload.cond_variable_chemin = document.getElementById('nf-cond-variable-chemin').value;
|
||||
} else {
|
||||
var fieldSel = document.getElementById('nf-cond-field');
|
||||
var fieldOpt = fieldSel.options[fieldSel.selectedIndex];
|
||||
payload.cond_definition_id = parseInt(document.getElementById('nf-cond-definition').value, 10);
|
||||
payload.cond_row_id = parseInt(document.getElementById('nf-cond-row').value, 10) || null;
|
||||
payload.cond_field = fieldSel.value;
|
||||
payload.cond_field_type = fieldOpt ? fieldOpt.dataset.type : '';
|
||||
}
|
||||
}
|
||||
if (flowConditionExtraClauses.length) {
|
||||
if (condSource !== 'collision' && flowConditionExtraClauses.length) {
|
||||
// 2.4 — clauses combinées : la clause "historique" ci-dessus devient
|
||||
// la première clause de la liste, suivie des clauses supplémentaires.
|
||||
var mainClause = condSource === 'variable'
|
||||
|
||||
Reference in New Issue
Block a user