193 lines
4.7 KiB
Svelte
193 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import type { CheckData, CheckResponse, ClientGameData, MapData } from "$lib/types";
|
|
import { page } from "$app/stores";
|
|
import { onMount } from "svelte";
|
|
import L from "leaflet";
|
|
|
|
import "leaflet/dist/leaflet.css";
|
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
|
import "leaflet-defaulticon-compatibility";
|
|
|
|
const zoom = 13;
|
|
const center: [number, number] = [45.742858495, 4.86163814];
|
|
|
|
const metroIcon = new L.Icon({ iconUrl: "/metro.svg", iconSize: [29, 15] });
|
|
const tramIcon = new L.Icon({ iconUrl: "/tram.svg", iconSize: [29, 15] });
|
|
|
|
let mapPromise = $state(fetchMap());
|
|
let gamePromise = $state(fetchGame());
|
|
let isChecking = $state(false);
|
|
let results: CheckResponse | null = $state(null);
|
|
|
|
let currentIndex = $state(0);
|
|
|
|
let map: L.Map | null = $state(null);
|
|
let playerMarker: L.Marker | null = $state(null);
|
|
let solutionMarker: L.Marker | null = $state(null);
|
|
let solutionLine: L.Polyline | null = $state(null);
|
|
|
|
onMount(() => {
|
|
window.addEventListener("beforeunload", async (e) => {
|
|
const gameData = await gamePromise;
|
|
if (gameData.stopNames.length > currentIndex + 1) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
|
|
async function fetchMap(): Promise<MapData> {
|
|
return fetch("/api/map?" + $page.url.searchParams).then((r) => r.json());
|
|
}
|
|
|
|
async function fetchGame(): Promise<ClientGameData> {
|
|
return fetch("/api/game?" + $page.url.searchParams).then((r) => r.json());
|
|
}
|
|
|
|
mapPromise.then((mapData) => {
|
|
map = L.map("map").setView(center, zoom);
|
|
|
|
L.tileLayer(`https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png`, {
|
|
attribution:
|
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
maxZoom: 20,
|
|
}).addTo(map);
|
|
|
|
// we know map isn't null
|
|
mapData.lines.forEach(([feature, color]) => {
|
|
L.geoJSON(feature, { style: { color } }).addTo(map!);
|
|
});
|
|
|
|
const icon = $page.url.searchParams.get("stops_type") === "tram" ? tramIcon : metroIcon;
|
|
mapData.stops.forEach((coords) => {
|
|
const marker = L.marker(coords, { icon }).addTo(map!);
|
|
marker.on("click", (e) => setMarker(e.latlng));
|
|
});
|
|
|
|
map.on("click", (e) => setMarker(e.latlng));
|
|
map.on("keydown", (e) => {
|
|
if (e.originalEvent.key === " ") {
|
|
checkLocation();
|
|
}
|
|
});
|
|
});
|
|
|
|
function setMarker(pos: L.LatLng) {
|
|
if (map && !results) {
|
|
playerMarker = (playerMarker ?? L.marker(pos).addTo(map)).setLatLng(pos);
|
|
}
|
|
}
|
|
|
|
async function checkLocation() {
|
|
if (!playerMarker || results || isChecking) return;
|
|
isChecking = true;
|
|
|
|
const gameData = await gamePromise;
|
|
|
|
const checkData: CheckData = {
|
|
gameId: gameData.gameId,
|
|
stopName: gameData.stopNames[currentIndex],
|
|
latlng: [playerMarker.getLatLng().lat, playerMarker.getLatLng().lng],
|
|
};
|
|
|
|
const response = await fetch("/api/check", {
|
|
method: "POST",
|
|
body: JSON.stringify(checkData),
|
|
});
|
|
|
|
if (response.ok && map) {
|
|
const res: CheckResponse = await response.json();
|
|
results = res;
|
|
|
|
solutionLine = L.polyline([checkData.latlng, res.solution], {
|
|
color: "black",
|
|
weight: 6,
|
|
}).addTo(map);
|
|
|
|
map.flyToBounds(solutionLine.getBounds(), { duration: 1 });
|
|
|
|
solutionMarker = L.marker(res.solution)
|
|
.bindPopup(gameData.stopNames[currentIndex])
|
|
.addTo(map)
|
|
.openPopup();
|
|
} else {
|
|
alert(`une erreur est survenue: ${response.status} ${await response.text()}`);
|
|
}
|
|
|
|
isChecking = false;
|
|
}
|
|
|
|
async function restartGame() {
|
|
if (!map) return;
|
|
const gameData = await gamePromise;
|
|
|
|
playerMarker?.removeFrom(map);
|
|
playerMarker = null;
|
|
|
|
solutionLine?.removeFrom(map);
|
|
solutionLine = null;
|
|
|
|
solutionMarker?.removeFrom(map);
|
|
solutionMarker = null;
|
|
|
|
map.setView(center, zoom);
|
|
results = null;
|
|
|
|
if (gameData.stopNames.length <= currentIndex + 1) {
|
|
window.location.href = "/results?" + new URLSearchParams({ gameId: gameData.gameId });
|
|
} else {
|
|
currentIndex++;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>TCL-Guessr</title>
|
|
</svelte:head>
|
|
|
|
{#await Promise.all([mapPromise, gamePromise])}
|
|
<h1>Chargement...</h1>
|
|
|
|
<div><button disabled>Chargement...</button></div>
|
|
{:then [, gameData]}
|
|
<h1>{gameData.stopNames[currentIndex]}</h1>
|
|
|
|
<div>
|
|
{#if results === null}
|
|
<button onclick={checkLocation} disabled={!playerMarker || isChecking}>VÉRIFIER</button>
|
|
{:else}
|
|
<button onclick={restartGame}>SUIVANT</button>
|
|
{/if}
|
|
</div>
|
|
{/await}
|
|
|
|
<span class="results" hidden={!results}>
|
|
{results?.score} points! Vous etiez à {results?.distance}m.
|
|
</span>
|
|
|
|
<div id="map"></div>
|
|
|
|
<style>
|
|
:global(body),
|
|
:global(html) {
|
|
margin: 0;
|
|
height: 100%;
|
|
}
|
|
|
|
:global(.contents) {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
button {
|
|
padding: 10px;
|
|
}
|
|
|
|
.results {
|
|
color: green;
|
|
font-size: 20px;
|
|
}
|
|
|
|
#map {
|
|
flex-grow: 1;
|
|
width: 100%;
|
|
}
|
|
</style>
|