feat: add difficulty selector
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
2255355f21
commit
c3dec90264
7 changed files with 175 additions and 139 deletions
|
@ -1,13 +1,16 @@
|
||||||
|
export interface GameOptions {
|
||||||
|
mode: "easy" | "hard" | "extreme demon ultra miguel";
|
||||||
|
}
|
||||||
|
|
||||||
export interface GameData {
|
export interface GameData {
|
||||||
center: [number, number];
|
|
||||||
lines: [GeoJSON.Feature, string][];
|
lines: [GeoJSON.Feature, string][];
|
||||||
stops: GeoJSON.Feature[];
|
stops: GeoJSON.Feature[];
|
||||||
stopName: string;
|
stopName: string;
|
||||||
stopId: string;
|
gameId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckData {
|
export interface CheckData {
|
||||||
stopId: string;
|
gameId: string;
|
||||||
latlng: [number, number];
|
latlng: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +30,20 @@ const stopsUrl =
|
||||||
let lazyLines: GeoJSON.FeatureCollection | null = null;
|
let lazyLines: GeoJSON.FeatureCollection | null = null;
|
||||||
let lazyStops: GeoJSON.Feature[] | null = null;
|
let lazyStops: GeoJSON.Feature[] | null = null;
|
||||||
|
|
||||||
|
const games: Record<string, GeoJSON.Feature> = {};
|
||||||
|
|
||||||
|
export function createGame(stop: GeoJSON.Feature): string {
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
games[uuid] = stop;
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopGame(uuid: string): GeoJSON.Feature | null {
|
||||||
|
const stop = games[uuid];
|
||||||
|
delete games[uuid];
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLines(fetch: FetchType): Promise<GeoJSON.FeatureCollection> {
|
export async function getLines(fetch: FetchType): Promise<GeoJSON.FeatureCollection> {
|
||||||
return lazyLines ?? (lazyLines = await fetch(linesUrl).then((r) => r.json()));
|
return lazyLines ?? (lazyLines = await fetch(linesUrl).then((r) => r.json()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,127 +1,37 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from "./$types";
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
import L from "leaflet";
|
|
||||||
|
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
|
||||||
import "leaflet-defaulticon-compatibility";
|
|
||||||
import type { CheckData, CheckResponse } from "$lib";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: PageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { data }: Props = $props();
|
|
||||||
|
|
||||||
const hidden = !$page.url.searchParams.has("debug");
|
|
||||||
const center = L.latLng(data.gameData.center);
|
|
||||||
|
|
||||||
const linesJson = data.gameData.lines.map(([feature, color]) => {
|
|
||||||
return L.geoJSON(feature, { style: { color } });
|
|
||||||
});
|
|
||||||
const pointsJson = data.gameData.stops.map((f) => L.geoJSON(f));
|
|
||||||
|
|
||||||
let latlng = $state(L.latLng(0, 0));
|
|
||||||
let results: CheckResponse | null = $state(null);
|
|
||||||
|
|
||||||
let lines = $state(true);
|
|
||||||
let labels = $state(false);
|
|
||||||
|
|
||||||
let map: L.Map | null = $state(null);
|
|
||||||
let playerMarker: L.Marker | null = $state(null);
|
|
||||||
|
|
||||||
let tileLayer = $derived(
|
|
||||||
L.tileLayer(
|
|
||||||
`https://basemaps.cartocdn.com/light_${labels ? "all" : "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,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (!map) return;
|
|
||||||
tileLayer.addTo(map);
|
|
||||||
|
|
||||||
if (lines) {
|
|
||||||
linesJson.forEach((line) => line.addTo(map!));
|
|
||||||
pointsJson.forEach((p) => p.addTo(map!));
|
|
||||||
} else {
|
|
||||||
linesJson.forEach((line) => line.removeFrom(map!));
|
|
||||||
pointsJson.forEach((p) => p.removeFrom(map!));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function createMap(node: HTMLElement) {
|
|
||||||
map = L.map(node).setView(center, 13);
|
|
||||||
playerMarker = L.marker([0, 0]).addTo(map);
|
|
||||||
|
|
||||||
map.on("click", (e) => {
|
|
||||||
if (map && playerMarker && !results) {
|
|
||||||
playerMarker.setLatLng(e.latlng);
|
|
||||||
latlng = e.latlng;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkLocation() {
|
|
||||||
const checkData: CheckData = {
|
|
||||||
stopId: data.gameData.stopId,
|
|
||||||
latlng: [latlng.lat, latlng.lng],
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch("/api/check", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(checkData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok && map) {
|
|
||||||
const res: CheckResponse = await response.json();
|
|
||||||
|
|
||||||
L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
|
|
||||||
results = res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>{data.gameData.stopName}</h1>
|
<h1>TCL-Guessr</h1>
|
||||||
|
|
||||||
<div hidden={results !== null}>
|
<form action="/game" method="GET">
|
||||||
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button>
|
<label>
|
||||||
</div>
|
difficulté: <select name="mode">
|
||||||
|
<option value="easy">Facile (pour les nuls)</option>
|
||||||
|
<option value="hard">Dur (pour les gigaillards)</option>
|
||||||
|
<option value="extreme demon ultra miguel">
|
||||||
|
EXTREME DEMON ULTRA MIGUEL DE LA MORT QUI TUE
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="results" hidden={!results}>
|
<input type="submit" value="LANCER LA PARTIE" />
|
||||||
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
|
</form>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div {hidden}>
|
|
||||||
<label>lines: <input type="checkbox" bind:checked={lines} /></label>
|
|
||||||
<label>labels: <input type="checkbox" bind:checked={labels} /></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="map" use:createMap></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
height: 100%;
|
width: fit-content;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.results {
|
input[type="submit"] {
|
||||||
color: green;
|
height: 50px;
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import type { PageLoad } from "./$types";
|
|
||||||
import type { GameData } from "$lib";
|
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch }) => {
|
|
||||||
const res = await fetch("/api/data");
|
|
||||||
const gameData: GameData = await res.json();
|
|
||||||
|
|
||||||
return { gameData };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ssr = false;
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { getStops, type CheckData, type CheckResponse } from "$lib";
|
import { stopGame, type CheckData, type CheckResponse } from "$lib";
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ fetch, request }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const stops = await getStops(fetch);
|
|
||||||
|
|
||||||
const data: CheckData = await request.json();
|
const data: CheckData = await request.json();
|
||||||
const stop = stops.find((f) => f.id === data.stopId);
|
const stop = stopGame(data.gameId);
|
||||||
|
|
||||||
if (stop) {
|
if (stop) {
|
||||||
// GeoJSON data is LonLat, not LatLon
|
// GeoJSON data is LonLat, not LatLon
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import type { RequestHandler } from "./$types";
|
import type { RequestHandler } from "./$types";
|
||||||
import { getLines, getStops, type GameData } from "$lib";
|
import { createGame, getLines, getStops, type GameData, type GameOptions } from "$lib";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ fetch }) => {
|
export const GET: RequestHandler = async ({ fetch, url }) => {
|
||||||
const lines = await getLines(fetch);
|
const lines = await getLines(fetch);
|
||||||
const stops = await getStops(fetch);
|
const stops = await getStops(fetch);
|
||||||
|
|
||||||
const bbox = lines.bbox ?? [0, 0, 0, 0];
|
const mode = url.searchParams.get("mode");
|
||||||
const centerLat = (bbox[1] + bbox[3]) / 2;
|
if (mode !== "easy" && mode !== "hard" && mode !== "extreme demon ultra miguel") {
|
||||||
const centerLon = (bbox[0] + bbox[2]) / 2;
|
return error(400, "gamemode is invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: GameOptions = { mode };
|
||||||
|
|
||||||
const lineColors: [GeoJSON.Feature, string][] = lines.features.map((f) => {
|
const lineColors: [GeoJSON.Feature, string][] = lines.features.map((f) => {
|
||||||
const components = f.properties!.couleur!.split(" ");
|
const components = f.properties!.couleur!.split(" ");
|
||||||
|
@ -28,13 +32,13 @@ export const GET: RequestHandler = async ({ fetch }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
|
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
|
||||||
|
const gameId = createGame(randomStop);
|
||||||
|
|
||||||
const data: GameData = {
|
const data: GameData = {
|
||||||
center: [centerLat, centerLon],
|
lines: options.mode === "easy" || options.mode === "hard" ? lineColors : [],
|
||||||
lines: lineColors,
|
stops: options.mode === "easy" ? crossingStops : [],
|
||||||
stops: crossingStops,
|
|
||||||
stopName: randomStop.properties!.nom,
|
stopName: randomStop.properties!.nom,
|
||||||
stopId: randomStop.id!.toString(),
|
gameId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
|
|
102
src/routes/game/+page.svelte
Normal file
102
src/routes/game/+page.svelte
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { CheckData, CheckResponse } from "$lib";
|
||||||
|
import type { PageData } from "./$types";
|
||||||
|
import L from "leaflet";
|
||||||
|
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||||
|
import "leaflet-defaulticon-compatibility";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
const center = L.latLng(45.742858495, 4.86163814);
|
||||||
|
|
||||||
|
let latlng = $state(L.latLng(0, 0));
|
||||||
|
let results: CheckResponse | null = $state(null);
|
||||||
|
|
||||||
|
let map: L.Map | null = $state(null);
|
||||||
|
|
||||||
|
function createMap(node: HTMLElement) {
|
||||||
|
map = L.map(node).setView(center, 13);
|
||||||
|
|
||||||
|
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
|
||||||
|
data.gameData.lines.forEach(([feature, color]) => {
|
||||||
|
L.geoJSON(feature, { style: { color } }).addTo(map!);
|
||||||
|
});
|
||||||
|
data.gameData.stops.forEach((f) => L.geoJSON(f).addTo(map!));
|
||||||
|
|
||||||
|
const playerMarker = L.marker([0, 0]).addTo(map);
|
||||||
|
|
||||||
|
map.on("click", (e) => {
|
||||||
|
if (map && playerMarker && !results) {
|
||||||
|
playerMarker.setLatLng(e.latlng);
|
||||||
|
latlng = e.latlng;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLocation() {
|
||||||
|
const checkData: CheckData = {
|
||||||
|
gameId: data.gameData.gameId,
|
||||||
|
latlng: [latlng.lat, latlng.lng],
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("/api/check", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(checkData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok && map) {
|
||||||
|
const res: CheckResponse = await response.json();
|
||||||
|
|
||||||
|
L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
|
||||||
|
results = res;
|
||||||
|
} else {
|
||||||
|
alert("you dirty little cheater");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<h1>{data.gameData.stopName}</h1>
|
||||||
|
|
||||||
|
<div hidden={results !== null}>
|
||||||
|
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
color: green;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
16
src/routes/game/+page.ts
Normal file
16
src/routes/game/+page.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { PageLoad } from "./$types";
|
||||||
|
import type { GameData } from "$lib";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ fetch, url }) => {
|
||||||
|
const res = await fetch("/api/data?" + url.searchParams);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return error(400, await res.text());
|
||||||
|
} else {
|
||||||
|
const gameData: GameData = await res.json();
|
||||||
|
return { gameData };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ssr = false;
|
Loading…
Add table
Add a link
Reference in a new issue