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 {
|
||||
guess?: Coordonnees | null;
|
||||
real?: Coordonnees | null;
|
||||
distanceKm?: number | null;
|
||||
interactive?: boolean;
|
||||
pairs?: Paire[];
|
||||
markers?: MarqueurInfo[];
|
||||
@@ -34,6 +35,7 @@
|
||||
let {
|
||||
guess = $bindable(null),
|
||||
real = null,
|
||||
distanceKm = null,
|
||||
interactive = true,
|
||||
pairs = [],
|
||||
markers = [],
|
||||
@@ -48,7 +50,6 @@
|
||||
let guessMarker: Marker | null = null;
|
||||
let realMarker: Marker | null = null;
|
||||
let explorerMarkers: Marker[] = [];
|
||||
let raf = 0;
|
||||
|
||||
const SRC_LIGNE = 'jwe-ligne';
|
||||
const SRC_PAIRES = 'jwe-paires';
|
||||
@@ -100,39 +101,47 @@
|
||||
id: 'jwe-ligne-line',
|
||||
type: 'line',
|
||||
source: SRC_LIGNE,
|
||||
filter: ['==', ['geometry-type'], 'LineString'],
|
||||
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 });
|
||||
|
||||
// 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 = 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 (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);
|
||||
}
|
||||
if (guess) {
|
||||
const bounds = new ml.LngLatBounds(
|
||||
[Math.min(from.lon, real.lon), Math.min(from.lat, real.lat)],
|
||||
[Math.max(from.lon, real.lon), Math.max(from.lat, real.lat)]
|
||||
[Math.min(guess.lon, real.lon), Math.min(guess.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 });
|
||||
} else {
|
||||
@@ -141,7 +150,6 @@
|
||||
}
|
||||
|
||||
function nettoyerRevelation(): void {
|
||||
cancelAnimationFrame(raf);
|
||||
realMarker?.remove();
|
||||
realMarker = null;
|
||||
setGeoJSON(SRC_LIGNE, { type: 'FeatureCollection', features: [] });
|
||||
@@ -311,7 +319,6 @@
|
||||
})();
|
||||
return () => {
|
||||
detruit = true;
|
||||
cancelAnimationFrame(raf);
|
||||
map?.remove();
|
||||
map = null;
|
||||
};
|
||||
|
||||
@@ -266,6 +266,7 @@
|
||||
<GameMap
|
||||
bind:guess
|
||||
real={result ? result.coordonnees : null}
|
||||
distanceKm={result ? result.distanceKm : null}
|
||||
interactive={!result}
|
||||
onvalidate={() => void soumettre(false)}
|
||||
/>
|
||||
|
||||
@@ -260,6 +260,7 @@
|
||||
<GameMap
|
||||
bind:guess
|
||||
real={result ? result.coordonnees : null}
|
||||
distanceKm={result ? result.distanceKm : null}
|
||||
interactive={!result}
|
||||
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-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