fix(resultat): le marqueur du joueur reste visible, marqueur reel + ligne + etiquette distance + fitBounds (comparaison visuelle facon GeoGuessr)
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
guess?: Coordonnees | null;
|
guess?: Coordonnees | null;
|
||||||
real?: Coordonnees | null;
|
real?: Coordonnees | null;
|
||||||
|
distanceKm?: number | null;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
pairs?: Paire[];
|
pairs?: Paire[];
|
||||||
markers?: MarqueurInfo[];
|
markers?: MarqueurInfo[];
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
let {
|
let {
|
||||||
guess = $bindable(null),
|
guess = $bindable(null),
|
||||||
real = null,
|
real = null,
|
||||||
|
distanceKm = null,
|
||||||
interactive = true,
|
interactive = true,
|
||||||
pairs = [],
|
pairs = [],
|
||||||
markers = [],
|
markers = [],
|
||||||
@@ -48,7 +50,6 @@
|
|||||||
let guessMarker: Marker | null = null;
|
let guessMarker: Marker | null = null;
|
||||||
let realMarker: Marker | null = null;
|
let realMarker: Marker | null = null;
|
||||||
let explorerMarkers: Marker[] = [];
|
let explorerMarkers: Marker[] = [];
|
||||||
let raf = 0;
|
|
||||||
|
|
||||||
const SRC_LIGNE = 'jwe-ligne';
|
const SRC_LIGNE = 'jwe-ligne';
|
||||||
const SRC_PAIRES = 'jwe-paires';
|
const SRC_PAIRES = 'jwe-paires';
|
||||||
@@ -100,39 +101,47 @@
|
|||||||
id: 'jwe-ligne-line',
|
id: 'jwe-ligne-line',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: SRC_LIGNE,
|
source: SRC_LIGNE,
|
||||||
|
filter: ['==', ['geometry-type'], 'LineString'],
|
||||||
paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
|
paint: { 'line-color': '#e4572e', 'line-width': 3, 'line-dasharray': [2, 1.5] }
|
||||||
});
|
});
|
||||||
|
map.addLayer({
|
||||||
|
id: 'jwe-ligne-label',
|
||||||
|
type: 'symbol',
|
||||||
|
source: SRC_LIGNE,
|
||||||
|
filter: ['==', ['geometry-type'], 'Point'],
|
||||||
|
layout: {
|
||||||
|
'text-field': ['get', 'etiquette'],
|
||||||
|
'text-size': 14,
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-offset': [0, -1.4]
|
||||||
|
},
|
||||||
|
paint: { 'text-color': '#0b3d2e', 'text-halo-color': '#fffdf8', 'text-halo-width': 2 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const features: GeoJSON.Feature[] = [];
|
||||||
|
if (guess) {
|
||||||
|
features.push(ligneFeature(guess, real, null));
|
||||||
|
if (distanceKm !== null) {
|
||||||
|
// Étiquette de distance au milieu de la ligne guess ↔ réel
|
||||||
|
features.push(
|
||||||
|
pointFeature(
|
||||||
|
{ lat: (guess.lat + real.lat) / 2, lon: (guess.lon + real.lon) / 2 },
|
||||||
|
{ etiquette: `${distanceKm} km` }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const features: GeoJSON.Feature[] = guess ? [ligneFeature(guess, real, null)] : [];
|
|
||||||
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });
|
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features });
|
||||||
|
|
||||||
// Marqueur vert = position réelle
|
// Marqueur vert = position réelle ; le marqueur corail du joueur RESTE
|
||||||
|
// à sa position : la comparaison visuelle guess ↔ réel est le cœur du jeu.
|
||||||
realMarker?.remove();
|
realMarker?.remove();
|
||||||
realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);
|
realMarker = new ml.Marker({ color: '#0b3d2e' }).setLngLat([real.lon, real.lat]).addTo(map);
|
||||||
|
|
||||||
// Le marqueur du joueur « vole » vers la position réelle (sauf reduced-motion)
|
if (guess) {
|
||||||
if (guessMarker && guess) {
|
|
||||||
const from = { ...guess };
|
|
||||||
if (reducedMotion) {
|
|
||||||
guessMarker.setLngLat([real.lon, real.lat]);
|
|
||||||
} else {
|
|
||||||
const debut = performance.now();
|
|
||||||
const duree = 1400;
|
|
||||||
const step = (now: number) => {
|
|
||||||
if (!guessMarker) return;
|
|
||||||
const k = Math.min(1, (now - debut) / duree);
|
|
||||||
const e = 1 - Math.pow(1 - k, 3);
|
|
||||||
guessMarker.setLngLat([
|
|
||||||
from.lon + (real.lon - from.lon) * e,
|
|
||||||
from.lat + (real.lat - from.lat) * e
|
|
||||||
]);
|
|
||||||
if (k < 1) raf = requestAnimationFrame(step);
|
|
||||||
};
|
|
||||||
raf = requestAnimationFrame(step);
|
|
||||||
}
|
|
||||||
const bounds = new ml.LngLatBounds(
|
const bounds = new ml.LngLatBounds(
|
||||||
[Math.min(from.lon, real.lon), Math.min(from.lat, real.lat)],
|
[Math.min(guess.lon, real.lon), Math.min(guess.lat, real.lat)],
|
||||||
[Math.max(from.lon, real.lon), Math.max(from.lat, real.lat)]
|
[Math.max(guess.lon, real.lon), Math.max(guess.lat, real.lat)]
|
||||||
);
|
);
|
||||||
map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 });
|
map.fitBounds(bounds, { padding: 80, duration: reducedMotion ? 0 : 900, maxZoom: 12 });
|
||||||
} else {
|
} else {
|
||||||
@@ -141,7 +150,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nettoyerRevelation(): void {
|
function nettoyerRevelation(): void {
|
||||||
cancelAnimationFrame(raf);
|
|
||||||
realMarker?.remove();
|
realMarker?.remove();
|
||||||
realMarker = null;
|
realMarker = null;
|
||||||
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
|
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
|
||||||
@@ -311,7 +319,6 @@
|
|||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
detruit = true;
|
detruit = true;
|
||||||
cancelAnimationFrame(raf);
|
|
||||||
map?.remove();
|
map?.remove();
|
||||||
map = null;
|
map = null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -266,6 +266,7 @@
|
|||||||
<GameMap
|
<GameMap
|
||||||
bind:guess
|
bind:guess
|
||||||
real={result ? result.coordonnees : null}
|
real={result ? result.coordonnees : null}
|
||||||
|
distanceKm={result ? result.distanceKm : null}
|
||||||
interactive={!result}
|
interactive={!result}
|
||||||
onvalidate={() => void soumettre(false)}
|
onvalidate={() => void soumettre(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -260,6 +260,7 @@
|
|||||||
<GameMap
|
<GameMap
|
||||||
bind:guess
|
bind:guess
|
||||||
real={result ? result.coordonnees : null}
|
real={result ? result.coordonnees : null}
|
||||||
|
distanceKm={result ? result.distanceKm : null}
|
||||||
interactive={!result}
|
interactive={!result}
|
||||||
onvalidate={() => void soumettre(false)}
|
onvalidate={() => void soumettre(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -106,3 +106,7 @@ Règles :
|
|||||||
|
|
||||||
- **ADR-001 v1 — Défi par lien à seed** (`/defi/<seed>`) : `src/lib/server/tirage.ts` (hash cyrb53 → PRNG mulberry32, Fisher-Yates déterministe, contraintes ≥1 MONUMENT / ≥1 LIEU / ≥2 régions par échanges déterministes — zéro `Math.random`, déterminisme inter-machines). `GET /api/defi/[seed]` → 5 rounds sanitisés identiques pour tous (`no-store`, seed alphanumérique 4–40). Accueil : « Créer un lien de défi » (seed 8 chars sans ambiguïté). Bilan : « Copier le lien du défi ». La comparaison des scores reste humaine (image canvas) — zéro compte, zéro état serveur, zéro leaderboard. La v2 temps réel (PlaySocketJS) reste conditionnée au gate de traction de l'ADR (≥30 parties/sem. ×4 sem. + ≥5 demandes explicites).
|
- **ADR-001 v1 — Défi par lien à seed** (`/defi/<seed>`) : `src/lib/server/tirage.ts` (hash cyrb53 → PRNG mulberry32, Fisher-Yates déterministe, contraintes ≥1 MONUMENT / ≥1 LIEU / ≥2 régions par échanges déterministes — zéro `Math.random`, déterminisme inter-machines). `GET /api/defi/[seed]` → 5 rounds sanitisés identiques pour tous (`no-store`, seed alphanumérique 4–40). Accueil : « Créer un lien de défi » (seed 8 chars sans ambiguïté). Bilan : « Copier le lien du défi ». La comparaison des scores reste humaine (image canvas) — zéro compte, zéro état serveur, zéro leaderboard. La v2 temps réel (PlaySocketJS) reste conditionnée au gate de traction de l'ADR (≥30 parties/sem. ×4 sem. + ≥5 demandes explicites).
|
||||||
- **ADR-002 — Carte SVG légère hors-jeu** : pipeline `app/scripts/build-carte-regions.mjs` (geo.api.gouv.fr → mapshaper 7 % → `src/lib/map/regions-geo.json`, 91,8 KB gz ≤ 150 KB) ; composant `CarteRegions.svelte` from scratch (SVG, projection équirectangulaire par inset, 2×2 responsive, `<button>` par région avec `aria-label` FR/créole, SSR sans JS, prop `marqueurs` pour le récap guess→réel, attribution © OSM/données publiques). Utilisée sur l'accueil (sélection de région) et les bilans défi — MapLibre reste strictement aux écrans de jeu (devinette, résultat, explorer). Attribution : contours © geo.api.gouv.fr / données publiques.
|
- **ADR-002 — Carte SVG légère hors-jeu** : pipeline `app/scripts/build-carte-regions.mjs` (geo.api.gouv.fr → mapshaper 7 % → `src/lib/map/regions-geo.json`, 91,8 KB gz ≤ 150 KB) ; composant `CarteRegions.svelte` from scratch (SVG, projection équirectangulaire par inset, 2×2 responsive, `<button>` par région avec `aria-label` FR/créole, SSR sans JS, prop `marqueurs` pour le récap guess→réel, attribution © OSM/données publiques). Utilisée sur l'accueil (sélection de région) et les bilans défi — MapLibre reste strictement aux écrans de jeu (devinette, résultat, explorer). Attribution : contours © geo.api.gouv.fr / données publiques.
|
||||||
|
|
||||||
|
## 9. Correctif UX résultat (2026-07-17)
|
||||||
|
|
||||||
|
- **Carte résultat façon GeoGuessr** : à la révélation, le marqueur du joueur **reste à sa position** (corail) et la position réelle apparaît (vert), reliées par une ligne pointillée avec **étiquette de distance au milieu** (`distanceKm`) ; la carte cadre les deux points (`fitBounds`, instantané si `prefers-reduced-motion`). Remplace l'ancien « vol » du marqueur vers la position réelle, qui détruisait la comparaison visuelle. Prop `distanceKm` ajoutée à `GameMap` (alimentée par la réponse serveur, jamais calculée côté client).
|
||||||
|
|||||||
Reference in New Issue
Block a user