feat: add restart button
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s
This commit is contained in:
parent
c06553fb74
commit
89a53862d9
5 changed files with 151 additions and 79 deletions
|
@ -1,3 +1,6 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { GameData, GameOptions } from "./types";
|
||||
|
||||
type FetchType = typeof fetch;
|
||||
|
||||
const metroUrl =
|
||||
|
@ -11,12 +14,18 @@ let lazyMetro: [GeoJSON.Feature, string][] | null = null;
|
|||
let lazyTram: [GeoJSON.Feature, string][] | null = null;
|
||||
let lazyStops: GeoJSON.Feature[] | null = null;
|
||||
|
||||
export async function createGame(stop: GeoJSON.Feature, kv: KVNamespace<string>): Promise<string> {
|
||||
export async function createGame(
|
||||
stop: GeoJSON.Feature,
|
||||
kv: KVNamespace<string>,
|
||||
): Promise<GameData> {
|
||||
const uuid = crypto.randomUUID();
|
||||
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(stop), { expirationTtl: 600 });
|
||||
|
||||
return uuid;
|
||||
return {
|
||||
gameId: uuid,
|
||||
stopName: stop.properties!.nom,
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopGame(
|
||||
|
@ -105,3 +114,16 @@ export async function getMergedStops(
|
|||
|
||||
return mergedStops;
|
||||
}
|
||||
|
||||
export function parseOptions(url: URL): GameOptions {
|
||||
const mode = url.searchParams.get("mode");
|
||||
if (mode !== "easy" && mode !== "hard" && mode !== "extreme demon ultra miguel") {
|
||||
error(400, "gamemode is invalid");
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
metro: url.searchParams.has("metro"),
|
||||
tram: url.searchParams.has("tram"),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ export interface GameOptions {
|
|||
tram: boolean;
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
export interface MapData {
|
||||
lines: [GeoJSON.Feature, string][];
|
||||
stops: [number, number][];
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
stopName: string;
|
||||
gameId: string;
|
||||
}
|
||||
|
|
27
src/routes/api/game/+server.ts
Normal file
27
src/routes/api/game/+server.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { createGame, getMergedStops, getMetro, getTram, parseOptions } from "$lib";
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch, url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
|
||||
const options = parseOptions(url);
|
||||
const lineColors: [GeoJSON.Feature, string][] = [];
|
||||
|
||||
if (options.metro) lineColors.push(...(await getMetro(fetch)));
|
||||
if (options.tram) lineColors.push(...(await getTram(fetch)));
|
||||
|
||||
const lineCodes = new Set<string>(lineColors.map(([f]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
|
||||
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
|
||||
|
||||
if (!randomStop) {
|
||||
error(400, "could not select random stop");
|
||||
}
|
||||
|
||||
const gameData = await createGame(randomStop, kv);
|
||||
|
||||
return Response.json(gameData);
|
||||
};
|
|
@ -1,49 +1,31 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import { createGame, getMergedStops, getMetro, getTram } from "$lib";
|
||||
import { getMergedStops, getMetro, getTram, parseOptions } from "$lib";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import type { GameData, GameOptions } from "$lib/types";
|
||||
import type { MapData } from "$lib/types";
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const options = parseOptions(url);
|
||||
const mapData: MapData = { lines: [], stops: [] };
|
||||
|
||||
const mode = url.searchParams.get("mode");
|
||||
if (mode !== "easy" && mode !== "hard" && mode !== "extreme demon ultra miguel") {
|
||||
error(400, "gamemode is invalid");
|
||||
if (options.mode !== "extreme demon ultra miguel") {
|
||||
const lineColors: [GeoJSON.Feature, string][] = [];
|
||||
|
||||
if (options.metro) lineColors.push(...(await getMetro(fetch)));
|
||||
if (options.tram) lineColors.push(...(await getTram(fetch)));
|
||||
|
||||
mapData.lines = lineColors;
|
||||
|
||||
if (options.mode === "easy") {
|
||||
const lineCodes = new Set<string>(lineColors.map(([f]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
|
||||
const strippedStops: [number, number][] = crossingStops.map((f) => {
|
||||
const coords = (f.geometry as GeoJSON.Point).coordinates;
|
||||
return [coords[1], coords[0]];
|
||||
});
|
||||
|
||||
mapData.stops = strippedStops;
|
||||
}
|
||||
}
|
||||
|
||||
const options: GameOptions = {
|
||||
mode,
|
||||
metro: url.searchParams.has("metro"),
|
||||
tram: url.searchParams.has("tram"),
|
||||
};
|
||||
|
||||
const lineColors: [GeoJSON.Feature, string][] = [];
|
||||
|
||||
if (options.metro) lineColors.push(...(await getMetro(fetch)));
|
||||
if (options.tram) lineColors.push(...(await getTram(fetch)));
|
||||
|
||||
const lineCodes = new Set<string>(lineColors.map(([f, _]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
|
||||
const strippedStops: [number, number][] = crossingStops.map((f) => {
|
||||
const coords = (f.geometry as GeoJSON.Point).coordinates;
|
||||
return [coords[1], coords[0]];
|
||||
});
|
||||
|
||||
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
|
||||
|
||||
if (!randomStop) {
|
||||
error(400, "could not select random stop");
|
||||
}
|
||||
|
||||
const gameId = await createGame(randomStop, kv);
|
||||
const gameData: GameData = {
|
||||
lines: options.mode === "easy" || options.mode === "hard" ? lineColors : [],
|
||||
stops: options.mode === "easy" ? strippedStops : [],
|
||||
stopName: randomStop.properties!.nom,
|
||||
gameId,
|
||||
};
|
||||
|
||||
return { gameData };
|
||||
return { mapData };
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import type { CheckData, CheckResponse } from "$lib/types";
|
||||
import type { CheckData, CheckResponse, GameData } from "$lib/types";
|
||||
import type { PageData } from "./$types";
|
||||
import { page } from "$app/stores";
|
||||
import L from "leaflet";
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
@ -13,22 +14,23 @@
|
|||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const zoom = 13;
|
||||
const center = L.latLng(45.742858495, 4.86163814);
|
||||
|
||||
let latlng = $state(L.latLng(0, 0));
|
||||
let gamePromise = $state(fetchGame());
|
||||
let results: CheckResponse | null = $state(null);
|
||||
|
||||
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);
|
||||
|
||||
async function fetchGame(): Promise<GameData> {
|
||||
return fetch("/api/game?" + $page.url.searchParams).then((r) => r.json());
|
||||
}
|
||||
|
||||
function createMap(node: HTMLElement) {
|
||||
map = L.map(node).setView(center, 13);
|
||||
|
||||
function setMarker(pos: L.LatLng) {
|
||||
if (map && playerMarker && !results) {
|
||||
playerMarker.setLatLng(pos);
|
||||
latlng = pos;
|
||||
}
|
||||
}
|
||||
map = L.map(node).setView(center, zoom);
|
||||
|
||||
L.tileLayer(`https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png`, {
|
||||
attribution:
|
||||
|
@ -37,28 +39,40 @@
|
|||
}).addTo(map);
|
||||
|
||||
// we know map isn't null
|
||||
data.gameData.lines.forEach(([feature, color]) => {
|
||||
data.mapData.lines.forEach(([feature, color]) => {
|
||||
L.geoJSON(feature, { style: { color } }).addTo(map!);
|
||||
});
|
||||
data.gameData.stops.forEach((coords) => {
|
||||
data.mapData.stops.forEach((coords) => {
|
||||
const marker = L.marker(coords).addTo(map!);
|
||||
marker.on("click", (e) => setMarker(e.latlng));
|
||||
});
|
||||
|
||||
const playerMarker = L.marker([0, 0]).addTo(map);
|
||||
|
||||
map.on("click", (e) => setMarker(e.latlng));
|
||||
map.on("keydown", (e) => {
|
||||
if (!results && latlng.lat !== 0 && latlng.lng !== 0 && e.originalEvent.key === " ") {
|
||||
if (e.originalEvent.key === " ") {
|
||||
checkLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setMarker(pos: L.LatLng) {
|
||||
if (map && !results) {
|
||||
if (playerMarker) {
|
||||
playerMarker = playerMarker.setLatLng(pos);
|
||||
} else {
|
||||
playerMarker = L.marker(pos).addTo(map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkLocation() {
|
||||
if (!playerMarker || results) return;
|
||||
|
||||
const gameData = await gamePromise;
|
||||
|
||||
const checkData: CheckData = {
|
||||
gameId: data.gameData.gameId,
|
||||
latlng: [latlng.lat, latlng.lng],
|
||||
gameId: gameData.gameId,
|
||||
latlng: [playerMarker.getLatLng().lat, playerMarker.getLatLng().lng],
|
||||
};
|
||||
|
||||
const response = await fetch("/api/check", {
|
||||
|
@ -68,35 +82,59 @@
|
|||
|
||||
if (response.ok && map) {
|
||||
const res: CheckResponse = await response.json();
|
||||
results = res;
|
||||
|
||||
const line = L.polyline([checkData.latlng, res.solution], {
|
||||
solutionLine = L.polyline([checkData.latlng, res.solution], {
|
||||
color: "black",
|
||||
weight: 6,
|
||||
}).addTo(map);
|
||||
map.flyToBounds(line.getBounds());
|
||||
|
||||
L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
|
||||
map.flyToBounds(solutionLine.getBounds(), { duration: 1 });
|
||||
|
||||
results = res;
|
||||
solutionMarker = L.marker(res.solution).bindPopup(gameData.stopName).addTo(map).openPopup();
|
||||
} else {
|
||||
alert("you dirty little cheater");
|
||||
alert("it seems than an error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
if (!map) return;
|
||||
|
||||
playerMarker?.removeFrom(map);
|
||||
playerMarker = null;
|
||||
|
||||
solutionLine?.removeFrom(map);
|
||||
solutionLine = null;
|
||||
|
||||
solutionMarker?.removeFrom(map);
|
||||
solutionMarker = null;
|
||||
|
||||
map.setView(center, zoom);
|
||||
results = null;
|
||||
|
||||
gamePromise = fetchGame();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>{data.gameData.stopName}</h1>
|
||||
{#await gamePromise then gameData}
|
||||
<div class="container">
|
||||
<h1>{gameData.stopName}</h1>
|
||||
|
||||
<div hidden={results !== null}>
|
||||
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button>
|
||||
<div>
|
||||
{#if results === null}
|
||||
<button onclick={checkLocation} disabled={!playerMarker}>SUBMIT</button>
|
||||
{:else}
|
||||
<button onclick={restartGame}>RESTART</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<span class="results" hidden={!results}>
|
||||
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
|
||||
</span>
|
||||
|
||||
<div id="map" use:createMap></div>
|
||||
</div>
|
||||
|
||||
<div class="results" hidden={!results}>
|
||||
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
|
||||
</div>
|
||||
|
||||
<div id="map" use:createMap></div>
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue